mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-10 17:25:09 +03:00
Compare commits
66 Commits
32bcbbdb2d
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12b5cc7f72 | ||
|
|
480e9e8234 | ||
|
|
29e1852a68 | ||
|
|
8314335b31 | ||
|
|
acb9f63e1d | ||
|
|
005a8b4a04 | ||
|
|
e2149974cc | ||
|
|
6c578292e8 | ||
|
|
28930c0463 | ||
|
|
37141afece | ||
|
|
493b14ba78 | ||
|
|
cf1de4de62 | ||
|
|
9fdb8410d3 | ||
|
|
a2b79462ab | ||
|
|
dce221be5a | ||
|
|
9d1ab3fba3 | ||
|
|
b3bd18845d | ||
|
|
435f6ec61d | ||
|
|
0497814004 | ||
|
|
4b1ef9e20d | ||
|
|
10d5250d23 | ||
|
|
2ee580d49d | ||
|
|
4a54029cac | ||
|
|
001848bf2f | ||
|
|
989bf80fe8 | ||
|
|
78b5f47668 | ||
|
|
97e9e44faa | ||
|
|
ff226f6d80 | ||
|
|
0cbdb6ffb3 | ||
|
|
b8117c5c34 | ||
|
|
a69614d464 | ||
|
|
58ee593e26 | ||
|
|
09bc9056c9 | ||
|
|
0c6df924d1 | ||
|
|
456817b4f4 | ||
|
|
16570ee34f | ||
|
|
2b40c61d8e | ||
|
|
dcc64cdeae | ||
|
|
2747d3d8b4 | ||
|
|
3c574a4182 | ||
|
|
311d4708e5 | ||
|
|
5cf4323d07 | ||
|
|
3976701ac6 | ||
|
|
9ded8d6ab2 | ||
|
|
cd2fff0655 | ||
|
|
10f61ffdc2 | ||
|
|
d72952bf93 | ||
|
|
a7c55db9ac | ||
|
|
bff47e2b81 | ||
|
|
3d478c4935 | ||
|
|
a658e987b7 | ||
|
|
7c8b0adc1e | ||
|
|
a732ebc3e1 | ||
|
|
30c0867e40 | ||
|
|
8f50ea64dc | ||
|
|
0797ebb695 | ||
|
|
c9391fb894 | ||
|
|
8a955888bf | ||
|
|
36e812e550 | ||
|
|
8baa995c7a | ||
|
|
f4a0535289 | ||
|
|
6665242edf | ||
|
|
3cdf1cce54 | ||
|
|
88ae00ba73 | ||
|
|
7c26575dbd | ||
|
|
93d064a9b0 |
@@ -2,6 +2,8 @@
|
|||||||
rustflags = ["-Ctarget-feature=+crt-static"]
|
rustflags = ["-Ctarget-feature=+crt-static"]
|
||||||
[target.i686-pc-windows-msvc]
|
[target.i686-pc-windows-msvc]
|
||||||
rustflags = ["-C", "target-feature=+crt-static", "-C", "link-args=/NODEFAULTLIB:MSVCRT"]
|
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")']
|
[target.'cfg(target_os="macos")']
|
||||||
rustflags = [
|
rustflags = [
|
||||||
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
"-C", "link-args=-sectcreate __CGPreLoginApp __cgpreloginapp /dev/null",
|
||||||
|
|||||||
39
.github/patches/apply_flutter_3.44_source_patches.sh
vendored
Normal file
39
.github/patches/apply_flutter_3.44_source_patches.sh
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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
|
||||||
28
.github/workflows/bridge.yml
vendored
28
.github/workflows/bridge.yml
vendored
@@ -7,7 +7,6 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_EXPAND_VERSION: "1.0.95"
|
CARGO_EXPAND_VERSION: "1.0.95"
|
||||||
FLUTTER_VERSION: "3.22.3"
|
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
|
|
||||||
@@ -18,10 +17,21 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
job:
|
job:
|
||||||
|
# Default bridge for every platform still on Flutter 3.24.5 (generated with 3.22.3).
|
||||||
- {
|
- {
|
||||||
target: x86_64-unknown-linux-gnu,
|
target: x86_64-unknown-linux-gnu,
|
||||||
os: ubuntu-22.04,
|
os: ubuntu-22.04,
|
||||||
extra-build-args: "",
|
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:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
@@ -64,13 +74,13 @@ jobs:
|
|||||||
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
|
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
|
||||||
with:
|
with:
|
||||||
path: /tmp/flutter_rust_bridge
|
path: /tmp/flutter_rust_bridge
|
||||||
key: vcpkg-${{ matrix.job.arch }}
|
key: bridge-${{ matrix.job.flutter-version }}
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ matrix.job.flutter-version }}
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install flutter rust bridge deps
|
- name: Install flutter rust bridge deps
|
||||||
@@ -78,7 +88,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked
|
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
|
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
|
||||||
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
|
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
|
||||||
|
|
||||||
- name: Run flutter rust bridge
|
- name: Run flutter rust bridge
|
||||||
run: |
|
run: |
|
||||||
@@ -88,7 +106,7 @@ jobs:
|
|||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: ${{ matrix.job.artifact-name }}
|
||||||
path: |
|
path: |
|
||||||
./src/bridge_generated.rs
|
./src/bridge_generated.rs
|
||||||
./src/bridge_generated.io.rs
|
./src/bridge_generated.io.rs
|
||||||
|
|||||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -81,6 +81,7 @@ jobs:
|
|||||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||||
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||||
# - { target: x86_64-pc-windows-msvc , 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-gnu , os: ubuntu-24.04 }
|
||||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
129
.github/workflows/flutter-build.yml
vendored
129
.github/workflows/flutter-build.yml
vendored
@@ -27,6 +27,11 @@ env:
|
|||||||
LLVM_VERSION: "15.0.6"
|
LLVM_VERSION: "15.0.6"
|
||||||
FLUTTER_VERSION: "3.24.5"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
ANDROID_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
|
# for arm64 linux because official Dart SDK does not work
|
||||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||||
@@ -39,7 +44,7 @@ env:
|
|||||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
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
|
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.9"
|
||||||
NDK_VERSION: "r28c"
|
NDK_VERSION: "r28c"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -53,14 +58,24 @@ jobs:
|
|||||||
|
|
||||||
build-RustDeskTempTopMostWindow:
|
build-RustDeskTempTopMostWindow:
|
||||||
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
|
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
|
||||||
with:
|
|
||||||
upload-artifact: ${{ inputs.upload-artifact }}
|
|
||||||
target: windows-2022
|
|
||||||
configuration: Release
|
|
||||||
platform: x64
|
|
||||||
target_version: Windows10
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
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 }}
|
||||||
|
configuration: Release
|
||||||
|
platform: ${{ matrix.job.platform }}
|
||||||
|
target_version: Windows10
|
||||||
|
|
||||||
build-for-windows-flutter:
|
build-for-windows-flutter:
|
||||||
name: ${{ matrix.job.target }}
|
name: ${{ matrix.job.target }}
|
||||||
@@ -76,9 +91,20 @@ jobs:
|
|||||||
target: x86_64-pc-windows-msvc,
|
target: x86_64-pc-windows-msvc,
|
||||||
os: windows-2022,
|
os: windows-2022,
|
||||||
arch: x86_64,
|
arch: x86_64,
|
||||||
|
flutter-arch: x64,
|
||||||
vcpkg-triplet: x64-windows-static,
|
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:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
@@ -95,36 +121,91 @@ jobs:
|
|||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
# 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' }}
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Install LLVM and Clang
|
- name: Install LLVM and Clang
|
||||||
uses: KyleMayes/install-llvm-action@1a3da29f56261a1e1f937ec88f0856a9b8321d7e # v1
|
uses: KyleMayes/install-llvm-action@ebc0426251bc40c7cd31162802432c68818ab8f0 # v2.0.9
|
||||||
with:
|
with:
|
||||||
version: ${{ env.LLVM_VERSION }}
|
version: ${{ env.LLVM_VERSION }}
|
||||||
|
|
||||||
- name: Install flutter
|
- 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
|
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
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
|
||||||
|
}
|
||||||
|
|
||||||
# https://github.com/flutter/flutter/issues/155685
|
# 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
|
- name: Replace engine with rustdesk custom flutter engine
|
||||||
|
if: ${{ matrix.job.arch == 'x86_64' }}
|
||||||
run: |
|
run: |
|
||||||
flutter doctor -v
|
flutter doctor -v
|
||||||
flutter precache --windows
|
flutter precache --windows
|
||||||
Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
|
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
|
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
|
- 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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
|
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
|
||||||
cd $(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
|
[[ "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
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
@@ -163,11 +244,19 @@ jobs:
|
|||||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
|
||||||
shell: bash
|
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
|
- name: Build rustdesk
|
||||||
run: |
|
run: |
|
||||||
# Windows: build RustDesk
|
# Windows: build RustDesk
|
||||||
python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack
|
# --hwcodec is shared by all Windows targets; per-target extras (e.g. --vram) come from the matrix
|
||||||
mv ./flutter/build/windows/x64/runner/Release ./rustdesk
|
python3 .\build.py --portable --flutter --skip-portable-pack --hwcodec ${{ matrix.job.build-args }}
|
||||||
|
mv ./flutter/build/windows/${{ matrix.job.flutter-arch }}/runner/Release ./rustdesk
|
||||||
|
|
||||||
# Download usbmmidd_v2.zip and extract it to ./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
|
Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip
|
||||||
@@ -223,7 +312,7 @@ jobs:
|
|||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
if: ${{ inputs.upload-artifact }}
|
if: ${{ inputs.upload-artifact }}
|
||||||
with:
|
with:
|
||||||
name: topmostwindow-artifacts
|
name: ${{ matrix.job.arch == 'aarch64' && 'topmostwindow-artifacts-ARM64' || 'topmostwindow-artifacts-x64' }}
|
||||||
path: "./rustdesk"
|
path: "./rustdesk"
|
||||||
|
|
||||||
- name: Upload unsigned
|
- name: Upload unsigned
|
||||||
@@ -256,13 +345,18 @@ jobs:
|
|||||||
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
|
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
|
||||||
|
|
||||||
- name: Build msi
|
- 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'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
run: |
|
run: |
|
||||||
pushd ./res/msi
|
pushd ./res/msi
|
||||||
python preprocess.py --arp -d ../../rustdesk
|
python preprocess.py --arp -d ../../rustdesk
|
||||||
nuget restore msi.sln
|
nuget restore msi.sln
|
||||||
msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10
|
$msiPlatform = if ('${{ matrix.job.arch }}' -eq 'aarch64') { 'ARM64' } else { 'x64' }
|
||||||
mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
|
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
|
||||||
sha256sum ../../SignOutput/rustdesk-*.msi
|
sha256sum ../../SignOutput/rustdesk-*.msi
|
||||||
|
|
||||||
- name: Sign rustdesk self-extracted file
|
- name: Sign rustdesk self-extracted file
|
||||||
@@ -1890,6 +1984,7 @@ jobs:
|
|||||||
sudo apt-get install -y libarchive-tools libfuse2
|
sudo apt-get install -y libarchive-tools libfuse2
|
||||||
# set-up appimage-builder
|
# set-up appimage-builder
|
||||||
# https://github.com/AppImage/AppImageKit/issues/1395
|
# https://github.com/AppImage/AppImageKit/issues/1395
|
||||||
|
sudo pip3 install "setuptools_scm<10"
|
||||||
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
|
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
|
||||||
# run appimage-builder
|
# run appimage-builder
|
||||||
pushd appimage
|
pushd appimage
|
||||||
|
|||||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
|||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
VERSION: "1.4.7"
|
VERSION: "1.4.9"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
|||||||
@@ -45,16 +45,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
|
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
|
||||||
|
|
||||||
# Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3
|
|
||||||
- name: Build the project
|
- name: Build the project
|
||||||
run: |
|
run: |
|
||||||
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
|
cd RustDeskTempTopMostWindow && git checkout ecd8d6a139eee76845ea66423fb739af450fda90
|
||||||
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
|
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
|
||||||
|
|
||||||
- name: Archive build artifacts
|
- name: Archive build artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: ${{ inputs.upload-artifact }}
|
if: ${{ inputs.upload-artifact }}
|
||||||
with:
|
with:
|
||||||
name: topmostwindow-artifacts
|
name: topmostwindow-artifacts-${{ inputs.platform }}
|
||||||
path: |
|
path: |
|
||||||
./${{ env.build_output_dir }}/WindowInjection.dll
|
./${{ env.build_output_dir }}/WindowInjection.dll
|
||||||
|
|||||||
85
.github/workflows/wf-cliprdr-ci.yml
vendored
85
.github/workflows/wf-cliprdr-ci.yml
vendored
@@ -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
|
|
||||||
62
Cargo.lock
generated
62
Cargo.lock
generated
@@ -1324,7 +1324,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-master"
|
name = "clipboard-master"
|
||||||
version = "4.0.0-beta.6"
|
version = "4.0.0-beta.6"
|
||||||
source = "git+https://github.com/rustdesk-org/clipboard-master#ddc39f00a6211959489ae683aa6ae6eedf03a809"
|
source = "git+https://github.com/rustdesk-org/clipboard-master#7762d74e38db37cfeb6ded88c964b9cdbddfb6db"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"objc",
|
"objc",
|
||||||
"objc-foundation",
|
"objc-foundation",
|
||||||
@@ -2329,7 +2329,7 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading 0.8.4",
|
"libloading 0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2694,7 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3952,7 +3952,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "hwcodec"
|
name = "hwcodec"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/rustdesk-org/hwcodec#398e5a8938dd8768ade0fcdc27ea80e8b4b38738"
|
source = "git+https://github.com/rustdesk-org/hwcodec#778df1f99597722473b29443bac22ae6c23946fe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -4494,7 +4494,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4695,7 +4695,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "magnum-opus"
|
name = "magnum-opus"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/rustdesk-org/magnum-opus#5cd2bf989c148662fa3a2d9d539a71d71fd1d256"
|
source = "git+https://github.com/rustdesk-org/magnum-opus#588c6e1f9ed50c3a01fa64f3bd3e7cdb0378a114"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
@@ -6588,7 +6588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8"
|
checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.10",
|
"cfg-if 0.1.10",
|
||||||
"rpassword 2.1.0",
|
"rpassword",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"termios 0.3.3",
|
"termios 0.3.3",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
@@ -6673,7 +6673,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6920,7 +6920,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rdev"
|
name = "rdev"
|
||||||
version = "0.5.0-2"
|
version = "0.5.0-2"
|
||||||
source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8"
|
source = "git+https://github.com/rustdesk-org/rdev#871bf1c856d6a30af2f56ab8848396a025140855"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa 0.24.1",
|
"cocoa 0.24.1",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
@@ -7152,17 +7152,6 @@ dependencies = [
|
|||||||
"winapi 0.2.8",
|
"winapi 0.2.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rpassword"
|
|
||||||
version = "7.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rtoolbox",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtcp"
|
name = "rtcp"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -7174,16 +7163,6 @@ dependencies = [
|
|||||||
"webrtc-util",
|
"webrtc-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtoolbox"
|
|
||||||
version = "0.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtp"
|
name = "rtp"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -7270,7 +7249,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.7"
|
version = "1.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7283,7 +7262,6 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cidr-utils",
|
"cidr-utils",
|
||||||
"clap 4.5.53",
|
|
||||||
"clipboard",
|
"clipboard",
|
||||||
"clipboard-master",
|
"clipboard-master",
|
||||||
"cocoa 0.24.1",
|
"cocoa 0.24.1",
|
||||||
@@ -7341,7 +7319,6 @@ dependencies = [
|
|||||||
"repng",
|
"repng",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ringbuf",
|
"ringbuf",
|
||||||
"rpassword 7.3.1",
|
|
||||||
"rubato",
|
"rubato",
|
||||||
"runas",
|
"runas",
|
||||||
"rust-pulsectl",
|
"rust-pulsectl",
|
||||||
@@ -7385,7 +7362,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.7"
|
version = "1.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -7457,7 +7434,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.11.0",
|
"linux-raw-sys 0.11.0",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7514,7 +7491,7 @@ dependencies = [
|
|||||||
"security-framework 3.5.1",
|
"security-framework 3.5.1",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9733,9 +9710,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols-wlr"
|
name = "wayland-protocols-wlr"
|
||||||
version = "0.3.3"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
|
checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -10838,16 +10815,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wl-clipboard-rs"
|
name = "wl-clipboard-rs"
|
||||||
version = "0.9.0"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f"
|
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"os_pipe",
|
"os_pipe",
|
||||||
"rustix 0.38.34",
|
"rustix 1.1.2",
|
||||||
"tempfile",
|
"thiserror 2.0.17",
|
||||||
"thiserror 1.0.61",
|
|
||||||
"tree_magic_mini",
|
"tree_magic_mini",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.7"
|
version = "1.4.9"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -22,7 +22,6 @@ path = "src/service.rs"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
inline = []
|
inline = []
|
||||||
cli = []
|
|
||||||
use_samplerate = ["samplerate"]
|
use_samplerate = ["samplerate"]
|
||||||
use_rubato = ["rubato"]
|
use_rubato = ["rubato"]
|
||||||
use_dasp = ["dasp"]
|
use_dasp = ["dasp"]
|
||||||
@@ -62,8 +61,6 @@ dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpol
|
|||||||
rubato = { version = "0.12", optional = true }
|
rubato = { version = "0.12", optional = true }
|
||||||
samplerate = { version = "0.2", optional = true }
|
samplerate = { version = "0.2", optional = true }
|
||||||
uuid = { version = "1.3", features = ["v4"] }
|
uuid = { version = "1.3", features = ["v4"] }
|
||||||
clap = "4.2"
|
|
||||||
rpassword = "7.2"
|
|
||||||
num_cpus = "1.15"
|
num_cpus = "1.15"
|
||||||
bytes = { version = "1.4", features = ["serde"] }
|
bytes = { version = "1.4", features = ["serde"] }
|
||||||
default-net = "0.14"
|
default-net = "0.14"
|
||||||
@@ -213,7 +210,7 @@ exclude = ["vdi/host", "examples/custom_plugin"]
|
|||||||
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
||||||
|
|
||||||
[package.metadata.winres]
|
[package.metadata.winres]
|
||||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||||
ProductName = "RustDesk"
|
ProductName = "RustDesk"
|
||||||
FileDescription = "RustDesk Remote Desktop"
|
FileDescription = "RustDesk Remote Desktop"
|
||||||
OriginalFilename = "rustdesk.exe"
|
OriginalFilename = "rustdesk.exe"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.7
|
version: 1.4.9
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.7
|
version: 1.4.9
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
11
build.py
11
build.py
@@ -17,7 +17,8 @@ osx = platform.platform().startswith(
|
|||||||
hbb_name = 'rustdesk' + ('.exe' if windows else '')
|
hbb_name = 'rustdesk' + ('.exe' if windows else '')
|
||||||
exe_path = 'target/release/' + hbb_name
|
exe_path = 'target/release/' + hbb_name
|
||||||
if windows:
|
if windows:
|
||||||
flutter_build_dir = 'build/windows/x64/runner/Release/'
|
win_arch = 'arm64' if platform.machine().lower() in ('arm64', 'aarch64') else 'x64'
|
||||||
|
flutter_build_dir = f'build/windows/{win_arch}/runner/Release/'
|
||||||
elif osx:
|
elif osx:
|
||||||
flutter_build_dir = 'build/macos/Build/Products/Release/'
|
flutter_build_dir = 'build/macos/Build/Products/Release/'
|
||||||
else:
|
else:
|
||||||
@@ -410,7 +411,12 @@ def build_flutter_dmg(version, features):
|
|||||||
system2(
|
system2(
|
||||||
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
||||||
os.chdir('flutter')
|
os.chdir('flutter')
|
||||||
system2('flutter build macos --release')
|
# 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('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
|
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
|
||||||
'''
|
'''
|
||||||
system2(
|
system2(
|
||||||
@@ -506,6 +512,7 @@ def main():
|
|||||||
'target\\release\\rustdesk.exe')
|
'target\\release\\rustdesk.exe')
|
||||||
else:
|
else:
|
||||||
print('Not signed')
|
print('Not signed')
|
||||||
|
os.makedirs(res_dir, exist_ok=True)
|
||||||
system2(
|
system2(
|
||||||
f'cp -rf target/release/RustDesk.exe {res_dir}')
|
f'cp -rf target/release/RustDesk.exe {res_dir}')
|
||||||
os.chdir('libs/portable')
|
os.chdir('libs/portable')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package com.carriez.flutter_hbb
|
|||||||
|
|
||||||
import android.accessibilityservice.AccessibilityService
|
import android.accessibilityservice.AccessibilityService
|
||||||
import android.accessibilityservice.GestureDescription
|
import android.accessibilityservice.GestureDescription
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Path
|
import android.graphics.Path
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -68,6 +69,16 @@ class InputService : AccessibilityService() {
|
|||||||
get() = ctx != null
|
get() = ctx != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun notifyInputState() {
|
||||||
|
val inputState = isOpen.toString()
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "input", "value" to inputState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val logTag = "input service"
|
private val logTag = "input service"
|
||||||
private var leftIsDown = false
|
private var leftIsDown = false
|
||||||
private var touchPath = Path()
|
private var touchPath = Path()
|
||||||
@@ -716,6 +727,7 @@ class InputService : AccessibilityService() {
|
|||||||
override fun onServiceConnected() {
|
override fun onServiceConnected() {
|
||||||
super.onServiceConnected()
|
super.onServiceConnected()
|
||||||
ctx = this
|
ctx = this
|
||||||
|
notifyInputState()
|
||||||
val info = AccessibilityServiceInfo()
|
val info = AccessibilityServiceInfo()
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
|
||||||
@@ -734,8 +746,16 @@ class InputService : AccessibilityService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
ctx = null
|
ctx = null
|
||||||
|
// Keep this fallback even though onUnbind usually notifies first.
|
||||||
|
notifyInputState()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUnbind(intent: Intent?): Boolean {
|
||||||
|
ctx = null
|
||||||
|
notifyInputState()
|
||||||
|
return super.onUnbind(intent)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onInterrupt() {}
|
override fun onInterrupt() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,12 +200,13 @@ class MainActivity : FlutterActivity() {
|
|||||||
"stop_input" -> {
|
"stop_input" -> {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
InputService.ctx?.disableSelf()
|
InputService.ctx?.disableSelf()
|
||||||
}
|
} else {
|
||||||
InputService.ctx = null
|
InputService.ctx = null
|
||||||
Companion.flutterMethodChannel?.invokeMethod(
|
Companion.flutterMethodChannel?.invokeMethod(
|
||||||
"on_state_changed",
|
"on_state_changed",
|
||||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||||
)
|
)
|
||||||
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
"cancel_notification" -> {
|
"cancel_notification" -> {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="199"><path fill="#0089d6" d="M118.432 187.698c32.89-5.81 60.055-10.618 60.367-10.684l.568-.12-31.052-36.935c-17.078-20.314-31.051-37.014-31.051-37.11 0-.182 32.063-88.477 32.243-88.792.06-.105 21.88 37.567 52.893 91.32 29.035 50.323 52.973 91.815 53.195 92.203l.405.707-98.684-.012-98.684-.013 59.8-10.564zM0 176.435c0-.052 14.631-25.451 32.514-56.442l32.514-56.347 37.891-31.799C123.76 14.358 140.867.027 140.935.001c.069-.026-.205.664-.609 1.534s-18.919 40.582-41.145 88.25l-40.41 86.67-29.386.037c-16.162.02-29.385-.005-29.385-.057z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 604 B |
7
flutter/assets/display_switcher.svg
Normal file
7
flutter/assets/display_switcher.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||||
|
<g fill="#000000" fill-rule="evenodd">
|
||||||
|
<rect x="4" y="6" width="24" height="16" rx="3"/>
|
||||||
|
<rect x="14.5" y="22" width="3" height="2"/>
|
||||||
|
<rect x="9.5" y="24" width="13" height="2.5" rx="1.25"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
@@ -6,83 +6,82 @@ ANDROID_ABI=$1
|
|||||||
|
|
||||||
# Build RustDesk dependencies for Android using vcpkg.json
|
# Build RustDesk dependencies for Android using vcpkg.json
|
||||||
# Required:
|
# Required:
|
||||||
# 1. set VCPKG_ROOT / ANDROID_NDK path environment variables
|
# 1. set VCPKG_ROOT / ANDROID_NDK_HOME path environment variables
|
||||||
# 2. vcpkg initialized
|
# 2. vcpkg initialized
|
||||||
# 3. ndk, version: r25c or newer
|
# 3. ndk, version: r25c or newer
|
||||||
|
|
||||||
if [ -z "$ANDROID_NDK_HOME" ]; then
|
if [ -z "${ANDROID_NDK_HOME}" ]; then
|
||||||
echo "Failed! Please set ANDROID_NDK_HOME"
|
echo "ERROR: Please set ANDROID_NDK_HOME environment variable" 1>&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$VCPKG_ROOT" ]; then
|
if [ -z "${VCPKG_ROOT}" ]; then
|
||||||
echo "Failed! Please set VCPKG_ROOT"
|
echo "ERROR: Please set VCPKG_ROOT environment variable" 1>&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
API_LEVEL="21"
|
case "${ANDROID_ABI}" in
|
||||||
|
arm64-v8a)
|
||||||
|
VCPKG_TARGET=arm64-android
|
||||||
|
;;
|
||||||
|
armeabi-v7a)
|
||||||
|
VCPKG_TARGET=arm-neon-android
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
VCPKG_TARGET=x64-android
|
||||||
|
;;
|
||||||
|
x86)
|
||||||
|
VCPKG_TARGET=x86-android
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: build_android_deps.sh <arm64-v8a|armeabi-v7a|x86_64|x86>" 1>&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Get directory of this script
|
# Get directory of this script
|
||||||
|
|
||||||
SCRIPTDIR="$(readlink -f "$0")"
|
SCRIPTDIR="$(readlink -f "$0")"
|
||||||
SCRIPTDIR="$(dirname "$SCRIPTDIR")"
|
SCRIPTDIR="$(dirname "${SCRIPTDIR}")"
|
||||||
|
|
||||||
# Check if vcpkg.json is one level up - in root directory of RD
|
# Check if vcpkg.json is one level up - in root directory of RD
|
||||||
|
|
||||||
if [ ! -f "$SCRIPTDIR/../vcpkg.json" ]; then
|
if [ ! -f "${SCRIPTDIR}/../vcpkg.json" ]; then
|
||||||
echo "Failed! Please check where vcpkg.json is!"
|
echo "ERROR: Can not find vcpkg.json in RustDesk top-level directory" 1>&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NDK llvm toolchain
|
echo "INFO: Building and install vcpkg dependencies for Android ${ANDROID_ABI} ..."
|
||||||
|
|
||||||
HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/llvm/prebuilt/`
|
pushd "${SCRIPTDIR}/.."
|
||||||
TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG
|
|
||||||
|
|
||||||
function build {
|
"${VCPKG_ROOT}/vcpkg" install \
|
||||||
ANDROID_ABI=$1
|
--triplet "${VCPKG_TARGET}" \
|
||||||
|
--x-install-root="${VCPKG_ROOT}/installed"
|
||||||
|
|
||||||
case "$ANDROID_ABI" in
|
|
||||||
arm64-v8a)
|
|
||||||
ABI=aarch64-linux-android$API_LEVEL
|
|
||||||
VCPKG_TARGET=arm64-android
|
|
||||||
;;
|
|
||||||
armeabi-v7a)
|
|
||||||
ABI=armv7a-linux-androideabi$API_LEVEL
|
|
||||||
VCPKG_TARGET=arm-neon-android
|
|
||||||
;;
|
|
||||||
x86_64)
|
|
||||||
ABI=x86_64-linux-android$API_LEVEL
|
|
||||||
VCPKG_TARGET=x64-android
|
|
||||||
;;
|
|
||||||
x86)
|
|
||||||
ABI=i686-linux-android$API_LEVEL
|
|
||||||
VCPKG_TARGET=x86-android
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "ERROR: ANDROID_ABI must be one of: arm64-v8a, armeabi-v7a, x86_64, x86" >&2
|
|
||||||
return 1
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "*** [$ANDROID_ABI][Start] Build and install vcpkg dependencies"
|
|
||||||
pushd "$SCRIPTDIR/.."
|
|
||||||
$VCPKG_ROOT/vcpkg install --triplet $VCPKG_TARGET --x-install-root="$VCPKG_ROOT/installed"
|
|
||||||
popd
|
popd
|
||||||
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-$VCPKG_TARGET-rel-out.log" || true
|
|
||||||
echo "*** [$ANDROID_ABI][Finished] Build and install vcpkg dependencies"
|
|
||||||
|
|
||||||
if [ -d "$VCPKG_ROOT/installed/arm-neon-android" ]; then
|
echo "INFO: Completed building vcpkg dependencies for Android ${ANDROID_ABI}"
|
||||||
echo "*** [Start] Move arm-neon-android to arm-android"
|
|
||||||
|
|
||||||
mv "$VCPKG_ROOT/installed/arm-neon-android" "$VCPKG_ROOT/installed/arm-android"
|
if [ "${ANDROID_ABI}" = 'armeabi-v7a' ]; then
|
||||||
|
# Symlink arm-neon-android to arm-android because cargo-ndk does not
|
||||||
|
# understand NEON suffix.
|
||||||
|
|
||||||
echo "*** [Finished] Move arm-neon-android to arm-android"
|
if [ -d "${VCPKG_ROOT}/installed/arm-neon-android" ]; then
|
||||||
fi
|
echo 'INFO: Symlinking arm-neon-android to arm-android'
|
||||||
}
|
|
||||||
|
|
||||||
if [ ! -z "$ANDROID_ABI" ]; then
|
ln -sf \
|
||||||
build "$ANDROID_ABI"
|
"${VCPKG_ROOT}/installed/arm-neon-android" \
|
||||||
|
"${VCPKG_ROOT}/installed/arm-android"
|
||||||
|
|
||||||
|
echo 'INFO: Symlinked arm-neon-android to arm-android'
|
||||||
else
|
else
|
||||||
echo "Usage: build-android-deps.sh <ANDROID-ABI>" >&2
|
cat 0<<.a
|
||||||
|
ERROR: 'vcpkg install' seem to complete successfully but
|
||||||
|
directory '${VCPKG_ROOT}/installed/arm-neon-android' is missing!
|
||||||
|
|
||||||
|
.a
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1185,6 +1185,48 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
VoidCallback? onSubmit,
|
VoidCallback? onSubmit,
|
||||||
int? submitTimeout}) {
|
int? submitTimeout}) {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
|
if (type.contains('insecure-connection')) {
|
||||||
|
Future<void> closeSession() async {
|
||||||
|
await bind.sessionSetCommon(
|
||||||
|
sessionId: sessionId,
|
||||||
|
key: 'continue-insecure-connection',
|
||||||
|
value: 'N',
|
||||||
|
);
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
closeConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void continueSession() {
|
||||||
|
unawaited(
|
||||||
|
bind.sessionSetCommon(
|
||||||
|
sessionId: sessionId,
|
||||||
|
key: 'continue-insecure-connection',
|
||||||
|
value: 'Y',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogManager.show(
|
||||||
|
(setState, close, context) => CustomAlertDialog(
|
||||||
|
title: null,
|
||||||
|
content: SelectionArea(child: msgboxContent(type, title, text)),
|
||||||
|
actions: [
|
||||||
|
dialogButton(
|
||||||
|
'Continue',
|
||||||
|
onPressed: continueSession,
|
||||||
|
isOutline: true,
|
||||||
|
),
|
||||||
|
dialogButton('Disconnect', onPressed: closeSession),
|
||||||
|
],
|
||||||
|
onSubmit: closeSession,
|
||||||
|
onCancel: closeSession,
|
||||||
|
),
|
||||||
|
tag: '$sessionId-$type-$title-$text-$link',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> buttons = [];
|
List<Widget> buttons = [];
|
||||||
bool hasOk = false;
|
bool hasOk = false;
|
||||||
submit() {
|
submit() {
|
||||||
@@ -3350,7 +3392,12 @@ Future<List<Rect>> getScreenRectList() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
||||||
{bool updateCursorPos = true}) {
|
{bool updateCursorPos = true, bool recordSelection = true}) {
|
||||||
|
if (recordSelection) {
|
||||||
|
ffi.ffiModel.lastUserDisplay = i;
|
||||||
|
ffi.ffiModel.cancelPendingRestoreTimer();
|
||||||
|
ffi.ffiModel.pendingMonitorRestore = null;
|
||||||
|
}
|
||||||
final displays = i == kAllDisplayValue
|
final displays = i == kAllDisplayValue
|
||||||
? List.generate(pi.displays.length, (index) => index)
|
? List.generate(pi.displays.length, (index) => index)
|
||||||
: [i];
|
: [i];
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||||
import '../../../models/platform_model.dart';
|
import '../../../models/platform_model.dart';
|
||||||
@@ -5,27 +8,136 @@ import 'package:flutter_hbb/models/peer_model.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
List<Peer> mergeAutocompletePeers({
|
||||||
|
Iterable<Peer> addressBookPeers = const [],
|
||||||
|
Iterable<Peer> groupPeers = const [],
|
||||||
|
Iterable<Peer> lanPeers = const [],
|
||||||
|
Iterable<Peer> recentPeers = const [],
|
||||||
|
Iterable<String> restRecentPeerIds = const [],
|
||||||
|
}) {
|
||||||
|
final combinedPeers = <String, Peer>{};
|
||||||
|
|
||||||
|
void addPeer(Peer peer) {
|
||||||
|
if (peer.id.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final existingPeer = combinedPeers[peer.id];
|
||||||
|
if (existingPeer == null) {
|
||||||
|
combinedPeers[peer.id] = Peer.copy(peer);
|
||||||
|
} else if (peer.online) {
|
||||||
|
existingPeer.online = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final peer in addressBookPeers) {
|
||||||
|
addPeer(peer);
|
||||||
|
}
|
||||||
|
for (final peer in groupPeers) {
|
||||||
|
addPeer(peer);
|
||||||
|
}
|
||||||
|
for (final peer in lanPeers) {
|
||||||
|
addPeer(peer);
|
||||||
|
}
|
||||||
|
for (final peer in recentPeers) {
|
||||||
|
addPeer(peer);
|
||||||
|
}
|
||||||
|
for (final id in restRecentPeerIds) {
|
||||||
|
if (id.isNotEmpty && !combinedPeers.containsKey(id)) {
|
||||||
|
combinedPeers[id] = Peer.fromJson({'id': id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedPeers.values.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool updateAutocompletePeerOnlineStates(
|
||||||
|
List<Peer> peers, {
|
||||||
|
required Set<String> onlines,
|
||||||
|
required Set<String> offlines,
|
||||||
|
}) {
|
||||||
|
var changed = false;
|
||||||
|
for (final peer in peers) {
|
||||||
|
if (onlines.contains(peer.id)) {
|
||||||
|
if (!peer.online) {
|
||||||
|
peer.online = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if (offlines.contains(peer.id)) {
|
||||||
|
if (peer.online) {
|
||||||
|
peer.online = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
List<String> autocompleteOnlineQueryIds(
|
||||||
|
Iterable<Peer> options, {
|
||||||
|
required int limit,
|
||||||
|
}) {
|
||||||
|
final ids = <String>[];
|
||||||
|
final seenIds = <String>{};
|
||||||
|
for (final peer in options) {
|
||||||
|
if (peer.id.isEmpty || seenIds.contains(peer.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenIds.add(peer.id);
|
||||||
|
ids.add(peer.id);
|
||||||
|
if (ids.length >= limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
class AllPeersLoader {
|
class AllPeersLoader {
|
||||||
List<Peer> peers = [];
|
List<Peer> peers = [];
|
||||||
|
|
||||||
bool _isPeersLoading = false;
|
bool _isPeersLoading = false;
|
||||||
bool _isPeersLoaded = false;
|
bool _isPeersLoaded = false;
|
||||||
|
Set<String> _lastQueryOnlineIds = {};
|
||||||
|
DateTime _lastQueryOnlineTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
Timer? _queryOnlineTimer;
|
||||||
|
List<Peer> _lastQueryOnlineOptions = const [];
|
||||||
|
Set<String> _lastOnlineIds = {};
|
||||||
|
Set<String> _lastOfflineIds = {};
|
||||||
|
final Future<void> Function(List<String> ids) _queryOnlines;
|
||||||
|
final Duration _queryOnlineDebounce;
|
||||||
|
void Function(VoidCallback)? _setState;
|
||||||
|
bool _isCleared = false;
|
||||||
|
|
||||||
final String _listenerKey = 'AllPeersLoader';
|
final String _listenerKey = 'AllPeersLoader';
|
||||||
|
static const String _cbQueryOnlines = 'callback_query_onlines';
|
||||||
late void Function(VoidCallback) setState;
|
static const Duration _queryOnlineInterval = Duration(seconds: 5);
|
||||||
|
static const Duration _defaultQueryOnlineDebounce =
|
||||||
|
Duration(milliseconds: 300);
|
||||||
|
static const int _maxQueryOnlineOptions = 20;
|
||||||
|
|
||||||
bool get needLoad => !_isPeersLoaded && !_isPeersLoading;
|
bool get needLoad => !_isPeersLoaded && !_isPeersLoading;
|
||||||
bool get isPeersLoaded => _isPeersLoaded;
|
bool get isPeersLoaded => _isPeersLoaded;
|
||||||
|
|
||||||
AllPeersLoader();
|
AllPeersLoader({
|
||||||
|
@visibleForTesting Future<void> Function(List<String> ids)? queryOnlines,
|
||||||
|
@visibleForTesting Duration? queryOnlineDebounce,
|
||||||
|
}) : _queryOnlines = queryOnlines ?? ((ids) => bind.queryOnlines(ids: ids)),
|
||||||
|
_queryOnlineDebounce =
|
||||||
|
queryOnlineDebounce ?? _defaultQueryOnlineDebounce;
|
||||||
|
|
||||||
void init(void Function(VoidCallback) setState) {
|
void init(void Function(VoidCallback) setState) {
|
||||||
this.setState = setState;
|
_setState = setState;
|
||||||
|
_isCleared = false;
|
||||||
gFFI.recentPeersModel.addListener(_mergeAllPeers);
|
gFFI.recentPeersModel.addListener(_mergeAllPeers);
|
||||||
gFFI.lanPeersModel.addListener(_mergeAllPeers);
|
gFFI.lanPeersModel.addListener(_mergeAllPeers);
|
||||||
gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
||||||
gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
||||||
|
platformFFI.registerEventHandler(_cbQueryOnlines, _listenerKey,
|
||||||
|
(evt) async {
|
||||||
|
_updateOnlineState(evt);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
@@ -33,6 +145,11 @@ class AllPeersLoader {
|
|||||||
gFFI.lanPeersModel.removeListener(_mergeAllPeers);
|
gFFI.lanPeersModel.removeListener(_mergeAllPeers);
|
||||||
gFFI.abModel.removePeerUpdateListener(_listenerKey);
|
gFFI.abModel.removePeerUpdateListener(_listenerKey);
|
||||||
gFFI.groupModel.removePeerUpdateListener(_listenerKey);
|
gFFI.groupModel.removePeerUpdateListener(_listenerKey);
|
||||||
|
platformFFI.unregisterEventHandler(_cbQueryOnlines, _listenerKey);
|
||||||
|
_queryOnlineTimer?.cancel();
|
||||||
|
_lastQueryOnlineOptions = const [];
|
||||||
|
_setState = null;
|
||||||
|
_isCleared = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getAllPeers() async {
|
Future<void> getAllPeers() async {
|
||||||
@@ -59,50 +176,106 @@ class AllPeersLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _mergeAllPeers() {
|
void _mergeAllPeers() {
|
||||||
Map<String, dynamic> combinedPeers = {};
|
if (_isCleared) {
|
||||||
for (var p in gFFI.abModel.allPeers()) {
|
return;
|
||||||
if (!combinedPeers.containsKey(p.id)) {
|
|
||||||
combinedPeers[p.id] = p.toJson();
|
|
||||||
}
|
}
|
||||||
}
|
peers = mergeAutocompletePeers(
|
||||||
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
|
addressBookPeers: gFFI.abModel.allPeers(),
|
||||||
if (!combinedPeers.containsKey(p.id)) {
|
groupPeers: gFFI.groupModel.peers,
|
||||||
combinedPeers[p.id] = p.toJson();
|
lanPeers: gFFI.lanPeersModel.peers,
|
||||||
}
|
recentPeers: gFFI.recentPeersModel.peers,
|
||||||
}
|
restRecentPeerIds: gFFI.recentPeersModel.restPeerIds,
|
||||||
|
);
|
||||||
List<Peer> parsedPeers = [];
|
_applyLastOnlineState(peers);
|
||||||
for (var peer in combinedPeers.values) {
|
_scheduleSetState(() {
|
||||||
parsedPeers.add(Peer.fromJson(peer));
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> peerIds = combinedPeers.keys.toSet();
|
|
||||||
for (final peer in gFFI.lanPeersModel.peers) {
|
|
||||||
if (!peerIds.contains(peer.id)) {
|
|
||||||
parsedPeers.add(peer);
|
|
||||||
peerIds.add(peer.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final peer in gFFI.recentPeersModel.peers) {
|
|
||||||
if (!peerIds.contains(peer.id)) {
|
|
||||||
parsedPeers.add(peer);
|
|
||||||
peerIds.add(peer.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (final id in gFFI.recentPeersModel.restPeerIds) {
|
|
||||||
if (!peerIds.contains(id)) {
|
|
||||||
parsedPeers.add(Peer.fromJson({'id': id}));
|
|
||||||
peerIds.add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peers = parsedPeers;
|
|
||||||
setState(() {
|
|
||||||
_isPeersLoading = false;
|
_isPeersLoading = false;
|
||||||
_isPeersLoaded = true;
|
_isPeersLoaded = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateOnlineState(Map<String, dynamic> evt) {
|
||||||
|
if (_isCleared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastOnlineIds = _splitPeerIds(evt['onlines']);
|
||||||
|
_lastOfflineIds = _splitPeerIds(evt['offlines']);
|
||||||
|
final peersChanged = _applyLastOnlineState(peers);
|
||||||
|
final optionsChanged = _applyLastOnlineState(_lastQueryOnlineOptions);
|
||||||
|
if (peersChanged || optionsChanged) {
|
||||||
|
_scheduleSetState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleSetState(VoidCallback callback) {
|
||||||
|
if (_isCleared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final setState = _setState;
|
||||||
|
if (setState == null) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
setState(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _applyLastOnlineState(List<Peer> peers) {
|
||||||
|
return updateAutocompletePeerOnlineStates(
|
||||||
|
peers,
|
||||||
|
onlines: _lastOnlineIds,
|
||||||
|
offlines: _lastOfflineIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> _splitPeerIds(dynamic ids) {
|
||||||
|
if (ids is! String || ids.isEmpty) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return ids.split(',').where((id) => id.isNotEmpty).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void queryOnlines(Iterable<Peer> options) {
|
||||||
|
if (_isCleared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastQueryOnlineOptions = options.toList(growable: false);
|
||||||
|
final ids = autocompleteOnlineQueryIds(
|
||||||
|
_lastQueryOnlineOptions,
|
||||||
|
limit: _maxQueryOnlineOptions,
|
||||||
|
).toSet();
|
||||||
|
_queryOnlineTimer?.cancel();
|
||||||
|
_queryOnlineTimer = null;
|
||||||
|
if (ids.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (setEquals(ids, _lastQueryOnlineIds) &&
|
||||||
|
now.difference(_lastQueryOnlineTime) < _queryOnlineInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queryOnlineTimer = Timer(_queryOnlineDebounce, () async {
|
||||||
|
try {
|
||||||
|
await _queryOnlines(ids.toList(growable: false));
|
||||||
|
if (_isCleared) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastQueryOnlineIds = ids;
|
||||||
|
_lastQueryOnlineTime = DateTime.now();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('query autocomplete online state failed: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void updateOnlineStateForTesting(Map<String, dynamic> evt) {
|
||||||
|
_updateOnlineState(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool applyLastOnlineStateForTesting(List<Peer> peers) {
|
||||||
|
return _applyLastOnlineState(peers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutocompletePeerTile extends StatefulWidget {
|
class AutocompletePeerTile extends StatefulWidget {
|
||||||
|
|||||||
@@ -24,6 +24,35 @@ const kOpSvgList = [
|
|||||||
'microsoft'
|
'microsoft'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
class _OidcProviderBranding {
|
||||||
|
final String label;
|
||||||
|
final String iconKey;
|
||||||
|
|
||||||
|
const _OidcProviderBranding({
|
||||||
|
required this.label,
|
||||||
|
required this.iconKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_OidcProviderBranding _oidcProviderBranding(String op) {
|
||||||
|
switch (op.toLowerCase()) {
|
||||||
|
case 'azure':
|
||||||
|
return _OidcProviderBranding(
|
||||||
|
label: 'Microsoft',
|
||||||
|
iconKey: 'microsoft',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return _OidcProviderBranding(
|
||||||
|
label: {
|
||||||
|
'github': 'GitHub',
|
||||||
|
'gitlab': 'GitLab',
|
||||||
|
}[op.toLowerCase()] ??
|
||||||
|
toCapitalized(op),
|
||||||
|
iconKey: op.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _IconOP extends StatelessWidget {
|
class _IconOP extends StatelessWidget {
|
||||||
final String op;
|
final String op;
|
||||||
final String? icon;
|
final String? icon;
|
||||||
@@ -74,11 +103,8 @@ class ButtonOP extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final opLabel = {
|
final branding = _oidcProviderBranding(op);
|
||||||
'github': 'GitHub',
|
final buttonLabel = translate("Continue with {${branding.label}}");
|
||||||
'gitlab': 'GitLab'
|
|
||||||
}[op.toLowerCase()] ??
|
|
||||||
toCapitalized(op);
|
|
||||||
return Row(children: [
|
return Row(children: [
|
||||||
Container(
|
Container(
|
||||||
height: height,
|
height: height,
|
||||||
@@ -95,7 +121,7 @@ class ButtonOP extends StatelessWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 30,
|
width: 30,
|
||||||
child: _IconOP(
|
child: _IconOP(
|
||||||
op: op,
|
op: branding.iconKey,
|
||||||
icon: icon,
|
icon: icon,
|
||||||
margin: EdgeInsets.only(right: 5),
|
margin: EdgeInsets.only(right: 5),
|
||||||
),
|
),
|
||||||
@@ -103,8 +129,7 @@ class ButtonOP extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Center(
|
child: Center(child: Text(buttonLabel)),
|
||||||
child: Text(translate("Continue with {$opLabel}"))),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -72,10 +72,24 @@ Widget waylandKeyboardScopeChip(BuildContext context, String text) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// macOS privacy mode blacks out all online displays, so switching the remote
|
bool _isWindowsMode1PrivacyImpl(String privacyModeImpl) {
|
||||||
// display does not weaken the local privacy protection.
|
return privacyModeImpl == kPrivacyModeImplMag ||
|
||||||
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
|
privacyModeImpl == kPrivacyModeImplExcludeFromCapture;
|
||||||
return pi.platform == kPeerPlatformMacOS;
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TTextMenu {
|
class TTextMenu {
|
||||||
@@ -592,7 +606,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
// to-do:
|
// to-do:
|
||||||
// 1. Web desktop
|
// 1. Web desktop
|
||||||
// 2. Mobile, copy the image to the clipboard
|
// 2. Mobile, copy the image to the clipboard
|
||||||
if (isDesktop) {
|
if ((isDefaultConn || ffi.connType == ConnType.viewCamera) && isDesktop) {
|
||||||
final isScreenshotSupported = bind.sessionGetCommonSync(
|
final isScreenshotSupported = bind.sessionGetCommonSync(
|
||||||
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
||||||
if ('true' == isScreenshotSupported) {
|
if ('true' == isScreenshotSupported) {
|
||||||
@@ -964,7 +978,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
|
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
if (pi.isSupportMultiDisplay &&
|
if (pi.isSupportMultiDisplay &&
|
||||||
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
|
(privacyModeState.isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
|
||||||
pi.displaysCount.value > 1 &&
|
pi.displaysCount.value > 1 &&
|
||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||||
final value =
|
final value =
|
||||||
@@ -1048,7 +1063,20 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
return []; // No permission and not active, hide options.
|
return []; // No permission and not active, hide options.
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
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) {
|
||||||
final enabled = !ffiModel.viewOnly &&
|
final enabled = !ffiModel.viewOnly &&
|
||||||
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
||||||
return TToggleMenu(
|
return TToggleMenu(
|
||||||
@@ -1056,16 +1084,7 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
onChanged: enabled
|
onChanged: enabled
|
||||||
? (value) {
|
? (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
if (!allowDisplaySwitchInPrivacyMode(pi) &&
|
if (!checkDisplayAllowedForPrivacyMode(targetImplKey, value)) {
|
||||||
ffiModel.pi.currentDisplay != 0 &&
|
|
||||||
ffiModel.pi.currentDisplay != kAllDisplayValue) {
|
|
||||||
msgBox(
|
|
||||||
sessionId,
|
|
||||||
'custom-nook-nocancel-hasclose',
|
|
||||||
'info',
|
|
||||||
'Please switch to Display 1 first',
|
|
||||||
'',
|
|
||||||
ffi.dialogManager);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final option = 'privacy-mode';
|
final option = 'privacy-mode';
|
||||||
@@ -1083,7 +1102,7 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
getDefaultMenu((sid, opt) async {
|
getDefaultMenu((sid, opt) async {
|
||||||
bind.sessionToggleOption(sessionId: sid, value: opt);
|
bind.sessionToggleOption(sessionId: sid, value: opt);
|
||||||
togglePrivacyModeTime = DateTime.now();
|
togglePrivacyModeTime = DateTime.now();
|
||||||
})
|
}, kPrivacyModeImplMag)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (privacyModeImpls.isEmpty) {
|
if (privacyModeImpls.isEmpty) {
|
||||||
@@ -1097,7 +1116,7 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
bind.sessionTogglePrivacyMode(
|
bind.sessionTogglePrivacyMode(
|
||||||
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
|
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
|
||||||
togglePrivacyModeTime = DateTime.now();
|
togglePrivacyModeTime = DateTime.now();
|
||||||
})
|
}, implKey)
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
final visibleImpls = hasPrivacyModePermission
|
final visibleImpls = hasPrivacyModePermission
|
||||||
@@ -1118,6 +1137,9 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
? (value) {
|
? (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
if (value && !hasPrivacyModePermission) return;
|
if (value && !hasPrivacyModePermission) return;
|
||||||
|
if (!checkDisplayAllowedForPrivacyMode(implKey, value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
togglePrivacyModeTime = DateTime.now();
|
togglePrivacyModeTime = DateTime.now();
|
||||||
bind.sessionTogglePrivacyMode(
|
bind.sessionTogglePrivacyMode(
|
||||||
sessionId: sessionId, implKey: implKey, on: value);
|
sessionId: sessionId, implKey: implKey, on: value);
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
|||||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||||
"supported_privacy_mode_impl";
|
"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 kPeerPlatformWindows = "Windows";
|
||||||
const String kPeerPlatformLinux = "Linux";
|
const String kPeerPlatformLinux = "Linux";
|
||||||
const String kPeerPlatformMacOS = "Mac OS";
|
const String kPeerPlatformMacOS = "Mac OS";
|
||||||
@@ -170,6 +174,8 @@ const String kOptionShowVirtualMouse = "show-virtual-mouse";
|
|||||||
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
||||||
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
||||||
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
||||||
|
const String kOptionAllowMonitorSwitchMainToolbar = "allow-monitor-switch-main-toolbar";
|
||||||
|
const String kOptionAllowMonitorSwitchMinToolbar = "allow-monitor-switch-min-toolbar";
|
||||||
const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys";
|
const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys";
|
||||||
|
|
||||||
// network options
|
// network options
|
||||||
|
|||||||
@@ -398,6 +398,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
.contains(textToFind) ||
|
.contains(textToFind) ||
|
||||||
peer.alias.toLowerCase().contains(textToFind))
|
peer.alias.toLowerCase().contains(textToFind))
|
||||||
.toList();
|
.toList();
|
||||||
|
_allPeersLoader.queryOnlines(_autocompleteOpts);
|
||||||
}
|
}
|
||||||
return _autocompleteOpts;
|
return _autocompleteOpts;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ class _GeneralState extends State<_General> {
|
|||||||
final RxBool serviceStop =
|
final RxBool serviceStop =
|
||||||
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
|
isWeb ? RxBool(false) : Get.find<RxBool>(tag: 'stop-service');
|
||||||
RxBool serviceBtnEnabled = true.obs;
|
RxBool serviceBtnEnabled = true.obs;
|
||||||
|
final GlobalKey _minToolbarOptionKey = GlobalKey();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -482,13 +483,15 @@ class _GeneralState extends State<_General> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget other() {
|
Widget other() {
|
||||||
|
final incomingOnly = bind.isIncomingOnly();
|
||||||
|
final outgoingOnly = bind.isOutgoingOnly();
|
||||||
final showAutoUpdate = isWindows && bind.mainIsInstalled();
|
final showAutoUpdate = isWindows && bind.mainIsInstalled();
|
||||||
final children = <Widget>[
|
final children = <Widget>[
|
||||||
if (!isWeb && !bind.isIncomingOnly())
|
if (!isWeb && !incomingOnly)
|
||||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||||
kOptionEnableConfirmClosingTabs,
|
kOptionEnableConfirmClosingTabs,
|
||||||
isServer: false),
|
isServer: false),
|
||||||
if (!bind.isIncomingOnly())
|
if (!incomingOnly)
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'allow-remote-toolbar-docking-any-edge',
|
'allow-remote-toolbar-docking-any-edge',
|
||||||
@@ -498,9 +501,10 @@ class _GeneralState extends State<_General> {
|
|||||||
reloadAllWindows();
|
reloadAllWindows();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (!isWeb && !outgoingOnly)
|
||||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
||||||
if (!isWeb) wallpaper(),
|
if (!isWeb) wallpaper(),
|
||||||
if (!isWeb && !bind.isIncomingOnly()) ...[
|
if (!isWeb && !incomingOnly) ...[
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Open connection in new tab',
|
'Open connection in new tab',
|
||||||
@@ -539,6 +543,7 @@ class _GeneralState extends State<_General> {
|
|||||||
isServer: false,
|
isServer: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
if (!isWeb && !bind.isCustomClient())
|
if (!isWeb && !bind.isCustomClient())
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
@@ -553,13 +558,13 @@ class _GeneralState extends State<_General> {
|
|||||||
kOptionAllowAutoUpdate,
|
kOptionAllowAutoUpdate,
|
||||||
isServer: true,
|
isServer: true,
|
||||||
),
|
),
|
||||||
if (isWindows && !bind.isOutgoingOnly())
|
if (isWindows && !outgoingOnly)
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Capture screen using DirectX',
|
'Capture screen using DirectX',
|
||||||
kOptionDirectxCapture,
|
kOptionDirectxCapture,
|
||||||
),
|
),
|
||||||
if (!bind.isIncomingOnly()) ...[
|
if (!isWeb && !incomingOnly) ...[
|
||||||
_OptionCheckBox(
|
_OptionCheckBox(
|
||||||
context,
|
context,
|
||||||
'Enable UDP hole punching',
|
'Enable UDP hole punching',
|
||||||
@@ -573,7 +578,6 @@ class _GeneralState extends State<_General> {
|
|||||||
isServer: false,
|
isServer: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add client-side wakelock option for desktop platforms
|
// Add client-side wakelock option for desktop platforms
|
||||||
@@ -605,6 +609,47 @@ class _GeneralState extends State<_General> {
|
|||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
children.add(_OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Show monitor switch button on the main toolbar',
|
||||||
|
kOptionAllowMonitorSwitchMainToolbar,
|
||||||
|
isServer: false,
|
||||||
|
update: (enabled) async {
|
||||||
|
if (!enabled) {
|
||||||
|
await mainSetLocalBoolOption(
|
||||||
|
kOptionAllowMonitorSwitchMinToolbar, false);
|
||||||
|
}
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
reloadAllWindows();
|
||||||
|
if (enabled) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final ctx = _minToolbarOptionKey.currentContext;
|
||||||
|
if (ctx != null) {
|
||||||
|
Scrollable.ensureVisible(
|
||||||
|
ctx,
|
||||||
|
alignment: 0.5,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
if (mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar)) {
|
||||||
|
children.add(KeyedSubtree(
|
||||||
|
key: _minToolbarOptionKey,
|
||||||
|
child: _OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'Show on the minimized toolbar',
|
||||||
|
kOptionAllowMonitorSwitchMinToolbar,
|
||||||
|
isServer: false,
|
||||||
|
update: (_) {
|
||||||
|
reloadAllWindows();
|
||||||
|
},
|
||||||
|
).marginOnly(left: _kCheckBoxLeftMargin * 3),
|
||||||
|
));
|
||||||
|
}
|
||||||
return _Card(title: 'Other', children: children);
|
return _Card(title: 'Other', children: children);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2429,7 +2474,7 @@ class _AboutState extends State<_About> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
|
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Tech Pte. Ltd.\n$license',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// Register this terminal model with FFI for event routing
|
// Register this terminal model with FFI for event routing
|
||||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||||
|
|
||||||
|
// Auto-close tab when shell exits
|
||||||
|
_terminalModel.onClosed = () {
|
||||||
|
if (mounted) {
|
||||||
|
widget.tabController.closeBy(widget.tabKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize terminal connection
|
// Initialize terminal connection
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.tabController.onSelected?.call(widget.id);
|
widget.tabController.onSelected?.call(widget.id);
|
||||||
|
|||||||
@@ -779,6 +779,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: _DraggableShowHide(
|
child: _DraggableShowHide(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
|
ffi: widget.ffi,
|
||||||
sessionId: widget.ffi.sessionId,
|
sessionId: widget.ffi.sessionId,
|
||||||
dragging: _dragging,
|
dragging: _dragging,
|
||||||
fraction: _fraction,
|
fraction: _fraction,
|
||||||
@@ -805,13 +806,25 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
|
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
|
||||||
final List<Widget> toolbarItems = [];
|
final List<Widget> toolbarItems = [];
|
||||||
toolbarItems.add(_PinMenu(state: widget.state));
|
toolbarItems.add(_PinMenu(state: widget.state));
|
||||||
|
toolbarItems.add(Obx(() {
|
||||||
|
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||||
|
if ((privacyModeState.isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
|
||||||
|
pi.displaysCount.value > 1 &&
|
||||||
|
mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar)) {
|
||||||
|
return _MainMonitorSwitchButton(id: widget.id, ffi: widget.ffi);
|
||||||
|
} else {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
}));
|
||||||
if (!isWebDesktop) {
|
if (!isWebDesktop) {
|
||||||
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbarItems.add(Obx(() {
|
toolbarItems.add(Obx(() {
|
||||||
if ((PrivacyModeState.find(widget.id).isEmpty ||
|
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||||
allowDisplaySwitchInPrivacyMode(pi)) &&
|
if ((privacyModeState.isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
|
||||||
pi.displaysCount.value > 1) {
|
pi.displaysCount.value > 1) {
|
||||||
return _MonitorMenu(
|
return _MonitorMenu(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
@@ -964,6 +977,88 @@ class _MobileActionMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MonitorCycle {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
const _MonitorCycle(this.id, this.ffi);
|
||||||
|
|
||||||
|
PeerInfo get _pi => ffi.ffiModel.pi;
|
||||||
|
int get total => _pi.displays.length;
|
||||||
|
int get _current => CurrentDisplayState.find(id).value;
|
||||||
|
bool get _inRange => _current >= 0 && _current < total;
|
||||||
|
|
||||||
|
String get label => _inRange ? '${_current + 1}' : '*';
|
||||||
|
String get tooltip => '${translate('Switch display')} ($label/$total)';
|
||||||
|
|
||||||
|
void next() {
|
||||||
|
final t = total;
|
||||||
|
if (t < 2) return;
|
||||||
|
final from = _inRange ? _current : -1;
|
||||||
|
final target = (from + 1) % t;
|
||||||
|
final isChooseDisplayToOpenInNewWindow = _pi.isSupportMultiDisplay &&
|
||||||
|
bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) ==
|
||||||
|
'Y';
|
||||||
|
if (isChooseDisplayToOpenInNewWindow) {
|
||||||
|
openMonitorInNewTabOrWindow(target, ffi.id, _pi);
|
||||||
|
} else {
|
||||||
|
openMonitorInTheSameTab(target, ffi, _pi, updateCursorPos: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MainMonitorSwitchButton extends StatelessWidget {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
|
||||||
|
const _MainMonitorSwitchButton({
|
||||||
|
Key? key,
|
||||||
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cycle = _MonitorCycle(id, ffi);
|
||||||
|
return Obx(() {
|
||||||
|
if (cycle.total < 2) return const Offstage();
|
||||||
|
final label = cycle.label;
|
||||||
|
|
||||||
|
return _IconMenuButton(
|
||||||
|
tooltip: cycle.tooltip,
|
||||||
|
color: _ToolbarTheme.blueColor,
|
||||||
|
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||||
|
onPressed: cycle.next,
|
||||||
|
icon: SizedBox(
|
||||||
|
width: _ToolbarTheme.buttonSize,
|
||||||
|
height: _ToolbarTheme.buttonSize,
|
||||||
|
child: Stack(
|
||||||
|
alignment: const Alignment(0, -0.125),
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/display_switcher.svg',
|
||||||
|
colorFilter:
|
||||||
|
const ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||||
|
width: _ToolbarTheme.buttonSize,
|
||||||
|
height: _ToolbarTheme.buttonSize,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 11,
|
||||||
|
height: 1,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _MonitorMenu extends StatelessWidget {
|
class _MonitorMenu extends StatelessWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
@@ -1072,8 +1167,8 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
tooltip: isMulti
|
tooltip: isMulti
|
||||||
? ''
|
? ''
|
||||||
: isAllMonitors
|
: isAllMonitors
|
||||||
? 'all monitors'
|
? 'All monitors'
|
||||||
: '#${i + 1} monitor',
|
: '#{${i + 1}} monitor',
|
||||||
hMargin: isMulti ? null : 6,
|
hMargin: isMulti ? null : 6,
|
||||||
vMargin: isMulti ? null : 12,
|
vMargin: isMulti ? null : 12,
|
||||||
topLevel: false,
|
topLevel: false,
|
||||||
@@ -2757,7 +2852,7 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
|
|||||||
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
|
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
|
||||||
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
|
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
|
||||||
button = Tooltip(
|
button = Tooltip(
|
||||||
message: widget.tooltip,
|
message: translate(widget.tooltip),
|
||||||
child: button,
|
child: button,
|
||||||
);
|
);
|
||||||
if (widget.topLevel) {
|
if (widget.topLevel) {
|
||||||
@@ -2970,6 +3065,7 @@ class RdoMenuButton<T> extends StatelessWidget {
|
|||||||
|
|
||||||
class _DraggableShowHide extends StatefulWidget {
|
class _DraggableShowHide extends StatefulWidget {
|
||||||
final String id;
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
final SessionID sessionId;
|
final SessionID sessionId;
|
||||||
final RxDouble fraction;
|
final RxDouble fraction;
|
||||||
final Rx<_ToolbarEdge> edge;
|
final Rx<_ToolbarEdge> edge;
|
||||||
@@ -2993,6 +3089,7 @@ class _DraggableShowHide extends StatefulWidget {
|
|||||||
const _DraggableShowHide({
|
const _DraggableShowHide({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
required this.fraction,
|
required this.fraction,
|
||||||
required this.edge,
|
required this.edge,
|
||||||
@@ -3249,6 +3346,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildDraggable(context),
|
_buildDraggable(context),
|
||||||
|
Obx(() => collapse.isTrue
|
||||||
|
? _MinimizedMonitorSwitchButton(id: widget.id, ffi: widget.ffi)
|
||||||
|
: const Offstage()),
|
||||||
Obx(() => buttonWrapper(
|
Obx(() => buttonWrapper(
|
||||||
() {
|
() {
|
||||||
widget.setFullscreen(!isFullscreen.value);
|
widget.setFullscreen(!isFullscreen.value);
|
||||||
@@ -3409,3 +3509,73 @@ class EdgeThicknessControl extends StatelessWidget {
|
|||||||
return slider;
|
return slider;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MinimizedMonitorSwitchButton extends StatelessWidget {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
|
||||||
|
const _MinimizedMonitorSwitchButton({
|
||||||
|
Key? key,
|
||||||
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const double iconSize = 20;
|
||||||
|
final cycle = _MonitorCycle(id, ffi);
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
final label = cycle.label;
|
||||||
|
if (!mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMainToolbar) ||
|
||||||
|
!mainGetLocalBoolOptionSync(kOptionAllowMonitorSwitchMinToolbar)) {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
if (cycle.total < 2) return const Offstage();
|
||||||
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
|
if (privacyModeState.isNotEmpty &&
|
||||||
|
!allowDisplaySwitchInPrivacyMode(
|
||||||
|
ffi.ffiModel.pi, privacyModeState.value)) {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Tooltip(
|
||||||
|
message: cycle.tooltip,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: cycle.next,
|
||||||
|
style: ButtonStyle(
|
||||||
|
minimumSize: MaterialStateProperty.all(const Size(0, 0)),
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||||
|
if (states.contains(MaterialState.hovered)) {
|
||||||
|
return _ToolbarTheme.blueColor.withOpacity(0.15);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
alignment: const Alignment(0, -0.125),
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/display_switcher.svg',
|
||||||
|
colorFilter:
|
||||||
|
ColorFilter.mode(_ToolbarTheme.blueColor, BlendMode.srcIn),
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 9,
|
||||||
|
height: 1,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
.contains(textToFind) ||
|
.contains(textToFind) ||
|
||||||
peer.alias.toLowerCase().contains(textToFind))
|
peer.alias.toLowerCase().contains(textToFind))
|
||||||
.toList();
|
.toList();
|
||||||
|
_allPeersLoader.queryOnlines(_autocompleteOpts);
|
||||||
}
|
}
|
||||||
return _autocompleteOpts;
|
return _autocompleteOpts;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1220,7 +1220,11 @@ void showOptions(
|
|||||||
if (image != null) {
|
if (image != null) {
|
||||||
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
||||||
}
|
}
|
||||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
|
if (pi.displays.length > 1 &&
|
||||||
|
pi.currentDisplay != kAllDisplayValue &&
|
||||||
|
(privacyModeState.isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value))) {
|
||||||
final cur = pi.currentDisplay;
|
final cur = pi.currentDisplay;
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
||||||
@@ -1274,8 +1278,6 @@ void showOptions(
|
|||||||
await toolbarDisplayToggle(context, id, gFFI);
|
await toolbarDisplayToggle(context, id, gFFI);
|
||||||
|
|
||||||
List<TToggleMenu> privacyModeList = [];
|
List<TToggleMenu> privacyModeList = [];
|
||||||
// privacy mode
|
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
|
||||||
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
||||||
privacyModeState.isNotEmpty) {
|
privacyModeState.isNotEmpty) {
|
||||||
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// Register this terminal model with FFI for event routing
|
// Register this terminal model with FFI for event routing
|
||||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||||
|
|
||||||
|
// Auto-close connection when shell exits
|
||||||
|
_terminalModel.onClosed = () {
|
||||||
|
if (mounted) {
|
||||||
|
closeConnection(id: widget.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Web desktop users have full hardware keyboard access, so the on-screen
|
// Web desktop users have full hardware keyboard access, so the on-screen
|
||||||
// terminal extra keys bar is unnecessary and disabled.
|
// terminal extra keys bar is unnecessary and disabled.
|
||||||
_showTerminalExtraKeys = !isWebDesktop &&
|
_showTerminalExtraKeys = !isWebDesktop &&
|
||||||
|
|||||||
@@ -142,12 +142,22 @@ class FileModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
|
||||||
|
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||||
|
if (id == null || !jobController.hasTransferConflictJob(id)) {
|
||||||
|
debugPrint("Ignore stale override confirm event: $evt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
evtLoop.pushEvent(
|
evtLoop.pushEvent(
|
||||||
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> overrideFileConfirm(Map<String, dynamic> evt,
|
Future<void> overrideFileConfirm(Map<String, dynamic> evt,
|
||||||
{bool? overrideConfirm, bool skip = false}) async {
|
{bool? overrideConfirm, bool skip = false}) async {
|
||||||
|
final id = int.tryParse(evt['id']?.toString() ?? '') ?? 0;
|
||||||
|
if (id == 0 || !jobController.hasTransferConflictJob(id)) {
|
||||||
|
debugPrint("Ignore override confirm for inactive job: $evt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// If `skip == true`, it means to skip this file without showing dialog.
|
// If `skip == true`, it means to skip this file without showing dialog.
|
||||||
// Because `resp` may be null after the user operation or the last remembered operation,
|
// Because `resp` may be null after the user operation or the last remembered operation,
|
||||||
// and we should distinguish them.
|
// and we should distinguish them.
|
||||||
@@ -156,15 +166,12 @@ class FileModel {
|
|||||||
? await showFileConfirmDialog(translate("Overwrite"),
|
? await showFileConfirmDialog(translate("Overwrite"),
|
||||||
"${evt['read_path']}", true, evt['is_identical'] == "true")
|
"${evt['read_path']}", true, evt['is_identical'] == "true")
|
||||||
: null);
|
: null);
|
||||||
final id = int.tryParse(evt['id']) ?? 0;
|
if (!jobController.hasTransferConflictJob(id)) {
|
||||||
if (false == resp) {
|
debugPrint("Ignore override confirm result for inactive job: $evt");
|
||||||
final jobIndex = jobController.getJob(id);
|
return;
|
||||||
if (jobIndex != -1) {
|
|
||||||
await jobController.cancelJob(id);
|
|
||||||
final job = jobController.jobTable[jobIndex];
|
|
||||||
job.state = JobState.done;
|
|
||||||
jobController.jobTable.refresh();
|
|
||||||
}
|
}
|
||||||
|
if (false == resp) {
|
||||||
|
await jobController.cancelTransferConflictBatch(id);
|
||||||
} else {
|
} else {
|
||||||
var need_override = false;
|
var need_override = false;
|
||||||
if (resp == null) {
|
if (resp == null) {
|
||||||
@@ -176,6 +183,7 @@ class FileModel {
|
|||||||
}
|
}
|
||||||
// Update the loop config.
|
// Update the loop config.
|
||||||
if (fileConfirmCheckboxRemember) {
|
if (fileConfirmCheckboxRemember) {
|
||||||
|
jobController.rememberTransferConflictBatch(id, resp);
|
||||||
evtLoop.setSkip(!need_override);
|
evtLoop.setSkip(!need_override);
|
||||||
}
|
}
|
||||||
await bind.sessionSetConfirmOverrideFile(
|
await bind.sessionSetConfirmOverrideFile(
|
||||||
@@ -285,6 +293,8 @@ class FileModel {
|
|||||||
final isWindows = otherSideData.options.isWindows;
|
final isWindows = otherSideData.options.isWindows;
|
||||||
final showHidden = otherSideData.options.showHidden;
|
final showHidden = otherSideData.options.showHidden;
|
||||||
final jobID = jobController.addTransferJob(entry, false);
|
final jobID = jobController.addTransferJob(entry, false);
|
||||||
|
jobController.registerTransferConflictBatch([jobID],
|
||||||
|
batchId: int.tryParse(obj['batchId']?.toString() ?? ''));
|
||||||
webSendLocalFiles(
|
webSendLocalFiles(
|
||||||
handleIndex: handleIndex,
|
handleIndex: handleIndex,
|
||||||
actId: jobID,
|
actId: jobID,
|
||||||
@@ -570,8 +580,15 @@ class FileController {
|
|||||||
final toPath = otherSideData.directory.path;
|
final toPath = otherSideData.directory.path;
|
||||||
final isWindows = otherSideData.options.isWindows;
|
final isWindows = otherSideData.options.isWindows;
|
||||||
final showHidden = otherSideData.options.showHidden;
|
final showHidden = otherSideData.options.showHidden;
|
||||||
|
final transferJobs = <(Entry, int)>[];
|
||||||
|
final transferJobIds = <int>[];
|
||||||
for (var from in items.items) {
|
for (var from in items.items) {
|
||||||
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
|
||||||
|
transferJobs.add((from, jobID));
|
||||||
|
transferJobIds.add(jobID);
|
||||||
|
}
|
||||||
|
jobController.registerTransferConflictBatch(transferJobIds);
|
||||||
|
for (final (from, jobID) in transferJobs) {
|
||||||
bind.sessionSendFiles(
|
bind.sessionSendFiles(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
actId: jobID,
|
actId: jobID,
|
||||||
@@ -917,6 +934,10 @@ class JobController {
|
|||||||
static final JobID jobID = JobID();
|
static final JobID jobID = JobID();
|
||||||
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
final jobTable = List<JobProgress>.empty(growable: true).obs;
|
||||||
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
final jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||||
|
int _nextTransferConflictBatchId = 1;
|
||||||
|
final Map<int, int> _transferConflictJobToBatch = {};
|
||||||
|
int? _transferConflictRememberBatchId;
|
||||||
|
bool? _transferConflictRememberOverrideConfirm;
|
||||||
final GetSessionID getSessionID;
|
final GetSessionID getSessionID;
|
||||||
final GetDialogManager getDialogManager;
|
final GetDialogManager getDialogManager;
|
||||||
SessionID get sessionId => getSessionID();
|
SessionID get sessionId => getSessionID();
|
||||||
@@ -929,6 +950,57 @@ class JobController {
|
|||||||
return jobTable.indexWhere((element) => element.id == id);
|
return jobTable.indexWhere((element) => element.id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void registerTransferConflictBatch(Iterable<int> jobIds, {int? batchId}) {
|
||||||
|
final ids = jobIds.toList(growable: false);
|
||||||
|
if (ids.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
batchId ??= _nextTransferConflictBatchId++;
|
||||||
|
if (batchId >= _nextTransferConflictBatchId) {
|
||||||
|
_nextTransferConflictBatchId = batchId + 1;
|
||||||
|
}
|
||||||
|
for (final jobId in ids) {
|
||||||
|
_transferConflictJobToBatch[jobId] = batchId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int? transferConflictBatchId(int jobId) {
|
||||||
|
return _transferConflictJobToBatch[jobId];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasTransferConflictJob(int jobId) {
|
||||||
|
return transferConflictBatchId(jobId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTransferConflictRememberBatch(int? batchId) {
|
||||||
|
return batchId != null && batchId == _transferConflictRememberBatchId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool? transferConflictRememberOverrideConfirm(int? batchId) {
|
||||||
|
if (!isTransferConflictRememberBatch(batchId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _transferConflictRememberOverrideConfirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rememberTransferConflictBatch(int jobId, bool? overrideConfirm) {
|
||||||
|
_transferConflictRememberBatchId = _transferConflictJobToBatch[jobId];
|
||||||
|
_transferConflictRememberOverrideConfirm = overrideConfirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregisterTransferConflictJob(int jobId) {
|
||||||
|
final batchId = _transferConflictJobToBatch.remove(jobId);
|
||||||
|
if (batchId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_transferConflictJobToBatch.containsValue(batchId)) {
|
||||||
|
if (_transferConflictRememberBatchId == batchId) {
|
||||||
|
_transferConflictRememberBatchId = null;
|
||||||
|
_transferConflictRememberOverrideConfirm = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// return jobID
|
// return jobID
|
||||||
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
int addTransferJob(Entry from, bool isRemoteToLocal) {
|
||||||
final jobID = JobController.jobID.next();
|
final jobID = JobController.jobID.next();
|
||||||
@@ -1000,7 +1072,10 @@ class JobController {
|
|||||||
id = int.parse(evt['id']);
|
id = int.parse(evt['id']);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
final jobIndex = getJob(id);
|
final jobIndex = getJob(id);
|
||||||
if (jobIndex == -1) return true;
|
if (jobIndex == -1) {
|
||||||
|
unregisterTransferConflictJob(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
final job = jobTable[jobIndex];
|
final job = jobTable[jobIndex];
|
||||||
job.recvJobRes = true;
|
job.recvJobRes = true;
|
||||||
if (job.type == JobType.deleteFile) {
|
if (job.type == JobType.deleteFile) {
|
||||||
@@ -1026,6 +1101,9 @@ class JobController {
|
|||||||
job.state = JobState.done;
|
job.state = JobState.done;
|
||||||
}
|
}
|
||||||
jobTable.refresh();
|
jobTable.refresh();
|
||||||
|
if (job.state == JobState.done || job.state == JobState.error) {
|
||||||
|
unregisterTransferConflictJob(id);
|
||||||
|
}
|
||||||
if (job.type == JobType.deleteDir) {
|
if (job.type == JobType.deleteDir) {
|
||||||
return job.state == JobState.done;
|
return job.state == JobState.done;
|
||||||
} else {
|
} else {
|
||||||
@@ -1035,9 +1113,15 @@ class JobController {
|
|||||||
|
|
||||||
void jobError(Map<String, dynamic> evt) {
|
void jobError(Map<String, dynamic> evt) {
|
||||||
final err = evt['err'].toString();
|
final err = evt['err'].toString();
|
||||||
int jobIndex = getJob(int.parse(evt['id']));
|
final id = int.tryParse(evt['id']?.toString() ?? '');
|
||||||
|
if (id == null) {
|
||||||
|
debugPrint("Ignore job error with invalid id: $evt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int jobIndex = getJob(id);
|
||||||
if (jobIndex != -1) {
|
if (jobIndex != -1) {
|
||||||
final job = jobTable[jobIndex];
|
final job = jobTable[jobIndex];
|
||||||
|
if (job.state == JobState.done && job.err == "cancel") return;
|
||||||
job.state = JobState.error;
|
job.state = JobState.error;
|
||||||
job.err = err;
|
job.err = err;
|
||||||
job.recvJobRes = true;
|
job.recvJobRes = true;
|
||||||
@@ -1060,6 +1144,11 @@ class JobController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
jobTable.refresh();
|
jobTable.refresh();
|
||||||
|
if (job.state == JobState.done || job.state == JobState.error) {
|
||||||
|
unregisterTransferConflictJob(job.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unregisterTransferConflictJob(id);
|
||||||
}
|
}
|
||||||
if (err == _kOneWayFileTransferError) {
|
if (err == _kOneWayFileTransferError) {
|
||||||
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
|
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
|
||||||
@@ -1096,9 +1185,42 @@ class JobController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cancelJob(int id) async {
|
Future<void> cancelJob(int id) async {
|
||||||
|
unregisterTransferConflictJob(id);
|
||||||
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> cancelTransferConflictBatch(int jobId) async {
|
||||||
|
final batchId = _transferConflictJobToBatch[jobId];
|
||||||
|
final batchJobIds = batchId == null ? [jobId] : <int>[];
|
||||||
|
if (batchId != null) {
|
||||||
|
for (final entry in _transferConflictJobToBatch.entries) {
|
||||||
|
if (entry.value == batchId) {
|
||||||
|
batchJobIds.add(entry.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final id in batchJobIds) {
|
||||||
|
unregisterTransferConflictJob(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final jobIdsToCancel = batchJobIds.toSet();
|
||||||
|
for (final job in jobTable) {
|
||||||
|
if (!jobIdsToCancel.contains(job.id) || job.state == JobState.done) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
job.state = JobState.done;
|
||||||
|
job.err = "cancel";
|
||||||
|
job.recvJobRes = true;
|
||||||
|
}
|
||||||
|
jobTable.refresh();
|
||||||
|
for (final id in batchJobIds) {
|
||||||
|
try {
|
||||||
|
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to cancel transfer job $id in conflict batch: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadLastJob(Map<String, dynamic> evt) async {
|
Future<void> loadLastJob(Map<String, dynamic> evt) async {
|
||||||
debugPrint("load last job: $evt");
|
debugPrint("load last job: $evt");
|
||||||
Map<String, dynamic> jobDetail = json.decode(evt['value']);
|
Map<String, dynamic> jobDetail = json.decode(evt['value']);
|
||||||
@@ -1145,7 +1267,7 @@ class JobController {
|
|||||||
..state = JobState.paused;
|
..state = JobState.paused;
|
||||||
jobTable.add(jobProgress);
|
jobTable.add(jobProgress);
|
||||||
}
|
}
|
||||||
|
registerTransferConflictBatch([currJobId]);
|
||||||
await bind.sessionAddJob(
|
await bind.sessionAddJob(
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
isRemote: isRemote,
|
isRemote: isRemote,
|
||||||
@@ -1193,6 +1315,9 @@ class JobController {
|
|||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
jobTable.clear();
|
jobTable.clear();
|
||||||
|
_transferConflictJobToBatch.clear();
|
||||||
|
_transferConflictRememberBatchId = null;
|
||||||
|
_transferConflictRememberOverrideConfirm = null;
|
||||||
jobResultListener.clear();
|
jobResultListener.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1535,6 +1660,9 @@ class JobProgress {
|
|||||||
|
|
||||||
String display() {
|
String display() {
|
||||||
if (type == JobType.transfer) {
|
if (type == JobType.transfer) {
|
||||||
|
if (state == JobState.done && err == "cancel") {
|
||||||
|
return translate("Cancel");
|
||||||
|
}
|
||||||
if (state == JobState.done && err == "skipped") {
|
if (state == JobState.done && err == "skipped") {
|
||||||
return translate("Skipped");
|
return translate("Skipped");
|
||||||
}
|
}
|
||||||
@@ -1844,21 +1972,44 @@ class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> {
|
|||||||
|
|
||||||
class FileDialogEventLoop
|
class FileDialogEventLoop
|
||||||
extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
|
extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
|
||||||
|
int? _batchId;
|
||||||
bool? _overrideConfirm;
|
bool? _overrideConfirm;
|
||||||
bool _skip = false;
|
bool _skip = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onPreConsume(
|
Future<void> onPreConsume(
|
||||||
BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
|
BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
|
||||||
var event = evt as _FileDialogEvent;
|
final event = evt as _FileDialogEvent;
|
||||||
|
final model = event.fileModel.target;
|
||||||
|
final jobId = int.tryParse(evt.data['id']?.toString() ?? '');
|
||||||
|
final batchId = model == null || jobId == null
|
||||||
|
? null
|
||||||
|
: model.jobController.transferConflictBatchId(jobId);
|
||||||
|
final keepRemembered = model != null &&
|
||||||
|
model.jobController.isTransferConflictRememberBatch(batchId);
|
||||||
|
// The loop only preloads the remembered batch choice. The model updates it
|
||||||
|
// after the user answers the current overwrite dialog.
|
||||||
|
if (_batchId != batchId && !keepRemembered) {
|
||||||
|
_batchId = batchId;
|
||||||
|
_overrideConfirm = null;
|
||||||
|
_skip = false;
|
||||||
|
} else {
|
||||||
|
_batchId = batchId;
|
||||||
|
}
|
||||||
|
if (keepRemembered) {
|
||||||
|
_overrideConfirm =
|
||||||
|
model.jobController.transferConflictRememberOverrideConfirm(batchId);
|
||||||
|
_skip = _overrideConfirm == null;
|
||||||
|
}
|
||||||
event.setOverrideConfirm(_overrideConfirm);
|
event.setOverrideConfirm(_overrideConfirm);
|
||||||
event.setSkip(_skip);
|
event.setSkip(_skip);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>");
|
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} batchId: $_batchId overrideConfirm: $_overrideConfirm, skip: $_skip>");
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onEventsClear() {
|
Future<void> onEventsClear() {
|
||||||
|
_batchId = null;
|
||||||
_overrideConfirm = null;
|
_overrideConfirm = null;
|
||||||
_skip = false;
|
_skip = false;
|
||||||
return super.onEventsClear();
|
return super.onEventsClear();
|
||||||
|
|||||||
@@ -1307,7 +1307,8 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
final canvasPosition = _pointerPositionForRemoteCanvas(e);
|
||||||
|
handleMouse(_getMouseEvent(e, _kMouseEventMove), canvasPosition,
|
||||||
edgeScroll: useEdgeScroll);
|
edgeScroll: useEdgeScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1548,7 +1549,8 @@ class InputModel {
|
|||||||
_relativeMouse
|
_relativeMouse
|
||||||
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventDown));
|
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventDown));
|
||||||
} else {
|
} else {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
|
final canvasPosition = _pointerPositionForRemoteCanvas(e);
|
||||||
|
handleMouse(_getMouseEvent(e, _kMouseEventDown), canvasPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1570,7 +1572,8 @@ class InputModel {
|
|||||||
_relativeMouse
|
_relativeMouse
|
||||||
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventUp));
|
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventUp));
|
||||||
} else {
|
} else {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
|
final canvasPosition = _pointerPositionForRemoteCanvas(e);
|
||||||
|
handleMouse(_getMouseEvent(e, _kMouseEventUp), canvasPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1592,12 +1595,40 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
final canvasPosition = _pointerPositionForRemoteCanvas(e);
|
||||||
|
handleMouse(_getMouseEvent(e, _kMouseEventMove), canvasPosition,
|
||||||
edgeScroll: useEdgeScroll);
|
edgeScroll: useEdgeScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert pointer coordinates into the visible remote canvas space.
|
||||||
|
///
|
||||||
|
/// On mobile, the remote page body is wrapped in `SafeArea`, but the pointer
|
||||||
|
/// listener that feeds these events sits outside that subtree. As a result,
|
||||||
|
/// `event.localPosition` still includes the top/left safe-area inset.
|
||||||
|
///
|
||||||
|
/// When the keyboard-visible path shows `KeyHelpTools`, the remote canvas is
|
||||||
|
/// also shifted downward by `CanvasModel.getAdjustY()`. The downstream mouse
|
||||||
|
/// mapping logic expects coordinates relative to the visible canvas area, so
|
||||||
|
/// we subtract both the mobile safe-area padding and the current canvas
|
||||||
|
/// adjustment before passing the position into mouse mapping.
|
||||||
|
///
|
||||||
|
/// Desktop and web desktop continue to use the global position directly
|
||||||
|
/// because their pointer mapping is window-based.
|
||||||
|
Offset _pointerPositionForRemoteCanvas(PointerEvent event) {
|
||||||
|
if (isDesktop || isWebDesktop) {
|
||||||
|
return event.position;
|
||||||
|
}
|
||||||
|
final mediaData = MediaQueryData.fromView(
|
||||||
|
WidgetsBinding.instance.platformDispatcher.views.first);
|
||||||
|
final adjustY = parent.target?.canvasModel.getAdjustY() ?? 0.0;
|
||||||
|
return Offset(
|
||||||
|
event.localPosition.dx - mediaData.padding.left,
|
||||||
|
event.localPosition.dy - mediaData.padding.top - adjustY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
|
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
|
||||||
List<RemoteWindowCoords> remoteWindowCoords) async {
|
List<RemoteWindowCoords> remoteWindowCoords) async {
|
||||||
final coords =
|
final coords =
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ import 'package:flutter_hbb/native/custom_cursor.dart'
|
|||||||
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
|
||||||
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
|
||||||
final _constSessionId = Uuid().v4obj();
|
final _constSessionId = Uuid().v4obj();
|
||||||
|
// Empirical restart reconnect cadence: keep the last frame briefly and retry quickly.
|
||||||
|
const _restartReconnectSilentDelaySecs = 5;
|
||||||
|
|
||||||
class CachedPeerData {
|
class CachedPeerData {
|
||||||
Map<String, dynamic> updatePrivacyMode = {};
|
Map<String, dynamic> updatePrivacyMode = {};
|
||||||
@@ -110,6 +112,9 @@ class CachedPeerData {
|
|||||||
class FfiModel with ChangeNotifier {
|
class FfiModel with ChangeNotifier {
|
||||||
CachedPeerData cachedPeerData = CachedPeerData();
|
CachedPeerData cachedPeerData = CachedPeerData();
|
||||||
PeerInfo _pi = PeerInfo();
|
PeerInfo _pi = PeerInfo();
|
||||||
|
int? lastUserDisplay;
|
||||||
|
int? pendingMonitorRestore;
|
||||||
|
Timer? _pendingRestoreTimer;
|
||||||
Rect? _rect;
|
Rect? _rect;
|
||||||
|
|
||||||
var _inputBlocked = false;
|
var _inputBlocked = false;
|
||||||
@@ -119,6 +124,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
bool _touchMode = false;
|
bool _touchMode = false;
|
||||||
late VirtualMouseMode virtualMouseMode;
|
late VirtualMouseMode virtualMouseMode;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
Timer? _restartReconnectDelayTimer;
|
||||||
var _reconnects = 1;
|
var _reconnects = 1;
|
||||||
DateTime? _offlineReconnectStartTime;
|
DateTime? _offlineReconnectStartTime;
|
||||||
bool _viewOnly = false;
|
bool _viewOnly = false;
|
||||||
@@ -245,11 +251,14 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
_pi = PeerInfo();
|
_pi = PeerInfo();
|
||||||
|
lastUserDisplay = null;
|
||||||
|
_cancelPendingMonitorRestore();
|
||||||
_secure = null;
|
_secure = null;
|
||||||
_direct = null;
|
_direct = null;
|
||||||
_inputBlocked = false;
|
_inputBlocked = false;
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
waitForImageTimer?.cancel();
|
waitForImageTimer?.cancel();
|
||||||
timerScreenshot?.cancel();
|
timerScreenshot?.cancel();
|
||||||
@@ -341,6 +350,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'connection_ready') {
|
} else if (name == 'connection_ready') {
|
||||||
setConnectionType(peerId, evt['secure'] == 'true',
|
setConnectionType(peerId, evt['secure'] == 'true',
|
||||||
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
||||||
|
resetRestartReconnectState();
|
||||||
} else if (name == 'switch_display') {
|
} else if (name == 'switch_display') {
|
||||||
// switch display is kept for backward compatibility
|
// switch display is kept for backward compatibility
|
||||||
handleSwitchDisplay(evt, sessionId, peerId);
|
handleSwitchDisplay(evt, sessionId, peerId);
|
||||||
@@ -922,8 +932,29 @@ class FfiModel with ChangeNotifier {
|
|||||||
enterUserLoginAndPasswordDialog(
|
enterUserLoginAndPasswordDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||||
} else if (type == 'restarting') {
|
} else if (type == 'restarting') {
|
||||||
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
|
// Treat restart messages as reconnect control events. Rust still sends
|
||||||
hasCancel: false);
|
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
||||||
|
// frame briefly, then shows the Connecting overlay.
|
||||||
|
if (_restartReconnectDelayTimer == null) {
|
||||||
|
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||||
|
_cancelPendingMonitorRestore();
|
||||||
|
bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
|
||||||
|
clearPermissions();
|
||||||
|
// Retry once more after the silent window so restart reconnect attempts
|
||||||
|
// are spaced by the empirical short cadence instead of only updating UI.
|
||||||
|
_restartReconnectDelayTimer =
|
||||||
|
Timer(Duration(seconds: _restartReconnectSilentDelaySecs), () {
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
if (parent.target?.closed == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (type == 'restarting-show') {
|
||||||
|
_restartReconnectDelayTimer?.cancel();
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
} else if (type == 'wait-remote-accept-nook') {
|
} else if (type == 'wait-remote-accept-nook') {
|
||||||
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
||||||
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
||||||
@@ -949,6 +980,11 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetRestartReconnectState() {
|
||||||
|
_restartReconnectDelayTimer?.cancel();
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Auto-retry check for "Remote desktop is offline" error.
|
/// Auto-retry check for "Remote desktop is offline" error.
|
||||||
/// returns true to auto-retry, false otherwise.
|
/// returns true to auto-retry, false otherwise.
|
||||||
bool shouldAutoRetryOnOffline(
|
bool shouldAutoRetryOnOffline(
|
||||||
@@ -1054,10 +1090,22 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _cancelPendingMonitorRestore() {
|
||||||
|
_pendingRestoreTimer?.cancel();
|
||||||
|
_pendingRestoreTimer = null;
|
||||||
|
pendingMonitorRestore = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelPendingRestoreTimer() {
|
||||||
|
_pendingRestoreTimer?.cancel();
|
||||||
|
_pendingRestoreTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
||||||
bool forceRelay) {
|
bool forceRelay) {
|
||||||
// Disable relative mouse mode before reconnecting to ensure cursor is released.
|
// Disable relative mouse mode before reconnecting to ensure cursor is released.
|
||||||
parent.target?.inputModel.setRelativeMouseMode(false);
|
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||||
|
_cancelPendingMonitorRestore();
|
||||||
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
@@ -1371,9 +1419,29 @@ class FfiModel with ChangeNotifier {
|
|||||||
// now replaced to _updateCurDisplay
|
// now replaced to _updateCurDisplay
|
||||||
updateCurDisplay(sessionId);
|
updateCurDisplay(sessionId);
|
||||||
}
|
}
|
||||||
|
// After reconnecting, restore the last selected monitor once the canvas is ready.
|
||||||
|
// Switching earlier can offset the view if the monitor sizes differ.
|
||||||
|
final last = lastUserDisplay;
|
||||||
|
pendingMonitorRestore = (!isCache &&
|
||||||
|
last != null &&
|
||||||
|
last != currentDisplay &&
|
||||||
|
bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||||
|
sessionId: sessionId) !=
|
||||||
|
'Y' &&
|
||||||
|
((last == kAllDisplayValue && _pi.displays.isNotEmpty) ||
|
||||||
|
(last >= 0 && last < _pi.displays.length)))
|
||||||
|
? last
|
||||||
|
: null;
|
||||||
|
// Fallback if the first image event never reaches this tab (multi-UI).
|
||||||
|
_pendingRestoreTimer?.cancel();
|
||||||
|
if (pendingMonitorRestore != null) {
|
||||||
|
_pendingRestoreTimer = Timer(const Duration(milliseconds: 1500),
|
||||||
|
() => parent.target?._applyPendingMonitorRestore());
|
||||||
|
}
|
||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
_reconnects = 1;
|
_reconnects = 1;
|
||||||
_offlineReconnectStartTime = null;
|
_offlineReconnectStartTime = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
@@ -3666,6 +3734,7 @@ class FFI {
|
|||||||
|
|
||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
void mobileReset() {
|
void mobileReset() {
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
ffiModel.waitForFirstImage.value = true;
|
ffiModel.waitForFirstImage.value = true;
|
||||||
ffiModel.isRefreshing = false;
|
ffiModel.isRefreshing = false;
|
||||||
ffiModel.waitForImageDialogShow.value = true;
|
ffiModel.waitForImageDialogShow.value = true;
|
||||||
@@ -3879,13 +3948,32 @@ class FFI {
|
|||||||
}
|
}
|
||||||
if (ffiModel.waitForFirstImage.value == true) {
|
if (ffiModel.waitForFirstImage.value == true) {
|
||||||
ffiModel.waitForFirstImage.value = false;
|
ffiModel.waitForFirstImage.value = false;
|
||||||
|
ffiModel.cancelPendingRestoreTimer();
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
|
try {
|
||||||
await canvasModel.updateViewStyle();
|
await canvasModel.updateViewStyle();
|
||||||
await canvasModel.updateScrollStyle();
|
await canvasModel.updateScrollStyle();
|
||||||
await canvasModel.initializeEdgeScrollEdgeThickness();
|
await canvasModel.initializeEdgeScrollEdgeThickness();
|
||||||
for (final cb in imageModel.callbacksOnFirstImage) {
|
for (final cb in imageModel.callbacksOnFirstImage) {
|
||||||
cb(id);
|
cb(id);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
_applyPendingMonitorRestore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyPendingMonitorRestore() {
|
||||||
|
final restore = ffiModel.pendingMonitorRestore;
|
||||||
|
ffiModel._cancelPendingMonitorRestore();
|
||||||
|
if (restore == null || closed) return;
|
||||||
|
// The display list may have changed since the restore was queued.
|
||||||
|
final displays = ffiModel.pi.displays;
|
||||||
|
if ((restore == kAllDisplayValue && displays.isNotEmpty) ||
|
||||||
|
(restore >= 0 && restore < displays.length)) {
|
||||||
|
openMonitorInTheSameTab(restore, this, ffiModel.pi,
|
||||||
|
recordSelection: false, updateCursorPos: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ class Peer {
|
|||||||
note == other.note;
|
note == other.note;
|
||||||
}
|
}
|
||||||
|
|
||||||
Peer.copy(Peer other)
|
factory Peer.copy(Peer other) {
|
||||||
: this(
|
final peer = Peer(
|
||||||
id: other.id,
|
id: other.id,
|
||||||
hash: other.hash,
|
hash: other.hash,
|
||||||
password: other.password,
|
password: other.password,
|
||||||
@@ -162,6 +162,9 @@ class Peer {
|
|||||||
device_group_name: other.device_group_name,
|
device_group_name: other.device_group_name,
|
||||||
note: other.note,
|
note: other.note,
|
||||||
sameServer: other.sameServer);
|
sameServer: other.sameServer);
|
||||||
|
peer.online = other.online;
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UpdateEvent { online, load }
|
enum UpdateEvent { online, load }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
@@ -38,6 +37,10 @@ class TerminalModel with ChangeNotifier {
|
|||||||
|
|
||||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||||
|
|
||||||
|
/// Called when the terminal session ends (shell exits).
|
||||||
|
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
|
||||||
|
VoidCallback? onClosed;
|
||||||
|
|
||||||
Future<void> _handleInput(String data) async {
|
Future<void> _handleInput(String data) async {
|
||||||
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
|
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
|
||||||
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
|
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
|
||||||
@@ -247,6 +250,33 @@ class TerminalModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int getExitCodeFromEvt(Map<String, dynamic> evt) {
|
||||||
|
if (evt.containsKey('exit_code')) {
|
||||||
|
final v = evt['exit_code'];
|
||||||
|
if (v is int) {
|
||||||
|
// Desktop and mobile send exit_code as an int
|
||||||
|
return v;
|
||||||
|
} else if (v is String) {
|
||||||
|
// Web sends exit_code as a string
|
||||||
|
final parsed = int.tryParse(v);
|
||||||
|
if (parsed != null) {
|
||||||
|
return parsed;
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalModel] Failed to parse exit_code as integer: $v. Expected a numeric string.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalModel] Unexpected exit_code type: ${v.runtimeType}, value: $v. Expected int or String.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint('[TerminalModel] Event does not contain exit_code');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void handleTerminalResponse(Map<String, dynamic> evt) {
|
void handleTerminalResponse(Map<String, dynamic> evt) {
|
||||||
final String? type = evt['type'];
|
final String? type = evt['type'];
|
||||||
final int evtTerminalId = getTerminalIdFromEvt(evt);
|
final int evtTerminalId = getTerminalIdFromEvt(evt);
|
||||||
@@ -469,10 +499,12 @@ class TerminalModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||||
final int exitCode = evt['exit_code'] ?? 0;
|
final int exitCode = getExitCodeFromEvt(evt);
|
||||||
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||||
_terminalOpened = false;
|
_terminalOpened = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
// Auto-close the tab/page
|
||||||
|
onClosed?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||||
|
|||||||
@@ -1914,6 +1914,15 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("sessionHandleScreenshot");
|
throw UnimplementedError("sessionHandleScreenshot");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> sessionSetCommon(
|
||||||
|
{required UuidValue sessionId, required String key, required String value, dynamic hint}) {
|
||||||
|
js.context.callMethod('setByName', [
|
||||||
|
'common',
|
||||||
|
jsonEncode({'name': key, 'value': value})
|
||||||
|
]);
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
String? sessionGetCommonSync(
|
String? sessionGetCommonSync(
|
||||||
{required UuidValue sessionId,
|
{required UuidValue sessionId,
|
||||||
required String key,
|
required String key,
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
|
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
|
||||||
|
|
||||||
// The copyright displayed in application information
|
// The copyright displayed in application information
|
||||||
PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved.
|
PRODUCT_COPYRIGHT = Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# 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
|
# 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.9+67
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
|
|||||||
148
flutter/test/autocomplete_peer_merge_test.dart
Normal file
148
flutter/test/autocomplete_peer_merge_test.dart
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import 'package:flutter_hbb/common/widgets/autocomplete.dart';
|
||||||
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
Peer _peer({
|
||||||
|
required String id,
|
||||||
|
String alias = '',
|
||||||
|
String username = '',
|
||||||
|
String hostname = '',
|
||||||
|
bool online = false,
|
||||||
|
}) {
|
||||||
|
final peer = Peer(
|
||||||
|
id: id,
|
||||||
|
username: username,
|
||||||
|
hostname: hostname,
|
||||||
|
alias: alias,
|
||||||
|
platform: '',
|
||||||
|
tags: [],
|
||||||
|
hash: '',
|
||||||
|
password: '',
|
||||||
|
forceAlwaysRelay: false,
|
||||||
|
rdpPort: '',
|
||||||
|
rdpUsername: '',
|
||||||
|
loginName: '',
|
||||||
|
device_group_name: '',
|
||||||
|
note: '',
|
||||||
|
);
|
||||||
|
peer.online = online;
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('merged autocomplete peers keep address book metadata and online state',
|
||||||
|
() {
|
||||||
|
final peers = mergeAutocompletePeers(
|
||||||
|
addressBookPeers: [
|
||||||
|
_peer(id: '123456789', alias: 'Office PC', username: 'ab-user'),
|
||||||
|
],
|
||||||
|
lanPeers: [
|
||||||
|
_peer(id: '123456789', username: 'lan-user', online: true),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(peers, hasLength(1));
|
||||||
|
expect(peers.single.id, '123456789');
|
||||||
|
expect(peers.single.alias, 'Office PC');
|
||||||
|
expect(peers.single.username, 'ab-user');
|
||||||
|
expect(peers.single.online, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('peer copies preserve online state', () {
|
||||||
|
final peer = _peer(id: '987654321', online: true);
|
||||||
|
|
||||||
|
expect(Peer.copy(peer).online, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('online callbacks update autocomplete-only peers', () {
|
||||||
|
final peers = mergeAutocompletePeers(restRecentPeerIds: ['112233445']);
|
||||||
|
|
||||||
|
final changed = updateAutocompletePeerOnlineStates(
|
||||||
|
peers,
|
||||||
|
onlines: {'112233445'},
|
||||||
|
offlines: {},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(changed, isTrue);
|
||||||
|
expect(peers.single.online, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('online query ids are deduplicated and limited', () {
|
||||||
|
final peers = List.generate(
|
||||||
|
25,
|
||||||
|
(index) => _peer(id: index.toString()),
|
||||||
|
)..insert(1, _peer(id: '0'));
|
||||||
|
|
||||||
|
final ids = autocompleteOnlineQueryIds(peers, limit: 20);
|
||||||
|
|
||||||
|
expect(ids, hasLength(20));
|
||||||
|
expect(ids.first, '0');
|
||||||
|
expect(ids.where((id) => id == '0'), hasLength(1));
|
||||||
|
expect(ids.last, '19');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty online query ids cancel pending debounce', () async {
|
||||||
|
final queriedIds = <List<String>>[];
|
||||||
|
final loader = AllPeersLoader(
|
||||||
|
queryOnlines: (ids) async {
|
||||||
|
queriedIds.add(ids);
|
||||||
|
},
|
||||||
|
queryOnlineDebounce: Duration(milliseconds: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
loader.queryOnlines([_peer(id: '123456789')]);
|
||||||
|
loader.queryOnlines([]);
|
||||||
|
await Future.delayed(Duration(milliseconds: 2));
|
||||||
|
|
||||||
|
expect(queriedIds, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed online query enqueue does not suppress retry', () async {
|
||||||
|
var queryCount = 0;
|
||||||
|
final loader = AllPeersLoader(
|
||||||
|
queryOnlines: (ids) {
|
||||||
|
queryCount += 1;
|
||||||
|
return Future<void>.error(Exception('queue full'));
|
||||||
|
},
|
||||||
|
queryOnlineDebounce: Duration(milliseconds: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
loader.queryOnlines([_peer(id: '123456789')]);
|
||||||
|
await Future.delayed(Duration(milliseconds: 2));
|
||||||
|
|
||||||
|
loader.queryOnlines([_peer(id: '123456789')]);
|
||||||
|
await Future.delayed(Duration(milliseconds: 2));
|
||||||
|
|
||||||
|
expect(queryCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('online callback updates currently displayed options', () async {
|
||||||
|
final loader = AllPeersLoader(
|
||||||
|
queryOnlines: (ids) async {},
|
||||||
|
queryOnlineDebounce: Duration(milliseconds: 1),
|
||||||
|
);
|
||||||
|
final displayedOptions = [_peer(id: '123456789')];
|
||||||
|
|
||||||
|
loader.queryOnlines(displayedOptions);
|
||||||
|
loader.updateOnlineStateForTesting({
|
||||||
|
'onlines': '123456789',
|
||||||
|
'offlines': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(displayedOptions.single.online, isTrue);
|
||||||
|
await Future.delayed(Duration(milliseconds: 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cached online callback state is reapplied after peers merge', () {
|
||||||
|
final loader = AllPeersLoader();
|
||||||
|
loader.updateOnlineStateForTesting({
|
||||||
|
'onlines': '123456789',
|
||||||
|
'offlines': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
final mergedPeers = [_peer(id: '123456789')];
|
||||||
|
loader.applyLastOnlineStateForTesting(mergedPeers);
|
||||||
|
|
||||||
|
expect(mergedPeers.single.online, isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
63
flutter/test/cm_demo.dart
Normal file
63
flutter/test/cm_demo.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
||||||
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
final testClients = [
|
||||||
|
Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false),
|
||||||
|
Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false),
|
||||||
|
Client(2, false, false, false, "UserC", "331123123", true, false, false),
|
||||||
|
Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false,
|
||||||
|
false)
|
||||||
|
];
|
||||||
|
|
||||||
|
/// flutter run -d {platform} -t test/cm_demo.dart to test cm
|
||||||
|
void main() async {
|
||||||
|
isTest = true;
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await windowManager.ensureInitialized();
|
||||||
|
await windowManager.setSize(const Size(400, 600));
|
||||||
|
await windowManager.setAlignment(Alignment.topRight);
|
||||||
|
await initEnv(kAppTypeMain);
|
||||||
|
for (var client in testClients) {
|
||||||
|
gFFI.serverModel.clients.add(client);
|
||||||
|
gFFI.serverModel.tabController.add(TabInfo(
|
||||||
|
key: client.id.toString(),
|
||||||
|
label: client.name,
|
||||||
|
closable: false,
|
||||||
|
page: buildConnectionCard(client)));
|
||||||
|
}
|
||||||
|
|
||||||
|
runApp(GetMaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: MyTheme.lightTheme,
|
||||||
|
darkTheme: MyTheme.darkTheme,
|
||||||
|
themeMode: MyTheme.currentThemeMode(),
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: supportedLocales,
|
||||||
|
home: const DesktopServerPage()));
|
||||||
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||||
|
size: kConnectionManagerWindowSizeClosedChat);
|
||||||
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||||
|
await windowManager.show();
|
||||||
|
// ensure initial window size to be changed
|
||||||
|
await windowManager.setSize(kConnectionManagerWindowSizeClosedChat);
|
||||||
|
await Future.wait([
|
||||||
|
windowManager.setAlignment(Alignment.topRight),
|
||||||
|
windowManager.focus(),
|
||||||
|
windowManager.setOpacity(1)
|
||||||
|
]);
|
||||||
|
// ensure
|
||||||
|
windowManager.setAlignment(Alignment.topRight);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,62 +1,20 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
|
||||||
import 'package:flutter_hbb/consts.dart';
|
|
||||||
import 'package:flutter_hbb/desktop/pages/server_page.dart';
|
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
|
||||||
import 'package:flutter_hbb/main.dart';
|
|
||||||
import 'package:flutter_hbb/models/server_model.dart';
|
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
|
||||||
|
|
||||||
final testClients = [
|
import 'cm_demo.dart' as cm_demo;
|
||||||
Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false),
|
|
||||||
Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false),
|
|
||||||
Client(2, false, false, false, "UserC", "331123123", true, false, false, false),
|
|
||||||
Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false, false)
|
|
||||||
];
|
|
||||||
|
|
||||||
/// flutter run -d {platform} -t test/cm_test.dart to test cm
|
void main() {
|
||||||
void main(List<String> args) async {
|
test('connection manager demo clients match the current Client API', () {
|
||||||
isTest = true;
|
expect(cm_demo.testClients, hasLength(4));
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
expect(cm_demo.testClients.map((client) => client.name), [
|
||||||
await windowManager.ensureInitialized();
|
'UserAAAAAA',
|
||||||
await windowManager.setSize(const Size(400, 600));
|
'UserBBBBB',
|
||||||
await windowManager.setAlignment(Alignment.topRight);
|
'UserC',
|
||||||
await initEnv(kAppTypeMain);
|
'UserDDDDDDDDDDDd',
|
||||||
for (var client in testClients) {
|
|
||||||
gFFI.serverModel.clients.add(client);
|
|
||||||
gFFI.serverModel.tabController.add(TabInfo(
|
|
||||||
key: client.id.toString(),
|
|
||||||
label: client.name,
|
|
||||||
closable: false,
|
|
||||||
page: buildConnectionCard(client)));
|
|
||||||
}
|
|
||||||
|
|
||||||
runApp(GetMaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
theme: MyTheme.lightTheme,
|
|
||||||
darkTheme: MyTheme.darkTheme,
|
|
||||||
themeMode: MyTheme.currentThemeMode(),
|
|
||||||
localizationsDelegates: const [
|
|
||||||
GlobalMaterialLocalizations.delegate,
|
|
||||||
GlobalWidgetsLocalizations.delegate,
|
|
||||||
GlobalCupertinoLocalizations.delegate,
|
|
||||||
],
|
|
||||||
supportedLocales: supportedLocales,
|
|
||||||
home: const DesktopServerPage()));
|
|
||||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
|
||||||
size: kConnectionManagerWindowSizeClosedChat);
|
|
||||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
|
||||||
await windowManager.show();
|
|
||||||
// ensure initial window size to be changed
|
|
||||||
await windowManager.setSize(kConnectionManagerWindowSizeClosedChat);
|
|
||||||
await Future.wait([
|
|
||||||
windowManager.setAlignment(Alignment.topRight),
|
|
||||||
windowManager.focus(),
|
|
||||||
windowManager.setOpacity(1)
|
|
||||||
]);
|
]);
|
||||||
// ensure
|
expect(
|
||||||
windowManager.setAlignment(Alignment.topRight);
|
cm_demo.testClients.every(
|
||||||
|
(client) => client.keyboard && !client.clipboard && !client.audio),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
38
flutter/test/server_settings_dialog_test.dart
Normal file
38
flutter/test/server_settings_dialog_test.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('server settings text fields preserve literal input',
|
||||||
|
(tester) async {
|
||||||
|
final controller = TextEditingController(text: 'AbCdR1c1E=');
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: serverSettingsTextFormField(
|
||||||
|
label: 'Key',
|
||||||
|
controller: controller,
|
||||||
|
errorMsg: '',
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
final textField = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
|
||||||
|
expect(textField.controller, controller);
|
||||||
|
expect(textField.autofocus, isTrue);
|
||||||
|
expect(textField.keyboardType, TextInputType.visiblePassword);
|
||||||
|
expect(textField.textCapitalization, TextCapitalization.none);
|
||||||
|
expect(textField.autocorrect, isFalse);
|
||||||
|
expect(textField.enableSuggestions, isFalse);
|
||||||
|
expect(textField.smartDashesType, SmartDashesType.disabled);
|
||||||
|
expect(textField.smartQuotesType, SmartQuotesType.disabled);
|
||||||
|
expect(textField.enableIMEPersonalizedLearning, isFalse);
|
||||||
|
expect(
|
||||||
|
textField.spellCheckConfiguration,
|
||||||
|
const SpellCheckConfiguration.disabled(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -89,11 +89,11 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
BLOCK "040904e4"
|
BLOCK "040904e4"
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "Purslane Ltd" "\0"
|
VALUE "CompanyName", "Purslane Tech Pte. Ltd." "\0"
|
||||||
VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
|
VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
|
||||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
VALUE "InternalName", "rustdesk" "\0"
|
VALUE "InternalName", "rustdesk" "\0"
|
||||||
VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0"
|
VALUE "LegalCopyright", "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved." "\0"
|
||||||
VALUE "OriginalFilename", "rustdesk.exe" "\0"
|
VALUE "OriginalFilename", "rustdesk.exe" "\0"
|
||||||
VALUE "ProductName", "RustDesk" "\0"
|
VALUE "ProductName", "RustDesk" "\0"
|
||||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ impl FuseServer {
|
|||||||
offset: i64,
|
offset: i64,
|
||||||
size: u32,
|
size: u32,
|
||||||
) -> Result<Vec<u8>, std::io::Error> {
|
) -> Result<Vec<u8>, std::io::Error> {
|
||||||
// todo: async and concurrent read, generate stream_id per request
|
let request_stream_id = rand::random();
|
||||||
let cb_requested = unsafe {
|
let cb_requested = unsafe {
|
||||||
// convert `size` from u32 to i32
|
// convert `size` from u32 to i32
|
||||||
// yet with same bit representation
|
// yet with same bit representation
|
||||||
@@ -543,7 +543,7 @@ impl FuseServer {
|
|||||||
let (n_position_high, n_position_low) =
|
let (n_position_high, n_position_low) =
|
||||||
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
|
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
|
||||||
let request = ClipboardFile::FileContentsRequest {
|
let request = ClipboardFile::FileContentsRequest {
|
||||||
stream_id: node.stream_id,
|
stream_id: request_stream_id,
|
||||||
list_index: node.index as i32,
|
list_index: node.index as i32,
|
||||||
dw_flags: 2,
|
dw_flags: 2,
|
||||||
n_position_low,
|
n_position_low,
|
||||||
@@ -573,7 +573,7 @@ impl FuseServer {
|
|||||||
stream_id,
|
stream_id,
|
||||||
requested_data,
|
requested_data,
|
||||||
} => {
|
} => {
|
||||||
if stream_id != node.stream_id {
|
if stream_id != request_stream_id {
|
||||||
log::debug!("stream id mismatch, ignore");
|
log::debug!("stream id mismatch, ignore");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -611,11 +611,6 @@ struct FuseNode {
|
|||||||
/// connection id
|
/// connection id
|
||||||
pub conn_id: i32,
|
pub conn_id: i32,
|
||||||
|
|
||||||
// todo: use stream_id to identify a FileContents request-reply
|
|
||||||
// instead of a whole file
|
|
||||||
/// stream id
|
|
||||||
pub stream_id: i32,
|
|
||||||
|
|
||||||
/// file index in peer's file list
|
/// file index in peer's file list
|
||||||
/// NOTE:
|
/// NOTE:
|
||||||
/// it is NOT the same as inode, this is the index in the file list
|
/// it is NOT the same as inode, this is the index in the file list
|
||||||
@@ -639,7 +634,6 @@ impl FuseNode {
|
|||||||
pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
|
pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
|
||||||
Self {
|
Self {
|
||||||
conn_id: desc.conn_id,
|
conn_id: desc.conn_id,
|
||||||
stream_id: rand::random(),
|
|
||||||
index: inode as usize - 2,
|
index: inode as usize - 2,
|
||||||
name: desc
|
name: desc
|
||||||
.name
|
.name
|
||||||
@@ -656,7 +650,6 @@ impl FuseNode {
|
|||||||
pub fn new_root() -> Self {
|
pub fn new_root() -> Self {
|
||||||
Self {
|
Self {
|
||||||
conn_id: 0,
|
conn_id: 0,
|
||||||
stream_id: rand::random(),
|
|
||||||
index: 0,
|
index: 0,
|
||||||
name: String::from("/"),
|
name: String::from("/"),
|
||||||
parent: None,
|
parent: None,
|
||||||
|
|||||||
@@ -4,23 +4,24 @@ use super::filetype::FileDescription;
|
|||||||
use crate::{ClipboardFile, CliprdrError};
|
use crate::{ClipboardFile, CliprdrError};
|
||||||
use cs::FuseServer;
|
use cs::FuseServer;
|
||||||
use fuser::MountOption;
|
use fuser::MountOption;
|
||||||
use hbb_common::{config::APP_NAME, log};
|
use hbb_common::{config::Config, log};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
sync::{mpsc::Sender, Arc},
|
sync::{mpsc::Sender, Arc},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
|
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
|
||||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
|
let mnt_path = fuse_mount_point("cliprdr-client");
|
||||||
// No need to run `canonicalize()` here.
|
// No need to run `canonicalize()` here.
|
||||||
Arc::new(mnt_path)
|
Arc::new(mnt_path)
|
||||||
};
|
};
|
||||||
|
|
||||||
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
|
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
|
||||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
|
let mnt_path = fuse_mount_point("cliprdr-server");
|
||||||
// No need to run `canonicalize()` here.
|
// No need to run `canonicalize()` here.
|
||||||
Arc::new(mnt_path)
|
Arc::new(mnt_path)
|
||||||
};
|
};
|
||||||
@@ -31,6 +32,21 @@ lazy_static::lazy_static! {
|
|||||||
|
|
||||||
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
|
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum MountPointState {
|
||||||
|
HealthyMount,
|
||||||
|
NotMounted,
|
||||||
|
StaleMount,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fuse_mount_point(name: &str) -> String {
|
||||||
|
let mut path = PathBuf::from(Config::ipc_path(""));
|
||||||
|
path.pop();
|
||||||
|
path.push(name);
|
||||||
|
path.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
|
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
|
||||||
if is_client {
|
if is_client {
|
||||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||||
@@ -53,8 +69,27 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
|||||||
} else {
|
} else {
|
||||||
FUSE_CONTEXT_SERVER.lock()
|
FUSE_CONTEXT_SERVER.lock()
|
||||||
};
|
};
|
||||||
if fuse_context_lock.is_some() {
|
if let Some(ctx) = fuse_context_lock.as_ref() {
|
||||||
return Ok(());
|
match inspect_mount_point_state(&ctx.mount_point) {
|
||||||
|
MountPointState::HealthyMount => return Ok(()),
|
||||||
|
MountPointState::StaleMount | MountPointState::NotMounted => {
|
||||||
|
log::warn!(
|
||||||
|
"clipboard FUSE mount {} is disconnected, remounting",
|
||||||
|
ctx.mount_point.display()
|
||||||
|
);
|
||||||
|
let stale_context = fuse_context_lock.take();
|
||||||
|
drop(fuse_context_lock);
|
||||||
|
drop(stale_context);
|
||||||
|
return init_fuse_context(is_client);
|
||||||
|
}
|
||||||
|
MountPointState::Unknown => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to verify clipboard FUSE mount {}",
|
||||||
|
ctx.mount_point.display()
|
||||||
|
);
|
||||||
|
return Err(CliprdrError::CliprdrInit);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mount_point = if is_client {
|
let mount_point = if is_client {
|
||||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||||
@@ -63,10 +98,32 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mount_point = std::path::PathBuf::from(&*mount_point);
|
let mount_point = std::path::PathBuf::from(&*mount_point);
|
||||||
|
match inspect_mount_point_state(&mount_point) {
|
||||||
|
MountPointState::HealthyMount => {
|
||||||
|
log::warn!(
|
||||||
|
"clipboard FUSE mount {} is already active in another context",
|
||||||
|
mount_point.display()
|
||||||
|
);
|
||||||
|
return Err(CliprdrError::ClipboardOccupied);
|
||||||
|
}
|
||||||
|
MountPointState::StaleMount => {
|
||||||
|
log::warn!(
|
||||||
|
"clipboard FUSE mount {} is stale, cleaning up before remount",
|
||||||
|
mount_point.display()
|
||||||
|
);
|
||||||
|
unmount_fuse_mount_point(&mount_point);
|
||||||
|
validate_mount_state_after_stale_cleanup(
|
||||||
|
&mount_point,
|
||||||
|
inspect_mount_point_state(&mount_point),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
MountPointState::Unknown => return Err(CliprdrError::CliprdrInit),
|
||||||
|
MountPointState::NotMounted => {}
|
||||||
|
}
|
||||||
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
|
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
|
||||||
let server = Arc::new(Mutex::new(server));
|
let server = Arc::new(Mutex::new(server));
|
||||||
|
|
||||||
prepare_fuse_mount_point(&mount_point);
|
prepare_fuse_mount_point(&mount_point)?;
|
||||||
let mnt_opts = [
|
let mnt_opts = [
|
||||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||||
MountOption::NoAtime,
|
MountOption::NoAtime,
|
||||||
@@ -159,35 +216,246 @@ struct FuseContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this function must be called after the main IPC is up
|
// 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: &PathBuf) -> Result<(), CliprdrError> {
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, Permissions},
|
fs::{self, Permissions},
|
||||||
os::unix::prelude::PermissionsExt,
|
os::unix::prelude::PermissionsExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
fs::create_dir(mount_point).ok();
|
if let Some(parent) = mount_point.parent() {
|
||||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
reject_symlink_path(parent)?;
|
||||||
|
if let Err(e) = fs::create_dir_all(parent) {
|
||||||
|
log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e);
|
||||||
|
return Err(CliprdrError::CliprdrInit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = std::process::Command::new("umount")
|
reject_symlink_path(mount_point)?;
|
||||||
|
|
||||||
|
let recovered_stale_mount = if let Err(e) = fs::create_dir_all(mount_point) {
|
||||||
|
log::warn!(
|
||||||
|
"failed to create clipboard FUSE mount point {}, trying stale mount cleanup: {:?}",
|
||||||
|
mount_point.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
unmount_fuse_mount_point(mount_point);
|
||||||
|
fs::create_dir_all(mount_point).map_err(|e| {
|
||||||
|
log::error!(
|
||||||
|
"failed to create clipboard FUSE mount point {} after cleanup: {:?}",
|
||||||
|
mount_point.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
CliprdrError::CliprdrInit
|
||||||
|
})?;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
if let Err(e) = fs::set_permissions(mount_point, Permissions::from_mode(0o777)) {
|
||||||
|
log::warn!(
|
||||||
|
"failed to set clipboard FUSE mount point permissions {}: {:?}",
|
||||||
|
mount_point.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !recovered_stale_mount {
|
||||||
|
unmount_fuse_mount_point(mount_point);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inspect_mount_point_state(mount_point: &Path) -> MountPointState {
|
||||||
|
if ensure_mount_point_path_is_safe(mount_point).is_err() {
|
||||||
|
return MountPointState::Unknown;
|
||||||
|
}
|
||||||
|
inspect_mount_point_state_with(
|
||||||
|
mount_point,
|
||||||
|
std::fs::metadata(mount_point),
|
||||||
|
std::fs::read_to_string("/proc/self/mountinfo"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_mount_state_after_stale_cleanup(
|
||||||
|
mount_point: &Path,
|
||||||
|
mount_state: MountPointState,
|
||||||
|
) -> Result<(), CliprdrError> {
|
||||||
|
match mount_state {
|
||||||
|
MountPointState::NotMounted => Ok(()),
|
||||||
|
MountPointState::HealthyMount => {
|
||||||
|
log::warn!(
|
||||||
|
"clipboard FUSE mount {} is still active after stale cleanup",
|
||||||
|
mount_point.display()
|
||||||
|
);
|
||||||
|
Err(CliprdrError::ClipboardOccupied)
|
||||||
|
}
|
||||||
|
MountPointState::StaleMount => {
|
||||||
|
log::warn!(
|
||||||
|
"clipboard FUSE mount {} is still stale after cleanup",
|
||||||
|
mount_point.display()
|
||||||
|
);
|
||||||
|
Err(CliprdrError::CliprdrInit)
|
||||||
|
}
|
||||||
|
MountPointState::Unknown => {
|
||||||
|
log::warn!(
|
||||||
|
"failed to verify clipboard FUSE mount {} after cleanup",
|
||||||
|
mount_point.display()
|
||||||
|
);
|
||||||
|
Err(CliprdrError::CliprdrInit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inspect_mount_point_state_with<T>(
|
||||||
|
mount_point: &Path,
|
||||||
|
metadata_result: io::Result<T>,
|
||||||
|
mountinfo_result: io::Result<String>,
|
||||||
|
) -> MountPointState {
|
||||||
|
match metadata_result {
|
||||||
|
Ok(_) => match mountinfo_result {
|
||||||
|
Ok(mountinfo) => {
|
||||||
|
if is_mount_point_listed_in_mountinfo(mount_point, &mountinfo) {
|
||||||
|
MountPointState::HealthyMount
|
||||||
|
} else {
|
||||||
|
MountPointState::NotMounted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to read mountinfo for {:?}: {:?}", mount_point, e);
|
||||||
|
MountPointState::Unknown
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) if e.raw_os_error() == Some(libc::ENOTCONN) => MountPointState::StaleMount,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::NotFound => MountPointState::NotMounted,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to inspect FUSE mount {:?}: {:?}", mount_point, e);
|
||||||
|
MountPointState::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_mount_point_listed_in_mountinfo(mount_point: &Path, mountinfo: &str) -> bool {
|
||||||
|
let mount_point = mount_point.to_string_lossy();
|
||||||
|
mountinfo.lines().any(|line| {
|
||||||
|
let mut fields = line.split_whitespace();
|
||||||
|
let _mount_id = fields.next();
|
||||||
|
let _parent_id = fields.next();
|
||||||
|
let _major_minor = fields.next();
|
||||||
|
let _root = fields.next();
|
||||||
|
let mount_path = fields.next();
|
||||||
|
mount_path == Some(mount_point.as_ref())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_symlink_metadata_result(
|
||||||
|
path: &Path,
|
||||||
|
metadata_result: io::Result<std::fs::Metadata>,
|
||||||
|
allow_disconnected_mount: bool,
|
||||||
|
) -> Result<(), CliprdrError> {
|
||||||
|
match metadata_result {
|
||||||
|
Ok(metadata) if metadata.file_type().is_symlink() => {
|
||||||
|
log::warn!("refusing to use symlinked FUSE path {:?}", path);
|
||||||
|
Err(CliprdrError::CliprdrInit)
|
||||||
|
}
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) if allow_disconnected_mount && e.raw_os_error() == Some(libc::ENOTCONN) => Ok(()),
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("failed to inspect FUSE path {:?}: {:?}", path, e);
|
||||||
|
Err(CliprdrError::CliprdrInit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_symlink_path(path: &Path) -> Result<(), CliprdrError> {
|
||||||
|
reject_symlink_metadata_result(path, std::fs::symlink_metadata(path), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_mount_point_path_is_safe(mount_point: &Path) -> Result<(), CliprdrError> {
|
||||||
|
if let Some(parent) = mount_point.parent() {
|
||||||
|
reject_symlink_path(parent)?;
|
||||||
|
}
|
||||||
|
reject_symlink_metadata_result(mount_point, std::fs::symlink_metadata(mount_point), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmount_fuse_mount_point(mount_point: &Path) {
|
||||||
|
if ensure_mount_point_path_is_safe(mount_point).is_err() {
|
||||||
|
log::warn!(
|
||||||
|
"refusing to unmount unsafe clipboard FUSE mount point {:?}",
|
||||||
|
mount_point
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if inspect_mount_point_state_with(
|
||||||
|
mount_point,
|
||||||
|
std::fs::metadata(mount_point),
|
||||||
|
std::fs::read_to_string("/proc/self/mountinfo"),
|
||||||
|
) == MountPointState::NotMounted
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (program, args) in unmount_command_candidates() {
|
||||||
|
if run_unmount_command(program, args, mount_point) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::warn!(
|
||||||
|
"failed to unmount clipboard FUSE mount point {:?}",
|
||||||
|
mount_point
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unmount_command_candidates() -> [(&'static str, &'static [&'static str]); 3] {
|
||||||
|
[
|
||||||
|
("fusermount3", &["-uz"]),
|
||||||
|
("fusermount", &["-uz"]),
|
||||||
|
("umount", &["-l"]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool {
|
||||||
|
match std::process::Command::new(program)
|
||||||
|
.args(args)
|
||||||
.arg(mount_point)
|
.arg(mount_point)
|
||||||
.status()
|
.status()
|
||||||
{
|
{
|
||||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
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) {
|
fn uninit_fuse_context_(is_client: bool) {
|
||||||
if is_client {
|
let ctx = {
|
||||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
let mut fuse_context_lock = if is_client {
|
||||||
|
FUSE_CONTEXT_CLIENT.lock()
|
||||||
} else {
|
} else {
|
||||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
FUSE_CONTEXT_SERVER.lock()
|
||||||
}
|
};
|
||||||
|
fuse_context_lock.take()
|
||||||
|
};
|
||||||
|
drop(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for FuseContext {
|
impl Drop for FuseContext {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.session.lock().take().map(|s| s.join());
|
self.session.lock().take().map(|s| s.join());
|
||||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
log::info!(
|
||||||
|
"unmounting clipboard FUSE from {}",
|
||||||
|
self.mount_point.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,3 +491,66 @@ impl FuseContext {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::{fs, io};
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_mount_point_state_from_metadata_and_mountinfo() {
|
||||||
|
let mount_point = std::env::temp_dir().join(format!(
|
||||||
|
"rustdesk-fuse-mount-state-test-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
line!()
|
||||||
|
));
|
||||||
|
let mountinfo = format!(
|
||||||
|
"123 1 0:45 / {} rw,nosuid,nodev - fuse.rustdesk rustdesk rw\n",
|
||||||
|
mount_point.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(mountinfo)),
|
||||||
|
MountPointState::HealthyMount
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(String::new())),
|
||||||
|
MountPointState::NotMounted
|
||||||
|
);
|
||||||
|
|
||||||
|
let disconnected_metadata: io::Result<()> =
|
||||||
|
Err(io::Error::from_raw_os_error(libc::ENOTCONN));
|
||||||
|
assert_eq!(
|
||||||
|
inspect_mount_point_state_with(&mount_point, disconnected_metadata, Ok(String::new())),
|
||||||
|
MountPointState::StaleMount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
fn rejects_symlink_mount_point() {
|
||||||
|
let base = std::env::temp_dir().join(format!(
|
||||||
|
"rustdesk-fuse-symlink-test-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
line!()
|
||||||
|
));
|
||||||
|
let mount_parent = base.join("parent");
|
||||||
|
let mount_point = mount_parent.join("cliprdr-client");
|
||||||
|
let symlink_target = base.join("symlink-target");
|
||||||
|
let _ = fs::remove_dir_all(&base);
|
||||||
|
fs::create_dir_all(&base).unwrap();
|
||||||
|
fs::create_dir_all(&mount_parent).unwrap();
|
||||||
|
fs::create_dir_all(&symlink_target).unwrap();
|
||||||
|
symlink(&symlink_target, &mount_point).unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
prepare_fuse_mount_point(&mount_point),
|
||||||
|
Err(CliprdrError::CliprdrInit)
|
||||||
|
));
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -192,20 +192,16 @@ impl LocalFile {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if offset != self.offset.load(Ordering::Relaxed) {
|
let read_result = if offset != self.offset.load(Ordering::Relaxed) {
|
||||||
handle
|
handle
|
||||||
.seek(std::io::SeekFrom::Start(offset))
|
.seek(std::io::SeekFrom::Start(offset))
|
||||||
.map_err(|e| CliprdrError::FileError {
|
.and_then(|_| handle.read_exact(buf))
|
||||||
path: self.path.to_string_lossy().to_string(),
|
} else {
|
||||||
err: e,
|
handle.read_exact(buf)
|
||||||
})?;
|
};
|
||||||
|
if let Err(e) = read_result {
|
||||||
|
return Err(self.invalidate_handle(e));
|
||||||
}
|
}
|
||||||
handle
|
|
||||||
.read_exact(buf)
|
|
||||||
.map_err(|e| CliprdrError::FileError {
|
|
||||||
path: self.path.to_string_lossy().to_string(),
|
|
||||||
err: e,
|
|
||||||
})?;
|
|
||||||
let new_offset = offset + (buf.len() as u64);
|
let new_offset = offset + (buf.len() as u64);
|
||||||
self.offset.store(new_offset, Ordering::Relaxed);
|
self.offset.store(new_offset, Ordering::Relaxed);
|
||||||
|
|
||||||
@@ -217,6 +213,15 @@ impl LocalFile {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn invalidate_handle(&mut self, err: std::io::Error) -> CliprdrError {
|
||||||
|
self.offset.store(0, Ordering::Relaxed);
|
||||||
|
self.handle = None;
|
||||||
|
CliprdrError::FileError {
|
||||||
|
path: self.path.to_string_lossy().to_string(),
|
||||||
|
err,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||||
@@ -278,7 +283,10 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod file_list_test {
|
mod file_list_test {
|
||||||
use std::{path::PathBuf, sync::atomic::AtomicU64};
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::atomic::{AtomicU64, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
use hbb_common::bytes::{BufMut, BytesMut};
|
use hbb_common::bytes::{BufMut, BytesMut};
|
||||||
|
|
||||||
@@ -384,4 +392,30 @@ mod file_list_test {
|
|||||||
as_bin_parse_test("/test")?;
|
as_bin_parse_test("/test")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_exact_at_reopens_after_read_failure() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let file_path = std::env::temp_dir().join(format!(
|
||||||
|
"rustdesk-clipboard-local-file-{}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
std::fs::write(&file_path, b"")?;
|
||||||
|
|
||||||
|
let mut file = LocalFile::try_open(&std::env::temp_dir(), &file_path)?;
|
||||||
|
file.size = 1;
|
||||||
|
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
assert!(file.read_exact_at(&mut buf, 0).is_err());
|
||||||
|
assert!(file.handle.is_none());
|
||||||
|
assert_eq!(file.offset.load(Ordering::Relaxed), 0);
|
||||||
|
|
||||||
|
std::fs::write(&file_path, [42u8])?;
|
||||||
|
|
||||||
|
file.read_exact_at(&mut buf, 0)?;
|
||||||
|
assert_eq!(buf, [42u8]);
|
||||||
|
assert!(file.handle.is_none());
|
||||||
|
|
||||||
|
std::fs::remove_file(file_path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use hbb_common::{
|
|||||||
log,
|
log,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{path::PathBuf, sync::Arc, usize};
|
use std::{path::PathBuf, sync::Arc, time::SystemTime, usize};
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
// local files are cached, this value should not be changed when copying files
|
// local files are cached, this value should not be changed when copying files
|
||||||
@@ -30,9 +30,35 @@ enum FileContentsRequest {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cheap fingerprint of one top-level selected entry. A change in size/mtime --
|
||||||
|
// or a directory in the selection -- forces sync_files() to rebuild (see below).
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
struct FileSig {
|
||||||
|
size: u64,
|
||||||
|
mtime: Option<SystemTime>,
|
||||||
|
is_dir: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat the top-level selected paths only (no recursion), same order as `files`.
|
||||||
|
fn fingerprint(files: &[String]) -> Vec<FileSig> {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.map(|s| match std::fs::metadata(s) {
|
||||||
|
Ok(mt) => FileSig {
|
||||||
|
size: mt.len(),
|
||||||
|
mtime: mt.modified().ok(),
|
||||||
|
is_dir: mt.is_dir(),
|
||||||
|
},
|
||||||
|
Err(_) => FileSig::default(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ClipFiles {
|
struct ClipFiles {
|
||||||
files: Vec<String>,
|
files: Vec<String>,
|
||||||
|
// Fingerprint of `files` (same len/order); detects in-place edits on re-copy.
|
||||||
|
sigs: Vec<FileSig>,
|
||||||
file_list: Vec<LocalFile>,
|
file_list: Vec<LocalFile>,
|
||||||
first_file_index: usize,
|
first_file_index: usize,
|
||||||
files_pdu: Vec<u8>,
|
files_pdu: Vec<u8>,
|
||||||
@@ -41,12 +67,17 @@ struct ClipFiles {
|
|||||||
impl ClipFiles {
|
impl ClipFiles {
|
||||||
fn clear(&mut self) {
|
fn clear(&mut self) {
|
||||||
self.files.clear();
|
self.files.clear();
|
||||||
|
self.sigs.clear();
|
||||||
self.file_list.clear();
|
self.file_list.clear();
|
||||||
self.first_file_index = usize::MAX;
|
self.first_file_index = usize::MAX;
|
||||||
self.files_pdu.clear();
|
self.files_pdu.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
|
fn sync_files(
|
||||||
|
&mut self,
|
||||||
|
clipboard_files: &[String],
|
||||||
|
sigs: Vec<FileSig>,
|
||||||
|
) -> Result<(), CliprdrError> {
|
||||||
let clipboard_paths = clipboard_files
|
let clipboard_paths = clipboard_files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| PathBuf::from(s))
|
.map(|s| PathBuf::from(s))
|
||||||
@@ -58,6 +89,7 @@ impl ClipFiles {
|
|||||||
.position(|f| !f.path.is_dir())
|
.position(|f| !f.path.is_dir())
|
||||||
.unwrap_or(usize::MAX);
|
.unwrap_or(usize::MAX);
|
||||||
self.files = clipboard_files.to_vec();
|
self.files = clipboard_files.to_vec();
|
||||||
|
self.sigs = sigs;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,14 +290,128 @@ pub fn read_file_contents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
||||||
|
// Dedup: skip the rebuild only when paths + sizes + mtimes match and no dir is
|
||||||
|
// selected (a dir's own mtime doesn't change when a file inside it is edited).
|
||||||
|
let current = fingerprint(files);
|
||||||
let mut files_lock = CLIP_FILES.lock();
|
let mut files_lock = CLIP_FILES.lock();
|
||||||
if files_lock.files == files {
|
if files_lock.files == files
|
||||||
|
&& files_lock.sigs == current
|
||||||
|
&& !current.iter().any(|sig| sig.is_dir)
|
||||||
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
files_lock.sync_files(files)?;
|
files_lock.sync_files(files, current)?;
|
||||||
Ok(files_lock.build_file_list_pdu())
|
Ok(files_lock.build_file_list_pdu())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_file_list_pdu() -> Vec<u8> {
|
pub fn get_file_list_pdu() -> Vec<u8> {
|
||||||
CLIP_FILES.lock().files_pdu.clone()
|
CLIP_FILES.lock().files_pdu.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod sig_test {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
// Unique temp dir under the system temp dir; removed on drop (no dev-dep).
|
||||||
|
struct TmpDir(PathBuf);
|
||||||
|
impl TmpDir {
|
||||||
|
fn new(tag: &str) -> Self {
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut dir = std::env::temp_dir();
|
||||||
|
dir.push(format!("rustdesk_sig_test_{}_{}", tag, nanos));
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
TmpDir(dir)
|
||||||
|
}
|
||||||
|
fn join(&self, name: &str) -> PathBuf {
|
||||||
|
self.0.join(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for TmpDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_str(p: &PathBuf) -> String {
|
||||||
|
p.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fingerprint_missing_path_is_default() {
|
||||||
|
let tmp = TmpDir::new("missing");
|
||||||
|
let missing = path_str(&tmp.join("does_not_exist.bin"));
|
||||||
|
let sigs = fingerprint(&[missing]);
|
||||||
|
assert_eq!(sigs.len(), 1);
|
||||||
|
// A path that can't be stat'd -> default sig, which forces a rebuild.
|
||||||
|
assert_eq!(sigs[0], FileSig::default());
|
||||||
|
assert_eq!(sigs[0].mtime, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fingerprint_detects_inplace_edit() {
|
||||||
|
let tmp = TmpDir::new("edit");
|
||||||
|
let file = tmp.join("a.bin");
|
||||||
|
fs::write(&file, b"small").unwrap();
|
||||||
|
let p = path_str(&file);
|
||||||
|
|
||||||
|
let before = fingerprint(&[p.clone()]);
|
||||||
|
// Same content, same path: fingerprint must be stable.
|
||||||
|
let again = fingerprint(&[p.clone()]);
|
||||||
|
assert_eq!(before, again);
|
||||||
|
assert_eq!(before[0].size, 5);
|
||||||
|
assert!(!before[0].is_dir);
|
||||||
|
|
||||||
|
// Edit in place so the file grows.
|
||||||
|
fs::write(&file, b"much larger contents than before").unwrap();
|
||||||
|
let after = fingerprint(&[p]);
|
||||||
|
assert_ne!(before, after);
|
||||||
|
assert!(after[0].size > before[0].size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fingerprint_flags_directory() {
|
||||||
|
let tmp = TmpDir::new("dir");
|
||||||
|
let sub = tmp.join("subdir");
|
||||||
|
fs::create_dir_all(&sub).unwrap();
|
||||||
|
let sigs = fingerprint(&[path_str(&sub)]);
|
||||||
|
assert_eq!(sigs.len(), 1);
|
||||||
|
assert!(sigs[0].is_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recopy_after_edit_refreshes_cached_size() {
|
||||||
|
let tmp = TmpDir::new("recopy");
|
||||||
|
let file = tmp.join("doc.bin");
|
||||||
|
fs::write(&file, b"v1").unwrap(); // 2 bytes
|
||||||
|
let files = vec![path_str(&file)];
|
||||||
|
|
||||||
|
// Drive the public, guarded `sync_files` over the global CLIP_FILES;
|
||||||
|
// reset first (this is the only test that touches the global).
|
||||||
|
clear_files();
|
||||||
|
|
||||||
|
sync_files(&files).unwrap();
|
||||||
|
{
|
||||||
|
let cache = CLIP_FILES.lock();
|
||||||
|
let idx = cache.first_file_index;
|
||||||
|
assert_eq!(cache.file_list[idx].size, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-place edit grows the file; the re-copy must rebuild. Pre-fix the
|
||||||
|
// path-only guard early-returned and left the cached size stale at 2.
|
||||||
|
fs::write(&file, b"v2 is bigger").unwrap(); // 12 bytes
|
||||||
|
sync_files(&files).unwrap();
|
||||||
|
{
|
||||||
|
let cache = CLIP_FILES.lock();
|
||||||
|
let idx = cache.first_file_index;
|
||||||
|
assert_eq!(cache.file_list[idx].size, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_files(); // leave the global clean for other tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,14 @@
|
|||||||
|
|
||||||
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
|
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
|
||||||
#define WF_CLIPRDR_MAX_STREAMS 16384
|
#define WF_CLIPRDR_MAX_STREAMS 16384
|
||||||
|
/* Registered clipboard formats use IDs 0xC000 through 0xFFFF.
|
||||||
|
* https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */
|
||||||
|
#define WF_CLIPRDR_MAX_FORMATS 0x4000u
|
||||||
|
/* Registered format names are string atoms; cap the converted WCHAR name.
|
||||||
|
* https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */
|
||||||
|
#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u
|
||||||
|
/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */
|
||||||
|
#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u)
|
||||||
|
|
||||||
/* Validates the remote descriptor array size after cItems has been read safely. */
|
/* 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)
|
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
|
||||||
@@ -61,6 +69,25 @@ static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
|
|||||||
return size >= descriptors_size;
|
return size >= descriptors_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if (!value || !len)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
for (i = 0; i <= max_len; i++)
|
||||||
|
{
|
||||||
|
if (value[i] == '\0')
|
||||||
|
{
|
||||||
|
*len = i;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clipboard Formats
|
* Clipboard Formats
|
||||||
*/
|
*/
|
||||||
@@ -205,6 +232,7 @@ struct _CliprdrStream
|
|||||||
FILEDESCRIPTORW m_Dsc;
|
FILEDESCRIPTORW m_Dsc;
|
||||||
void *m_pData;
|
void *m_pData;
|
||||||
UINT32 m_connID;
|
UINT32 m_connID;
|
||||||
|
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
|
||||||
};
|
};
|
||||||
typedef struct _CliprdrStream CliprdrStream;
|
typedef struct _CliprdrStream CliprdrStream;
|
||||||
|
|
||||||
@@ -258,6 +286,9 @@ struct wf_clipboard
|
|||||||
char *req_fdata;
|
char *req_fdata;
|
||||||
HANDLE req_fevent;
|
HANDLE req_fevent;
|
||||||
BOOL req_f_received;
|
BOOL req_f_received;
|
||||||
|
UINT32 req_f_conn_id_expected; // connID of the outstanding request
|
||||||
|
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
|
||||||
|
LONG req_f_stream_id_seq; // source of unique per-stream ids
|
||||||
|
|
||||||
size_t nFiles;
|
size_t nFiles;
|
||||||
size_t file_array_size;
|
size_t file_array_size;
|
||||||
@@ -288,7 +319,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
|
|||||||
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
|
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
|
||||||
static UINT cliprdr_send_lock(wfClipboard *clipboard);
|
static UINT cliprdr_send_lock(wfClipboard *clipboard);
|
||||||
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
|
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
|
||||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid,
|
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
|
||||||
ULONG index, UINT32 flag, DWORD positionhigh,
|
ULONG index, UINT32 flag, DWORD positionhigh,
|
||||||
DWORD positionlow, ULONG request);
|
DWORD positionlow, ULONG request);
|
||||||
|
|
||||||
@@ -371,7 +402,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
|
|||||||
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
|
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
|
||||||
ULONG *pcbRead)
|
ULONG *pcbRead)
|
||||||
{
|
{
|
||||||
int ret;
|
UINT ret;
|
||||||
CliprdrStream *instance = (CliprdrStream *)This;
|
CliprdrStream *instance = (CliprdrStream *)This;
|
||||||
wfClipboard *clipboard;
|
wfClipboard *clipboard;
|
||||||
|
|
||||||
@@ -384,12 +415,23 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
|||||||
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
|
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
|
||||||
return S_FALSE;
|
return S_FALSE;
|
||||||
|
|
||||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex,
|
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
|
||||||
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
|
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
|
||||||
instance->m_lOffset.LowPart, cb);
|
instance->m_lOffset.LowPart, cb);
|
||||||
|
|
||||||
if (ret < 0)
|
if (ret != CHANNEL_RC_OK)
|
||||||
|
{
|
||||||
|
free(clipboard->req_fdata);
|
||||||
|
clipboard->req_fdata = NULL;
|
||||||
return E_FAIL;
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clipboard->req_fsize > cb)
|
||||||
|
{
|
||||||
|
free(clipboard->req_fdata);
|
||||||
|
clipboard->req_fdata = NULL;
|
||||||
|
return STG_E_READFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
if (clipboard->req_fdata)
|
if (clipboard->req_fdata)
|
||||||
{
|
{
|
||||||
@@ -601,6 +643,7 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
|||||||
instance->m_pData = pData;
|
instance->m_pData = pData;
|
||||||
instance->m_lOffset.QuadPart = 0;
|
instance->m_lOffset.QuadPart = 0;
|
||||||
instance->m_connID = connID;
|
instance->m_connID = connID;
|
||||||
|
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
|
||||||
|
|
||||||
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
|
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
|
||||||
{
|
{
|
||||||
@@ -611,16 +654,28 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
|||||||
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
|
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
|
||||||
{
|
{
|
||||||
/* get content size of this stream */
|
/* get content size of this stream */
|
||||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance,
|
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
|
||||||
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
|
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
|
||||||
8) == CHANNEL_RC_OK)
|
8) == CHANNEL_RC_OK)
|
||||||
{
|
{
|
||||||
success = TRUE;
|
success = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clipboard->req_fdata != NULL)
|
if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
|
||||||
|
{
|
||||||
|
LONGLONG sz = 0;
|
||||||
|
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
|
||||||
|
if (sz < 0)
|
||||||
|
success = FALSE;
|
||||||
|
else
|
||||||
|
instance->m_lSize.QuadPart = sz;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
success = FALSE;
|
||||||
|
}
|
||||||
|
if (clipboard->req_fdata)
|
||||||
{
|
{
|
||||||
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
|
|
||||||
free(clipboard->req_fdata);
|
free(clipboard->req_fdata);
|
||||||
clipboard->req_fdata = NULL;
|
clipboard->req_fdata = NULL;
|
||||||
}
|
}
|
||||||
@@ -1406,25 +1461,35 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format)
|
|||||||
return local_format;
|
return local_format;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void map_ensure_capacity(wfClipboard *clipboard)
|
static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity)
|
||||||
{
|
{
|
||||||
if (!clipboard)
|
size_t old_size;
|
||||||
return;
|
|
||||||
|
|
||||||
if (clipboard->map_size >= clipboard->map_capacity)
|
|
||||||
{
|
|
||||||
size_t new_size;
|
|
||||||
formatMapping *new_map;
|
formatMapping *new_map;
|
||||||
new_size = clipboard->map_capacity * 2;
|
|
||||||
|
if (!clipboard)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (!clipboard->format_mappings)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (capacity <= clipboard->map_capacity)
|
||||||
|
return TRUE;
|
||||||
|
|
||||||
|
if (capacity > WF_CLIPRDR_MAX_FORMATS ||
|
||||||
|
capacity > ((size_t)-1) / sizeof(formatMapping))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
old_size = clipboard->map_capacity;
|
||||||
new_map =
|
new_map =
|
||||||
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size);
|
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity);
|
||||||
|
|
||||||
if (!new_map)
|
if (!new_map)
|
||||||
return;
|
return FALSE;
|
||||||
|
|
||||||
|
memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size));
|
||||||
clipboard->format_mappings = new_map;
|
clipboard->format_mappings = new_map;
|
||||||
clipboard->map_capacity = new_size;
|
clipboard->map_capacity = capacity;
|
||||||
}
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL clear_format_map(wfClipboard *clipboard)
|
static BOOL clear_format_map(wfClipboard *clipboard)
|
||||||
@@ -1451,6 +1516,13 @@ static BOOL clear_format_map(wfClipboard *clipboard)
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard)
|
||||||
|
{
|
||||||
|
clear_format_map(clipboard);
|
||||||
|
clipboard->copied = FALSE;
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
||||||
{
|
{
|
||||||
CLIPRDR_TEMP_DIRECTORY tempDirectory;
|
CLIPRDR_TEMP_DIRECTORY tempDirectory;
|
||||||
@@ -1729,12 +1801,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
|||||||
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
||||||
}
|
}
|
||||||
|
|
||||||
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
|
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
|
||||||
UINT32 flag, DWORD positionhigh, DWORD positionlow,
|
UINT32 flag, DWORD positionhigh, DWORD positionlow,
|
||||||
ULONG nreq)
|
ULONG nreq)
|
||||||
{
|
{
|
||||||
UINT rc;
|
UINT rc;
|
||||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest;
|
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
|
||||||
|
|
||||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||||
return ERROR_INTERNAL_ERROR;
|
return ERROR_INTERNAL_ERROR;
|
||||||
@@ -1745,12 +1817,11 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
clipboard->req_f_received = FALSE;
|
clipboard->req_f_received = FALSE;
|
||||||
|
clipboard->req_f_conn_id_expected = connID;
|
||||||
|
clipboard->req_f_stream_id_expected = streamId;
|
||||||
|
|
||||||
fileContentsRequest.connID = connID;
|
fileContentsRequest.connID = connID;
|
||||||
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
|
fileContentsRequest.streamId = streamId;
|
||||||
// But it is OK, because it is only used to check if the stream is the same in
|
|
||||||
// `wf_cliprdr_server_file_contents_request()` function.
|
|
||||||
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
|
|
||||||
fileContentsRequest.listIndex = index;
|
fileContentsRequest.listIndex = index;
|
||||||
fileContentsRequest.dwFlags = flag;
|
fileContentsRequest.dwFlags = flag;
|
||||||
fileContentsRequest.nPositionLow = positionlow;
|
fileContentsRequest.nPositionLow = positionlow;
|
||||||
@@ -2443,6 +2514,16 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
|
|
||||||
if (!clear_format_map(clipboard))
|
if (!clear_format_map(clipboard))
|
||||||
return ERROR_INTERNAL_ERROR;
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
clipboard->copied = FALSE;
|
||||||
|
|
||||||
|
if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS)
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
|
||||||
|
if (formatList->numFormats > 0 && !formatList->formats)
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
|
||||||
|
if (!map_ensure_capacity(clipboard, formatList->numFormats))
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
|
||||||
clipboard->copied = TRUE;
|
clipboard->copied = TRUE;
|
||||||
|
|
||||||
@@ -2450,19 +2531,58 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
{
|
{
|
||||||
format = &(formatList->formats[i]);
|
format = &(formatList->formats[i]);
|
||||||
mapping = &(clipboard->format_mappings[i]);
|
mapping = &(clipboard->format_mappings[i]);
|
||||||
|
/* Do not validate the peer-provided formatId as a Windows registered format.
|
||||||
|
* It is only a remote protocol ID used when requesting data from the peer.
|
||||||
|
* For named formats, RegisterClipboardFormatW creates the local Windows
|
||||||
|
* clipboard ID below, and that local ID is checked before publishing. */
|
||||||
mapping->remote_format_id = format->formatId;
|
mapping->remote_format_id = format->formatId;
|
||||||
|
|
||||||
if (format->formatName)
|
if (format->formatName)
|
||||||
{
|
{
|
||||||
int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName,
|
size_t name_len;
|
||||||
strlen(format->formatName), NULL, 0);
|
int size;
|
||||||
mapping->name = calloc(size + 1, sizeof(WCHAR));
|
|
||||||
|
|
||||||
if (mapping->name)
|
if (!wf_cliprdr_bounded_strlen(format->formatName,
|
||||||
|
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
|
||||||
{
|
{
|
||||||
MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName),
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
mapping->name, size);
|
}
|
||||||
|
|
||||||
|
if (name_len == 0)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||||
|
NULL, 0);
|
||||||
|
if (size <= 0)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping->name = calloc((size_t)size + 1, sizeof(WCHAR));
|
||||||
|
if (!mapping->name)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||||
|
mapping->name, size) != size)
|
||||||
|
{
|
||||||
|
free(mapping->name);
|
||||||
|
mapping->name = NULL;
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
||||||
|
if (mapping->local_format_id == 0)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2472,7 +2592,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
clipboard->map_size++;
|
clipboard->map_size++;
|
||||||
map_ensure_capacity(clipboard);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_transferring(clipboard))
|
if (file_transferring(clipboard))
|
||||||
@@ -2923,6 +3042,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
|||||||
BOOL bIsStreamFile = TRUE;
|
BOOL bIsStreamFile = TRUE;
|
||||||
static LPSTREAM pStreamStc = NULL;
|
static LPSTREAM pStreamStc = NULL;
|
||||||
static UINT32 uStreamIdStc = 0;
|
static UINT32 uStreamIdStc = 0;
|
||||||
|
static UINT32 uConnIdStc = 0;
|
||||||
wfClipboard *clipboard;
|
wfClipboard *clipboard;
|
||||||
UINT rc = ERROR_INTERNAL_ERROR;
|
UINT rc = ERROR_INTERNAL_ERROR;
|
||||||
UINT sRc;
|
UINT sRc;
|
||||||
@@ -2996,7 +3116,8 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
|||||||
vFormatEtc.lindex = fileContentsRequest->listIndex;
|
vFormatEtc.lindex = fileContentsRequest->listIndex;
|
||||||
vFormatEtc.ptd = NULL;
|
vFormatEtc.ptd = NULL;
|
||||||
|
|
||||||
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc)
|
if ((uStreamIdStc != fileContentsRequest->streamId) ||
|
||||||
|
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
|
||||||
{
|
{
|
||||||
LPENUMFORMATETC pEnumFormatEtc;
|
LPENUMFORMATETC pEnumFormatEtc;
|
||||||
ULONG CeltFetched;
|
ULONG CeltFetched;
|
||||||
@@ -3027,6 +3148,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
|||||||
{
|
{
|
||||||
pStreamStc = vStgMedium.pstm;
|
pStreamStc = vStgMedium.pstm;
|
||||||
uStreamIdStc = fileContentsRequest->streamId;
|
uStreamIdStc = fileContentsRequest->streamId;
|
||||||
|
uConnIdStc = fileContentsRequest->connID;
|
||||||
bIsStreamFile = TRUE;
|
bIsStreamFile = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3190,6 +3312,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
|||||||
rc = ERROR_INTERNAL_ERROR;
|
rc = ERROR_INTERNAL_ERROR;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
|
||||||
|
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
|
||||||
|
return CHANNEL_RC_OK;
|
||||||
clipboard->req_fsize = 0;
|
clipboard->req_fsize = 0;
|
||||||
clipboard->req_fdata = NULL;
|
clipboard->req_fdata = NULL;
|
||||||
|
|
||||||
@@ -3200,6 +3325,13 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
clipboard->req_fsize = fileContentsResponse->cbRequested;
|
clipboard->req_fsize = fileContentsResponse->cbRequested;
|
||||||
|
/*
|
||||||
|
* Keep the zero-size allocation: supported Windows builds use the Microsoft
|
||||||
|
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
|
||||||
|
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
|
||||||
|
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
|
||||||
|
* nonzero length cannot have a NULL data pointer on the normal call path.
|
||||||
|
*/
|
||||||
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
|
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
|
||||||
if (!clipboard->req_fdata)
|
if (!clipboard->req_fdata)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ pub enum MouseButton {
|
|||||||
|
|
||||||
/// Scroll up button
|
/// Scroll up button
|
||||||
ScrollUp,
|
ScrollUp,
|
||||||
/// Left right button
|
/// Scroll down button
|
||||||
ScrollDown,
|
ScrollDown,
|
||||||
/// Left right button
|
/// Scroll left button
|
||||||
ScrollLeft,
|
ScrollLeft,
|
||||||
/// Left right button
|
/// Scroll right button
|
||||||
ScrollRight,
|
ScrollRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ impl KeyboardControllable for Enigo {
|
|||||||
// Windows uses uft-16 encoding. We need to check
|
// Windows uses uft-16 encoding. We need to check
|
||||||
// for variable length characters. As such some
|
// for variable length characters. As such some
|
||||||
// characters can be 32 bit long and those are
|
// characters can be 32 bit long and those are
|
||||||
// encoded in such called hight and low surrogates
|
// encoded in so-called high and low surrogates
|
||||||
// each 16 bit wide that needs to be send after
|
// each 16 bit wide that needs to be send after
|
||||||
// another to the SendInput function without
|
// another to the SendInput function without
|
||||||
// being interrupted by "keyup"
|
// being interrupted by "keyup"
|
||||||
|
|||||||
Submodule libs/hbb_common updated: 387603f47c...7e1c392c62
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.7"
|
version = "1.4.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ windows = { version = "0.61", features = [
|
|||||||
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
||||||
|
|
||||||
[package.metadata.winres]
|
[package.metadata.winres]
|
||||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||||
ProductName = "RustDesk"
|
ProductName = "RustDesk"
|
||||||
OriginalFilename = "rustdesk.exe"
|
OriginalFilename = "rustdesk.exe"
|
||||||
FileDescription = "RustDesk Remote Desktop"
|
FileDescription = "RustDesk Remote Desktop"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf {
|
|||||||
format!("{}-{}", target_arch, target_os)
|
format!("{}-{}", target_arch, target_os)
|
||||||
}
|
}
|
||||||
} else if target_os == "windows" {
|
} else if target_os == "windows" {
|
||||||
"x64-windows-static".to_owned()
|
format!("{}-windows-static", target_arch)
|
||||||
} else {
|
} else {
|
||||||
format!("{}-{}", target_arch, target_os)
|
format!("{}-{}", target_arch, target_os)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ mod webrtc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tile_log2(threads: u32) -> std::os::raw::c_uint {
|
||||||
|
(threads as f64).log2().ceil() as _
|
||||||
|
}
|
||||||
|
|
||||||
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
|
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
|
||||||
use aom_superblock_size::*;
|
use aom_superblock_size::*;
|
||||||
let resolution = width * height;
|
let resolution = width * height;
|
||||||
@@ -160,8 +164,7 @@ mod webrtc {
|
|||||||
} else {
|
} else {
|
||||||
AV1E_SET_TILE_COLUMNS
|
AV1E_SET_TILE_COLUMNS
|
||||||
};
|
};
|
||||||
// Failed on android
|
call_ctl!(ctx, tile_set, tile_log2(cfg.g_threads));
|
||||||
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
|
|
||||||
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
|
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
|
||||||
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
|
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
|
||||||
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
|
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
|
||||||
@@ -197,6 +200,23 @@ mod webrtc {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::os::raw::c_uint;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_log2_uses_c_uint_and_rounds_up() {
|
||||||
|
let one_thread: c_uint = tile_log2(1);
|
||||||
|
let three_threads: c_uint = tile_log2(3);
|
||||||
|
let max_threads: c_uint = tile_log2(64);
|
||||||
|
|
||||||
|
assert_eq!(one_thread, 0);
|
||||||
|
assert_eq!(three_threads, 2);
|
||||||
|
assert_eq!(max_threads, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncoderApi for AomEncoder {
|
impl EncoderApi for AomEncoder {
|
||||||
|
|||||||
@@ -52,6 +52,33 @@ lazy_static::lazy_static! {
|
|||||||
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
|
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 REFWICPixelFormatGUID = *const GUID;
|
||||||
pub type WICPixelFormatGUID = GUID;
|
pub type WICPixelFormatGUID = GUID;
|
||||||
|
|
||||||
@@ -247,6 +274,8 @@ pub struct CapturerMag {
|
|||||||
rect: RECT,
|
rect: RECT,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
|
excluded_window_target: Option<(String, String)>,
|
||||||
|
excluded_windows: Vec<HWND>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for CapturerMag {
|
impl Drop for CapturerMag {
|
||||||
@@ -261,6 +290,10 @@ impl CapturerMag {
|
|||||||
MagInterface::new().is_ok()
|
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> {
|
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||||
@@ -305,6 +338,8 @@ impl CapturerMag {
|
|||||||
},
|
},
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
excluded_window_target: None,
|
||||||
|
excluded_windows: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -436,19 +471,41 @@ impl CapturerMag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
||||||
let name_c = CString::new(name)?;
|
let mut hwnds = find_windows(cls, name)?;
|
||||||
unsafe {
|
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
|
||||||
let mut hwnd = if cls.len() == 0 {
|
self.excluded_window_target = Some((cls.to_owned(), name.to_owned()));
|
||||||
FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr())
|
if hwnds.is_empty() {
|
||||||
} else {
|
self.excluded_windows.clear();
|
||||||
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);
|
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 _;
|
||||||
|
unsafe {
|
||||||
if let Some(set_window_filter_list_func) =
|
if let Some(set_window_filter_list_func) =
|
||||||
self.mag_interface.set_window_filter_list_func
|
self.mag_interface.set_window_filter_list_func
|
||||||
{
|
{
|
||||||
@@ -456,16 +513,15 @@ impl CapturerMag {
|
|||||||
== set_window_filter_list_func(
|
== set_window_filter_list_func(
|
||||||
self.magnifier_window,
|
self.magnifier_window,
|
||||||
MW_FILTERMODE_EXCLUDE,
|
MW_FILTERMODE_EXCLUDE,
|
||||||
1,
|
count,
|
||||||
&mut hwnd,
|
hwnds.as_mut_ptr(),
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ErrorKind::Other,
|
ErrorKind::Other,
|
||||||
format!(
|
format!(
|
||||||
"Failed MagSetWindowFilterList for cls {} name {}, error {}",
|
"Failed MagSetWindowFilterList for {} windows, error {}",
|
||||||
cls,
|
count,
|
||||||
name,
|
|
||||||
Error::last_os_error()
|
Error::last_os_error()
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -496,6 +552,7 @@ impl CapturerMag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn frame(&mut self, data: &mut Vec<u8>) -> Result<()> {
|
pub(crate) fn frame(&mut self, data: &mut Vec<u8>) -> Result<()> {
|
||||||
|
self.refresh_excluded_windows()?;
|
||||||
Self::clear_data();
|
Self::clear_data();
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.4.7
|
pkgver=1.4.9
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#! /usr/bin/env bash
|
#! /usr/bin/env bash
|
||||||
sed -i "s/$1/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
|
sed -i "s/\b$1\b/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
|
||||||
cargo run # to bump version in cargo lock
|
cargo run # to bump version in cargo lock
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
<Configuration>Release</Configuration>
|
<Configuration>Release</Configuration>
|
||||||
<Platform>x64</Platform>
|
<Platform>x64</Platform>
|
||||||
</ProjectConfiguration>
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|ARM64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>ARM64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Globals">
|
<PropertyGroup Label="Globals">
|
||||||
<Keyword>Win32Proj</Keyword>
|
<Keyword>Win32Proj</Keyword>
|
||||||
@@ -22,6 +26,12 @@
|
|||||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||||
<CharacterSet>Unicode</CharacterSet>
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
</PropertyGroup>
|
</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" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||||
<ImportGroup Label="ExtensionSettings">
|
<ImportGroup Label="ExtensionSettings">
|
||||||
</ImportGroup>
|
</ImportGroup>
|
||||||
@@ -30,6 +40,9 @@
|
|||||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
<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" />
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||||
</ImportGroup>
|
</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" />
|
<PropertyGroup Label="UserMacros" />
|
||||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
<ClCompile>
|
<ClCompile>
|
||||||
@@ -53,6 +66,28 @@
|
|||||||
<ModuleDefinitionFile>CustomActions.def</ModuleDefinitionFile>
|
<ModuleDefinitionFile>CustomActions.def</ModuleDefinitionFile>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</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>
|
<ItemGroup>
|
||||||
<ClInclude Include="Common.h" />
|
<ClInclude Include="Common.h" />
|
||||||
<ClInclude Include="framework.h" />
|
<ClInclude Include="framework.h" />
|
||||||
@@ -65,6 +100,7 @@
|
|||||||
<ClCompile Include="FirewallRules.cpp" />
|
<ClCompile Include="FirewallRules.cpp" />
|
||||||
<ClCompile Include="pch.cpp">
|
<ClCompile Include="pch.cpp">
|
||||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
|
||||||
|
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="ReadConfig.cpp" />
|
<ClCompile Include="ReadConfig.cpp" />
|
||||||
<ClCompile Include="RemotePrinter.cpp" />
|
<ClCompile Include="RemotePrinter.cpp" />
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\li
|
|||||||
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
|
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
|
||||||
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
|
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
|
||||||
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
||||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
|
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Tech Pte. Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
|
||||||
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
|
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
|
||||||
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
|
||||||
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
|
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<IncludeSearchPaths>
|
<IncludeSearchPaths>
|
||||||
</IncludeSearchPaths>
|
</IncludeSearchPaths>
|
||||||
<Configurations>Release</Configurations>
|
<Configurations>Release</Configurations>
|
||||||
<Platforms>x64</Platforms>
|
<Platforms>x64;ARM64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Includes.wxi" />
|
<Content Include="Includes.wxi" />
|
||||||
|
|||||||
@@ -10,12 +10,17 @@ EndProject
|
|||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Release|x64 = Release|x64
|
Release|x64 = Release|x64
|
||||||
|
Release|ARM64 = Release|ARM64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.ActiveCfg = Release|x64
|
{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|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.ActiveCfg = Release|x64
|
||||||
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def make_parser():
|
|||||||
"-m",
|
"-m",
|
||||||
"--manufacturer",
|
"--manufacturer",
|
||||||
type=str,
|
type=str,
|
||||||
default="PURSLANE",
|
default="Purslane Tech Pte. Ltd.",
|
||||||
help="The app manufacturer.",
|
help="The app manufacturer.",
|
||||||
)
|
)
|
||||||
return parser
|
return parser
|
||||||
@@ -499,7 +499,7 @@ def update_license_file(app_name):
|
|||||||
license_content = f.read()
|
license_content = f.read()
|
||||||
license_content = license_content.replace("website rustdesk.com and other ", "")
|
license_content = license_content.replace("website rustdesk.com and other ", "")
|
||||||
license_content = license_content.replace("RustDesk", app_name)
|
license_content = license_content.replace("RustDesk", app_name)
|
||||||
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
|
license_content = re.sub(r"Purslane(?: Tech Pte\.)? Ltd", app_name, license_content, flags=re.IGNORECASE)
|
||||||
with open(license_file, "w", encoding="utf-8") as f:
|
with open(license_file, "w", encoding="utf-8") as f:
|
||||||
f.write(license_content)
|
f.write(license_content)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.7
|
Version: 1.4.9
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.7
|
Version: 1.4.9
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.7
|
Version: 1.4.9
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ pre_start()
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# When loging out from the interactive shell, the execution sequence is:
|
# When logging out from the interactive shell, the execution sequence is:
|
||||||
#
|
#
|
||||||
# IF ~/.bash_logout exists THEN
|
# IF ~/.bash_logout exists THEN
|
||||||
# execute ~/.bash_logout
|
# execute ~/.bash_logout
|
||||||
|
|||||||
7
res/vcpkg-triplets/arm-neon-android.cmake
Normal file
7
res/vcpkg-triplets/arm-neon-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
set(VCPKG_TARGET_ARCHITECTURE arm)
|
||||||
|
set(VCPKG_CRT_LINKAGE dynamic)
|
||||||
|
set(VCPKG_LIBRARY_LINKAGE static)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||||
|
set(VCPKG_MAKE_BUILD_TRIPLET "--host=armv7a-linux-androideabi")
|
||||||
|
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=armeabi-v7a -DANDROID_ARM_NEON=ON)
|
||||||
7
res/vcpkg-triplets/arm64-android.cmake
Normal file
7
res/vcpkg-triplets/arm64-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
set(VCPKG_TARGET_ARCHITECTURE arm64)
|
||||||
|
set(VCPKG_CRT_LINKAGE dynamic)
|
||||||
|
set(VCPKG_LIBRARY_LINKAGE static)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||||
|
set(VCPKG_MAKE_BUILD_TRIPLET "--host=aarch64-linux-android")
|
||||||
|
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=arm64-v8a)
|
||||||
7
res/vcpkg-triplets/x64-android.cmake
Normal file
7
res/vcpkg-triplets/x64-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
set(VCPKG_TARGET_ARCHITECTURE x64)
|
||||||
|
set(VCPKG_CRT_LINKAGE dynamic)
|
||||||
|
set(VCPKG_LIBRARY_LINKAGE static)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||||
|
set(VCPKG_MAKE_BUILD_TRIPLET "--host=x86_64-linux-android")
|
||||||
|
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86_64)
|
||||||
7
res/vcpkg-triplets/x86-android.cmake
Normal file
7
res/vcpkg-triplets/x86-android.cmake
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
set(VCPKG_TARGET_ARCHITECTURE x86)
|
||||||
|
set(VCPKG_CRT_LINKAGE dynamic)
|
||||||
|
set(VCPKG_LIBRARY_LINKAGE static)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_NAME Android)
|
||||||
|
set(VCPKG_CMAKE_SYSTEM_VERSION 21)
|
||||||
|
set(VCPKG_MAKE_BUILD_TRIPLET "--host=i686-linux-android")
|
||||||
|
set(VCPKG_CMAKE_CONFIGURE_OPTIONS -DANDROID_ABI=x86)
|
||||||
@@ -130,14 +130,18 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
|
|||||||
--cc=cl \
|
--cc=cl \
|
||||||
--enable-gpl \
|
--enable-gpl \
|
||||||
--enable-d3d11va \
|
--enable-d3d11va \
|
||||||
--enable-cuda \
|
|
||||||
--enable-ffnvcodec \
|
|
||||||
--enable-hwaccel=h264_nvdec \
|
|
||||||
--enable-hwaccel=hevc_nvdec \
|
|
||||||
--enable-hwaccel=h264_d3d11va \
|
--enable-hwaccel=h264_d3d11va \
|
||||||
--enable-hwaccel=hevc_d3d11va \
|
--enable-hwaccel=hevc_d3d11va \
|
||||||
--enable-hwaccel=h264_d3d11va2 \
|
--enable-hwaccel=h264_d3d11va2 \
|
||||||
--enable-hwaccel=hevc_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-amf \
|
--enable-amf \
|
||||||
--enable-encoder=h264_amf \
|
--enable-encoder=h264_amf \
|
||||||
--enable-encoder=hevc_amf \
|
--enable-encoder=hevc_amf \
|
||||||
@@ -147,6 +151,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
|
|||||||
--enable-encoder=h264_qsv \
|
--enable-encoder=h264_qsv \
|
||||||
--enable-encoder=hevc_qsv \
|
--enable-encoder=hevc_qsv \
|
||||||
")
|
")
|
||||||
|
endif()
|
||||||
|
|
||||||
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
|
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
|
||||||
set(LIB_MACHINE_ARG /machine:x86)
|
set(LIB_MACHINE_ARG /machine:x86)
|
||||||
@@ -154,6 +159,9 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
|
|||||||
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
|
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
|
||||||
set(LIB_MACHINE_ARG /machine:x64)
|
set(LIB_MACHINE_ARG /machine:x64)
|
||||||
string(APPEND OPTIONS " --arch=x86_64")
|
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()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported target architecture")
|
message(FATAL_ERROR "Unsupported target architecture")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
199
src/cli.rs
199
src/cli.rs
@@ -1,199 +0,0 @@
|
|||||||
use crate::client::*;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use hbb_common::{
|
|
||||||
config::PeerConfig,
|
|
||||||
config::READ_TIMEOUT,
|
|
||||||
futures::{SinkExt, StreamExt},
|
|
||||||
log,
|
|
||||||
message_proto::*,
|
|
||||||
protobuf::Message as _,
|
|
||||||
rendezvous_proto::ConnType,
|
|
||||||
tokio::{self, sync::mpsc},
|
|
||||||
Stream,
|
|
||||||
};
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Session {
|
|
||||||
id: String,
|
|
||||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
|
||||||
sender: mpsc::UnboundedSender<Data>,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let session = Self {
|
|
||||||
id: id.to_owned(),
|
|
||||||
sender,
|
|
||||||
password,
|
|
||||||
lc: Default::default(),
|
|
||||||
};
|
|
||||||
session.lc.write().unwrap().initialize(
|
|
||||||
id.to_owned(),
|
|
||||||
ConnType::PORT_FORWARD,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Interface for Session {
|
|
||||||
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
|
||||||
return self.lc.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) {
|
|
||||||
match msgtype {
|
|
||||||
"input-password" => {
|
|
||||||
self.sender
|
|
||||||
.send(Data::Login((self.password.clone(), true)))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
"re-input-password" => {
|
|
||||||
log::error!("{}: {}", title, text);
|
|
||||||
match rpassword::prompt_password("Enter password: ") {
|
|
||||||
Ok(password) => {
|
|
||||||
let login_data = Data::Login((password, true));
|
|
||||||
self.sender.send(login_data).ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("reinput password failed, {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msg if msg.contains("error") => {
|
|
||||||
log::error!("{}: {}: {}", msgtype, title, text);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::info!("{}: {}: {}", msgtype, title, text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_login_error(&self, err: &str) -> bool {
|
|
||||||
handle_login_error(self.lc.clone(), err, self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_peer_info(&self, pi: PeerInfo) {
|
|
||||||
self.lc.write().unwrap().handle_peer_info(&pi);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
|
|
||||||
log::info!(
|
|
||||||
"password={}",
|
|
||||||
hbb_common::password_security::temporary_password()
|
|
||||||
);
|
|
||||||
handle_hash(self.lc.clone(), &pass, hash, self, peer).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_login_from_ui(
|
|
||||||
&self,
|
|
||||||
os_username: String,
|
|
||||||
os_password: String,
|
|
||||||
password: String,
|
|
||||||
remember: bool,
|
|
||||||
peer: &mut Stream,
|
|
||||||
) {
|
|
||||||
handle_login_from_ui(
|
|
||||||
self.lc.clone(),
|
|
||||||
os_username,
|
|
||||||
os_password,
|
|
||||||
password,
|
|
||||||
remember,
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream) {
|
|
||||||
handle_test_delay(t, peer).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, data: Data) {
|
|
||||||
self.sender.send(data).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
pub async fn connect_test(id: &str, key: String, token: String) {
|
|
||||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
|
||||||
let handler = Session::new(&id, sender);
|
|
||||||
match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await {
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Failed to connect {}: {}", &id, err);
|
|
||||||
}
|
|
||||||
Ok((mut stream, direct)) => {
|
|
||||||
log::info!("direct: {}", direct);
|
|
||||||
// rpassword::prompt_password("Input anything to exit").ok();
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res {
|
|
||||||
Err(_) => {
|
|
||||||
log::error!("Timeout");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(Some(Ok(bytes))) => {
|
|
||||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
|
||||||
match msg_in.union {
|
|
||||||
Some(message::Union::Hash(hash)) => {
|
|
||||||
log::info!("Got hash");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
pub async fn start_one_port_forward(
|
|
||||||
id: String,
|
|
||||||
port: i32,
|
|
||||||
remote_host: String,
|
|
||||||
remote_port: i32,
|
|
||||||
key: String,
|
|
||||||
token: String,
|
|
||||||
) {
|
|
||||||
crate::common::test_rendezvous_server();
|
|
||||||
crate::common::test_nat_type();
|
|
||||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
|
||||||
let handler = Session::new(&id, sender);
|
|
||||||
if let Err(err) = crate::port_forward::listen(
|
|
||||||
handler.id.clone(),
|
|
||||||
handler.password.clone(),
|
|
||||||
port,
|
|
||||||
handler.clone(),
|
|
||||||
receiver,
|
|
||||||
&key,
|
|
||||||
&token,
|
|
||||||
handler.lc.clone(),
|
|
||||||
remote_host,
|
|
||||||
remote_port,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!("Failed to listen on {}: {}", port, err);
|
|
||||||
}
|
|
||||||
log::info!("port forward (:{}) exit", port);
|
|
||||||
}
|
|
||||||
@@ -96,6 +96,8 @@ pub mod screenshot;
|
|||||||
|
|
||||||
pub const MILLI1: Duration = Duration::from_millis(1);
|
pub const MILLI1: Duration = Duration::from_millis(1);
|
||||||
pub const SEC30: Duration = Duration::from_secs(30);
|
pub const SEC30: Duration = Duration::from_secs(30);
|
||||||
|
// Empirical restart reconnect grace window.
|
||||||
|
const RESTART_REMOTE_DEVICE_GRACE: Duration = Duration::from_secs(5 * 60);
|
||||||
pub const VIDEO_QUEUE_SIZE: usize = 120;
|
pub const VIDEO_QUEUE_SIZE: usize = 120;
|
||||||
const MAX_DECODE_FAIL_COUNTER: usize = 3;
|
const MAX_DECODE_FAIL_COUNTER: usize = 3;
|
||||||
|
|
||||||
@@ -939,21 +941,23 @@ impl Client {
|
|||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
fn try_stop_clipboard() {
|
fn try_stop_clipboard() {
|
||||||
// There's a bug here.
|
// Disconnected Flutter sessions may keep UI handlers alive, so only connected sessions
|
||||||
// If session is closed by the peer, `has_sessions_running()` will always return true.
|
// should block clipboard cleanup.
|
||||||
// It's better to check if the active session number.
|
|
||||||
// But it's not a problem, because the clipboard thread does not consume CPU.
|
|
||||||
//
|
|
||||||
// If we want to fix it, we can add a flag to indicate if session is active.
|
|
||||||
// But I think it's not necessary to introduce complexity at this point.
|
|
||||||
#[cfg(feature = "flutter")]
|
#[cfg(feature = "flutter")]
|
||||||
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
|
if crate::flutter::sessions::has_connected_sessions_running(ConnType::DEFAULT_CONN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
|
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
|
||||||
CLIPBOARD_STATE.lock().unwrap().running = false;
|
CLIPBOARD_STATE.lock().unwrap().running = false;
|
||||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||||
|
if let Err(e) = crate::clipboard::try_empty_clipboard_files_sync(
|
||||||
|
crate::clipboard::ClipboardSide::Client,
|
||||||
|
0,
|
||||||
|
) {
|
||||||
|
log::error!("Failed to empty client clipboard files: {}", e);
|
||||||
|
}
|
||||||
|
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
|
||||||
clipboard::platform::unix::fuse::uninit_fuse_context(true);
|
clipboard::platform::unix::fuse::uninit_fuse_context(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1740,7 +1744,10 @@ pub struct LoginConfigHandler {
|
|||||||
features: Option<Features>,
|
features: Option<Features>,
|
||||||
pub session_id: u64, // used for local <-> server communication
|
pub session_id: u64, // used for local <-> server communication
|
||||||
pub supported_encoding: SupportedEncoding,
|
pub supported_encoding: SupportedEncoding,
|
||||||
pub restarting_remote_device: bool,
|
restarting_remote_device: bool,
|
||||||
|
// Start time of the restart grace window. On Windows the peer may briefly
|
||||||
|
// reconnect before the real reboot disconnect.
|
||||||
|
restart_remote_device_at: Option<Instant>,
|
||||||
pub force_relay: bool,
|
pub force_relay: bool,
|
||||||
pub direct: Option<bool>,
|
pub direct: Option<bool>,
|
||||||
pub received: bool,
|
pub received: bool,
|
||||||
@@ -1849,7 +1856,7 @@ impl LoginConfigHandler {
|
|||||||
}
|
}
|
||||||
self.session_id = sid;
|
self.session_id = sid;
|
||||||
self.supported_encoding = Default::default();
|
self.supported_encoding = Default::default();
|
||||||
self.restarting_remote_device = false;
|
self.clear_restarting_remote_device();
|
||||||
self.force_relay =
|
self.force_relay =
|
||||||
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
config::option2bool("force-always-relay", &self.get_option("force-always-relay"))
|
||||||
|| force_relay
|
|| force_relay
|
||||||
@@ -2779,6 +2786,30 @@ impl LoginConfigHandler {
|
|||||||
msg_out
|
msg_out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn mark_restarting_remote_device(&mut self) {
|
||||||
|
self.restarting_remote_device = true;
|
||||||
|
self.restart_remote_device_at = Some(Instant::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_restarting_remote_device(&mut self) {
|
||||||
|
self.restarting_remote_device = false;
|
||||||
|
self.restart_remote_device_at = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_restarting_remote_device(&self) -> bool {
|
||||||
|
if !self.restarting_remote_device {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Keep this flag alive for a short grace window instead of clearing it on
|
||||||
|
// connection_ready or the first peer bytes. During OS restart the peer can
|
||||||
|
// briefly reconnect before the real reboot disconnect, and clearing it too
|
||||||
|
// early would let the next disconnect escape the restart flow and fall back
|
||||||
|
// to the normal error dialog / manual reconnect path.
|
||||||
|
self.restart_remote_device_at
|
||||||
|
.map(|started_at| started_at.elapsed() < RESTART_REMOTE_DEVICE_GRACE)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_conn_token(&self) -> Option<String> {
|
pub fn get_conn_token(&self) -> Option<String> {
|
||||||
if self.password.is_empty() {
|
if self.password.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -3718,9 +3749,18 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
|||||||
fn on_establish_connection_error(&self, err: String) {
|
fn on_establish_connection_error(&self, err: String) {
|
||||||
let title = "Connection Error";
|
let title = "Connection Error";
|
||||||
let text = err.to_string();
|
let text = err.to_string();
|
||||||
let lc = self.get_lch();
|
let lch = self.get_lch();
|
||||||
let direct = lc.read().unwrap().direct;
|
let (is_restarting, direct, received) = {
|
||||||
let received = lc.read().unwrap().received;
|
let lc = lch.read().unwrap();
|
||||||
|
(lc.is_restarting_remote_device(), lc.direct, lc.received)
|
||||||
|
};
|
||||||
|
if is_restarting {
|
||||||
|
log::info!("Restart remote device, suppress connection error: {err}");
|
||||||
|
// Flutter treats this as a reconnect control event. The text is kept
|
||||||
|
// for legacy UI and existing translation reuse.
|
||||||
|
self.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut relay_hint = false;
|
let mut relay_hint = false;
|
||||||
let mut relay_hint_type = "relay-hint";
|
let mut relay_hint_type = "relay-hint";
|
||||||
@@ -3752,6 +3792,7 @@ pub trait Interface: Send + Clone + 'static + Sized {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Data {
|
pub enum Data {
|
||||||
Close,
|
Close,
|
||||||
|
RejectInsecureConnection,
|
||||||
Login((String, String, String, bool)),
|
Login((String, String, String, bool)),
|
||||||
Message(Message),
|
Message(Message),
|
||||||
SendFiles((i32, JobType, String, String, i32, bool, bool)),
|
SendFiles((i32, JobType, String, String, i32, bool, bool)),
|
||||||
@@ -3775,11 +3816,33 @@ pub enum Data {
|
|||||||
ElevateWithLogon(String, String),
|
ElevateWithLogon(String, String),
|
||||||
NewVoiceCall,
|
NewVoiceCall,
|
||||||
CloseVoiceCall,
|
CloseVoiceCall,
|
||||||
|
ContinueInsecureConnection,
|
||||||
ResetDecoder(Option<usize>),
|
ResetDecoder(Option<usize>),
|
||||||
RenameFile((i32, String, String, bool)),
|
RenameFile((i32, String, String, bool)),
|
||||||
TakeScreenshot((i32, String)),
|
TakeScreenshot((i32, String)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn confirm_insecure_connection(
|
||||||
|
interface: &impl Interface,
|
||||||
|
receiver: &mut UnboundedReceiver<Data>,
|
||||||
|
) -> bool {
|
||||||
|
interface.msgbox(
|
||||||
|
"insecure-connection-nocancel-hasclose",
|
||||||
|
"Insecure Connection",
|
||||||
|
"conn-e2ee-unavailable-tip",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
while let Some(data) = receiver.recv().await {
|
||||||
|
match data {
|
||||||
|
Data::ContinueInsecureConnection => return true,
|
||||||
|
Data::RejectInsecureConnection => return false,
|
||||||
|
Data::Close => return false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Keycode for key events.
|
/// Keycode for key events.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ pub trait FileManager: Interface {
|
|||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
target_os = "ios",
|
target_os = "ios",
|
||||||
feature = "cli",
|
|
||||||
feature = "flutter"
|
feature = "flutter"
|
||||||
)))]
|
)))]
|
||||||
fn get_home_dir(&self) -> String {
|
fn get_home_dir(&self) -> String {
|
||||||
@@ -16,7 +15,6 @@ pub trait FileManager: Interface {
|
|||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
target_os = "ios",
|
target_os = "ios",
|
||||||
feature = "cli",
|
|
||||||
feature = "flutter"
|
feature = "flutter"
|
||||||
)))]
|
)))]
|
||||||
fn get_next_job_id(&self) -> i32 {
|
fn get_next_job_id(&self) -> i32 {
|
||||||
@@ -26,7 +24,6 @@ pub trait FileManager: Interface {
|
|||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
target_os = "ios",
|
target_os = "ios",
|
||||||
feature = "cli",
|
|
||||||
feature = "flutter"
|
feature = "flutter"
|
||||||
)))]
|
)))]
|
||||||
fn update_next_job_id(&self, id: i32) {
|
fn update_next_job_id(&self, id: i32) {
|
||||||
@@ -36,7 +33,6 @@ pub trait FileManager: Interface {
|
|||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
target_os = "ios",
|
target_os = "ios",
|
||||||
feature = "cli",
|
|
||||||
feature = "flutter"
|
feature = "flutter"
|
||||||
)))]
|
)))]
|
||||||
fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value {
|
fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value {
|
||||||
@@ -90,7 +86,6 @@ pub trait FileManager: Interface {
|
|||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
target_os = "ios",
|
target_os = "ios",
|
||||||
feature = "cli",
|
|
||||||
feature = "flutter"
|
feature = "flutter"
|
||||||
)))]
|
)))]
|
||||||
fn confirm_delete_files(&self, id: i32, file_num: i32) {
|
fn confirm_delete_files(&self, id: i32, file_num: i32) {
|
||||||
@@ -100,7 +95,6 @@ pub trait FileManager: Interface {
|
|||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
target_os = "ios",
|
target_os = "ios",
|
||||||
feature = "cli",
|
|
||||||
feature = "flutter"
|
feature = "flutter"
|
||||||
)))]
|
)))]
|
||||||
fn set_no_confirm(&self, id: i32) {
|
fn set_no_confirm(&self, id: i32) {
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ use crate::{
|
|||||||
common::get_default_sound_input,
|
common::get_default_sound_input,
|
||||||
ui_session_interface::{InvokeUiSession, Session},
|
ui_session_interface::{InvokeUiSession, Session},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Empirical no-data window before exposing the restart reconnect state to the UI.
|
||||||
|
// Restart msgbox text is kept as a legacy UI fallback; Flutter handles the type as a control event.
|
||||||
|
const RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
const KCP_CLOSE_REASON_FLUSH_DELAY: Duration = Duration::from_millis(30);
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip};
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
@@ -153,7 +158,6 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut last_recv_time = Instant::now();
|
|
||||||
let mut received = false;
|
let mut received = false;
|
||||||
let conn_type = if self.handler.is_file_transfer() {
|
let conn_type = if self.handler.is_file_transfer() {
|
||||||
ConnType::FILE_TRANSFER
|
ConnType::FILE_TRANSFER
|
||||||
@@ -180,8 +184,20 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_connected();
|
.set_connected();
|
||||||
|
let is_secured = peer.is_secured();
|
||||||
self.handler
|
self.handler
|
||||||
.set_connection_type(peer.is_secured(), direct, stream_type); // flutter -> connection_ready
|
.set_connection_type(is_secured, direct, stream_type); // flutter -> connection_ready
|
||||||
|
if !is_secured
|
||||||
|
&& !crate::common::is_direct_ip_access(&self.handler.get_id())
|
||||||
|
&& !client::confirm_insecure_connection(&self.handler, &mut self.receiver).await
|
||||||
|
{
|
||||||
|
self.send_close_reason(&mut peer, "").await;
|
||||||
|
if kcp.is_some() {
|
||||||
|
tokio::time::sleep(KCP_CLOSE_REASON_FLUSH_DELAY).await;
|
||||||
|
}
|
||||||
|
self.handle_disconnected(round);
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.handler.update_direct(Some(direct));
|
self.handler.update_direct(Some(direct));
|
||||||
if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA {
|
if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA {
|
||||||
self.handler
|
self.handler
|
||||||
@@ -219,6 +235,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
let mut fps_instant = Instant::now();
|
let mut fps_instant = Instant::now();
|
||||||
|
|
||||||
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
|
let _keep_it = client::hc_connection(feedback, rendezvous_server, token).await;
|
||||||
|
let mut last_recv_time = Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -244,7 +261,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
} else {
|
} else {
|
||||||
if self.handler.is_restarting_remote_device() {
|
if self.handler.is_restarting_remote_device() {
|
||||||
log::info!("Restart remote device");
|
log::info!("Restart remote device");
|
||||||
self.handler.msgbox("restarting", "Restarting remote device", "remote_restarting_tip", "");
|
self.handler.msgbox("restarting", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||||
} else {
|
} else {
|
||||||
log::info!("Reset by the peer");
|
log::info!("Reset by the peer");
|
||||||
self.handler.msgbox("error", "Connection Error", "Reset by the peer", "");
|
self.handler.msgbox("error", "Connection Error", "Reset by the peer", "");
|
||||||
@@ -279,6 +296,12 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = status_timer.tick() => {
|
_ = status_timer.tick() => {
|
||||||
|
if self.handler.is_restarting_remote_device()
|
||||||
|
&& last_recv_time.elapsed() >= RESTART_REMOTE_DEVICE_NO_DATA_TIMEOUT
|
||||||
|
{
|
||||||
|
self.handler.msgbox("restarting-show", "Restarting remote device", "Connection in progress. Please wait.", "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
let elapsed = fps_instant.elapsed().as_millis();
|
let elapsed = fps_instant.elapsed().as_millis();
|
||||||
if elapsed < 1000 {
|
if elapsed < 1000 {
|
||||||
continue;
|
continue;
|
||||||
@@ -328,13 +351,17 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
self.send_close_reason(&mut peer, "kcp").await;
|
self.send_close_reason(&mut peer, "kcp").await;
|
||||||
// KCP does not send messages immediately, so wait to ensure the last message is sent.
|
// KCP does not send messages immediately, so wait to ensure the last message is sent.
|
||||||
// 1ms works in my test, but 30ms is more reliable.
|
// 1ms works in my test, but 30ms is more reliable.
|
||||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
tokio::time::sleep(KCP_CLOSE_REASON_FLUSH_DELAY).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.handler.on_establish_connection_error(err.to_string());
|
self.handler.on_establish_connection_error(err.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.handle_disconnected(round);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_disconnected(&self, round: u32) {
|
||||||
// set_disconnected_ok is used to check if new connection round is started.
|
// set_disconnected_ok is used to check if new connection round is started.
|
||||||
let _set_disconnected_ok = self
|
let _set_disconnected_ok = self
|
||||||
.handler
|
.handler
|
||||||
@@ -350,6 +377,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
|
|
||||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||||
if self.handler.is_default() && _set_disconnected_ok {
|
if self.handler.is_default() && _set_disconnected_ok {
|
||||||
|
// Linux client cleanup runs synchronously in try_stop_clipboard() before FUSE is
|
||||||
|
// unmounted. Keep this async path for other file-clipboard platforms.
|
||||||
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
|
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1078,6 +1107,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
|
async fn send_toggle_virtual_display_msg(&self, peer: &mut Stream) {
|
||||||
|
if self.handler.is_view_camera() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if !self.peer_info.is_support_virtual_display() {
|
if !self.peer_info.is_support_virtual_display() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1099,6 +1131,9 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) {
|
async fn send_toggle_privacy_mode_msg(&self, peer: &mut Stream) {
|
||||||
|
if self.handler.is_view_camera() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let lc = self.handler.lc.read().unwrap();
|
let lc = self.handler.lc.read().unwrap();
|
||||||
if lc.version >= hbb_common::get_version_number("1.2.4")
|
if lc.version >= hbb_common::get_version_number("1.2.4")
|
||||||
&& lc.get_toggle_option("privacy-mode")
|
&& lc.get_toggle_option("privacy-mode")
|
||||||
|
|||||||
@@ -151,6 +151,14 @@ pub fn update_clipboard_files(files: Vec<String>, side: ClipboardSide) {
|
|||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
|
if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) {
|
||||||
|
log::error!("Failed to empty clipboard files: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
|
pub fn try_empty_clipboard_files_sync(_side: ClipboardSide, _conn_id: i32) -> ResultType<()> {
|
||||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||||
if ctx.is_none() {
|
if ctx.is_none() {
|
||||||
match ClipboardContext::new() {
|
match ClipboardContext::new() {
|
||||||
@@ -159,7 +167,7 @@ pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to create clipboard context: {}", e);
|
log::error!("Failed to create clipboard context: {}", e);
|
||||||
return;
|
bail!("Failed to create clipboard context: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,13 +185,14 @@ pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
|
|||||||
ctx.try_empty_clipboard_files(_side);
|
ctx.try_empty_clipboard_files(_side);
|
||||||
// No need to make sure the context is enabled.
|
// No need to make sure the context is enabled.
|
||||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||||
context.empty_clipboard(_conn_id).ok();
|
if !context.empty_clipboard(_conn_id)? {
|
||||||
|
bail!("Failed to empty clipboard files for conn_id {}", _conn_id);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -868,6 +877,7 @@ pub mod clipboard_listener {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(name.clone(), tx);
|
.insert(name.clone(), tx);
|
||||||
|
|
||||||
|
cleanup_stale_listener(&mut listener_lock);
|
||||||
if listener_lock.handle.is_none() {
|
if listener_lock.handle.is_none() {
|
||||||
log::info!("Start clipboard listener thread");
|
log::info!("Start clipboard listener thread");
|
||||||
let handler = Handler {
|
let handler = Handler {
|
||||||
@@ -893,6 +903,24 @@ pub mod clipboard_listener {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cleanup_stale_listener(listener: &mut ClipboardListener) {
|
||||||
|
if !listener
|
||||||
|
.handle
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, h)| h.is_finished())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some((shutdown, h)) = listener.handle.take() {
|
||||||
|
log::warn!("Cleaning up stale clipboard listener handle");
|
||||||
|
if let Err(e) = h.join() {
|
||||||
|
log::error!("Clipboard listener thread panicked during stale cleanup: {:?}", e);
|
||||||
|
}
|
||||||
|
drop(shutdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unsubscribe(name: &str) {
|
pub fn unsubscribe(name: &str) {
|
||||||
log::info!("Unsubscribe clipboard listener: {}", name);
|
log::info!("Unsubscribe clipboard listener: {}", name);
|
||||||
let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap();
|
let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap();
|
||||||
|
|||||||
@@ -332,12 +332,16 @@ pub mod unix_file_clip {
|
|||||||
log::debug!("format data response: msg_flags: {}", msg_flags);
|
log::debug!("format data response: msg_flags: {}", msg_flags);
|
||||||
|
|
||||||
if msg_flags != 0x1 {
|
if msg_flags != 0x1 {
|
||||||
// return failure message?
|
log::error!(
|
||||||
|
"peer reported clipboard format data failure: {}",
|
||||||
|
msg_flags
|
||||||
|
);
|
||||||
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("parsing file descriptors");
|
log::debug!("parsing file descriptors");
|
||||||
if fuse::init_fuse_context(true).is_ok() {
|
match fuse::init_fuse_context(side == ClipboardSide::Client) {
|
||||||
match fuse::format_data_response_to_urls(
|
Ok(()) => match fuse::format_data_response_to_urls(
|
||||||
side == ClipboardSide::Client,
|
side == ClipboardSide::Client,
|
||||||
format_data,
|
format_data,
|
||||||
conn_id,
|
conn_id,
|
||||||
@@ -348,9 +352,10 @@ pub mod unix_file_clip {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("failed to parse file descriptors: {:?}", e);
|
log::error!("failed to parse file descriptors: {:?}", e);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("failed to initialize clipboard FUSE context: {:?}", e);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// send error message to server
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClipboardFile::FileContentsRequest {
|
ClipboardFile::FileContentsRequest {
|
||||||
@@ -386,6 +391,7 @@ pub mod unix_file_clip {
|
|||||||
ClipboardFile::FileContentsResponse {
|
ClipboardFile::FileContentsResponse {
|
||||||
msg_flags,
|
msg_flags,
|
||||||
stream_id,
|
stream_id,
|
||||||
|
requested_data,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -393,13 +399,15 @@ pub mod unix_file_clip {
|
|||||||
msg_flags,
|
msg_flags,
|
||||||
stream_id,
|
stream_id,
|
||||||
);
|
);
|
||||||
if fuse::init_fuse_context(true).is_ok() {
|
let response = ClipboardFile::FileContentsResponse {
|
||||||
hbb_common::allow_err!(fuse::handle_file_content_response(
|
msg_flags,
|
||||||
side == ClipboardSide::Client,
|
stream_id,
|
||||||
clip
|
requested_data,
|
||||||
));
|
};
|
||||||
} else {
|
if let Err(e) =
|
||||||
// send error message to server
|
fuse::handle_file_content_response(side == ClipboardSide::Client, response)
|
||||||
|
{
|
||||||
|
log::error!("failed to handle file contents response: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClipboardFile::NotifyCallback {
|
ClipboardFile::NotifyCallback {
|
||||||
|
|||||||
@@ -764,15 +764,14 @@ async fn test_rendezvous_server_() {
|
|||||||
Config::reset_online();
|
Config::reset_online();
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
|
||||||
pub fn test_rendezvous_server() {
|
pub fn test_rendezvous_server() {
|
||||||
std::thread::spawn(test_rendezvous_server_);
|
std::thread::spawn(test_rendezvous_server_);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_rendezvous_server() {
|
pub fn refresh_rendezvous_server() {
|
||||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
test_rendezvous_server();
|
test_rendezvous_server();
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
if crate::ipc::test_rendezvous_server().is_err() {
|
if crate::ipc::test_rendezvous_server().is_err() {
|
||||||
test_rendezvous_server();
|
test_rendezvous_server();
|
||||||
@@ -2620,6 +2619,10 @@ pub fn get_control_permission(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_direct_ip_access(peer: &str) -> bool {
|
||||||
|
hbb_common::is_ip_str(peer) || hbb_common::is_domain_port_str(peer)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -1437,7 +1437,7 @@ fn try_send_close_event(event_stream: &Option<StreamSink<EventToUI>>) {
|
|||||||
pub fn update_text_clipboard_required() {
|
pub fn update_text_clipboard_required() {
|
||||||
let is_required = sessions::get_sessions()
|
let is_required = sessions::get_sessions()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|s| s.is_text_clipboard_required());
|
.any(|s| s.is_default() && s.is_text_clipboard_required());
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
|
let _ = scrap::android::ffi::call_clipboard_manager_enable_client_clipboard(is_required);
|
||||||
Client::set_is_text_clipboard_required(is_required);
|
Client::set_is_text_clipboard_required(is_required);
|
||||||
@@ -1447,13 +1447,16 @@ pub fn update_text_clipboard_required() {
|
|||||||
pub fn update_file_clipboard_required() {
|
pub fn update_file_clipboard_required() {
|
||||||
let is_required = sessions::get_sessions()
|
let is_required = sessions::get_sessions()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|s| s.is_file_clipboard_required());
|
.any(|s| s.is_default() && s.is_file_clipboard_required());
|
||||||
Client::set_is_file_clipboard_required(is_required);
|
Client::set_is_file_clipboard_required(is_required);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
|
pub fn send_clipboard_msg(msg: Message, _is_file: bool) {
|
||||||
for s in sessions::get_sessions() {
|
for s in sessions::get_sessions() {
|
||||||
|
if !s.is_default() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
if _is_file {
|
if _is_file {
|
||||||
if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version)
|
if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version)
|
||||||
@@ -2297,6 +2300,16 @@ pub mod sessions {
|
|||||||
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
|
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[cfg(not(target_os = "ios"))]
|
||||||
|
pub fn has_connected_sessions_running(conn_type: ConnType) -> bool {
|
||||||
|
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
|
||||||
|
*r#type == conn_type
|
||||||
|
&& s.session_handlers.read().unwrap().len() != 0
|
||||||
|
&& s.connection_round_state.lock().unwrap().is_connected()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) mod async_tasks {
|
pub(super) mod async_tasks {
|
||||||
|
|||||||
@@ -1026,7 +1026,7 @@ pub fn main_set_option(key: String, value: String) {
|
|||||||
set_option(key, value.clone());
|
set_option(key, value.clone());
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))]
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
crate::common::test_rendezvous_server();
|
crate::common::test_rendezvous_server();
|
||||||
} else {
|
} else {
|
||||||
set_option(key, value.clone());
|
set_option(key, value.clone());
|
||||||
@@ -1224,9 +1224,14 @@ pub fn main_set_local_option(key: String, value: String) {
|
|||||||
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER);
|
||||||
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER);
|
||||||
set_local_option(key, value.clone());
|
set_local_option(key, value.clone());
|
||||||
|
let is_render_target =
|
||||||
|
|session: &crate::flutter::FlutterSession| session.is_default() || session.is_view_camera();
|
||||||
if is_texture_render_key {
|
if is_texture_render_key {
|
||||||
let session_event = [("v", &value)];
|
let session_event = [("v", &value)];
|
||||||
for session in sessions::get_sessions() {
|
for session in sessions::get_sessions() {
|
||||||
|
if !is_render_target(&session) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
session.push_event("use_texture_render", &session_event, &[]);
|
session.push_event("use_texture_render", &session_event, &[]);
|
||||||
session.use_texture_render_changed();
|
session.use_texture_render_changed();
|
||||||
session.ui_handler.update_use_texture_render();
|
session.ui_handler.update_use_texture_render();
|
||||||
@@ -1234,6 +1239,9 @@ pub fn main_set_local_option(key: String, value: String) {
|
|||||||
}
|
}
|
||||||
if is_d3d_render_key {
|
if is_d3d_render_key {
|
||||||
for session in sessions::get_sessions() {
|
for session in sessions::get_sessions() {
|
||||||
|
if !is_render_target(&session) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
session.update_supported_decodings();
|
session.update_supported_decodings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2852,8 +2860,16 @@ pub fn main_get_common(key: String) -> String {
|
|||||||
crate::platform::windows::is_msi_installed(),
|
crate::platform::windows::is_msi_installed(),
|
||||||
crate::common::is_custom_client(),
|
crate::common::is_custom_client(),
|
||||||
) {
|
) {
|
||||||
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"),
|
(Ok(true), false) => match crate::platform::windows::release_arch_suffix() {
|
||||||
(Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"),
|
Some(arch) => format!("rustdesk-{_version}-{arch}.msi"),
|
||||||
|
None => "error:unsupported".to_owned(),
|
||||||
|
},
|
||||||
|
(Ok(true), true) | (Ok(false), _) => {
|
||||||
|
match crate::platform::windows::release_arch_suffix() {
|
||||||
|
Some(arch) => format!("rustdesk-{_version}-{arch}.exe"),
|
||||||
|
None => "error:unsupported".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
(Err(e), _) => {
|
(Err(e), _) => {
|
||||||
log::error!("Failed to check if is msi: {}", e);
|
log::error!("Failed to check if is msi: {}", e);
|
||||||
format!("error:update-failed-check-msi-tip")
|
format!("error:update-failed-check-msi-tip")
|
||||||
@@ -3012,6 +3028,16 @@ pub fn main_set_common(_key: String, _value: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn session_set_common(session_id: SessionID, key: String, value: String) {
|
||||||
|
if let Some(s) = sessions::get_session_by_session_id(&session_id) {
|
||||||
|
if key == "continue-insecure-connection"
|
||||||
|
{
|
||||||
|
s.continue_insecure_connection(value == "Y");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn session_get_common_sync(
|
pub fn session_get_common_sync(
|
||||||
session_id: SessionID,
|
session_id: SessionID,
|
||||||
key: String,
|
key: String,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ mod http_client;
|
|||||||
pub mod record_upload;
|
pub mod record_upload;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub use http_client::{
|
pub use http_client::{
|
||||||
create_http_client_async, create_http_client_async_with_url, create_http_client_with_url,
|
create_http_client_async, create_http_client_async_with_url_strict,
|
||||||
get_url_for_tls,
|
create_http_client_with_url, create_http_client_with_url_strict, get_url_for_tls,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::create_http_client_async_with_url;
|
use super::create_http_client_async_with_url_strict;
|
||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
bail,
|
bail,
|
||||||
lazy_static::lazy_static,
|
lazy_static::lazy_static,
|
||||||
@@ -167,7 +167,7 @@ async fn do_download(
|
|||||||
auto_del_dur: Option<Duration>,
|
auto_del_dur: Option<Duration>,
|
||||||
mut rx_cancel: UnboundedReceiver<()>,
|
mut rx_cancel: UnboundedReceiver<()>,
|
||||||
) -> ResultType<bool> {
|
) -> ResultType<bool> {
|
||||||
let client = create_http_client_async_with_url(&url).await;
|
let client = create_http_client_async_with_url_strict(&url).await?;
|
||||||
|
|
||||||
let mut is_all_downloaded = false;
|
let mut is_all_downloaded = false;
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
async_recursion::async_recursion,
|
async_recursion::async_recursion,
|
||||||
|
bail,
|
||||||
config::{Config, Socks5Server},
|
config::{Config, Socks5Server},
|
||||||
log::{self, info},
|
log::{self, info},
|
||||||
proxy::{Proxy, ProxyScheme},
|
proxy::{Proxy, ProxyScheme},
|
||||||
@@ -7,6 +8,7 @@ use hbb_common::{
|
|||||||
get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache,
|
get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache,
|
||||||
TlsType,
|
TlsType,
|
||||||
},
|
},
|
||||||
|
ResultType,
|
||||||
};
|
};
|
||||||
use reqwest::{blocking::Client as SyncClient, Client as AsyncClient};
|
use reqwest::{blocking::Client as SyncClient, Client as AsyncClient};
|
||||||
|
|
||||||
@@ -137,6 +139,32 @@ pub fn create_http_client_with_url(url: &str) -> SyncClient {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_http_client_with_url_strict(url: &str) -> ResultType<SyncClient> {
|
||||||
|
let parsed_url = url::Url::parse(url)?;
|
||||||
|
if parsed_url.scheme() != "https" {
|
||||||
|
bail!("Strict HTTP client requires HTTPS: {}", url);
|
||||||
|
}
|
||||||
|
let proxy_conf = Config::get_socks();
|
||||||
|
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||||
|
let cached_tls_type = get_cached_tls_type(tls_url);
|
||||||
|
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||||
|
let can_reuse_cached_probe =
|
||||||
|
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
|
||||||
|
let tls_type = if can_reuse_cached_probe {
|
||||||
|
cached_tls_type.unwrap_or(TlsType::Rustls)
|
||||||
|
} else {
|
||||||
|
TlsType::Rustls
|
||||||
|
};
|
||||||
|
Ok(create_http_client_with_url_(
|
||||||
|
url,
|
||||||
|
tls_url,
|
||||||
|
tls_type,
|
||||||
|
can_reuse_cached_probe,
|
||||||
|
Some(false),
|
||||||
|
Some(false),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn create_http_client_with_url_(
|
fn create_http_client_with_url_(
|
||||||
url: &str,
|
url: &str,
|
||||||
tls_url: &str,
|
tls_url: &str,
|
||||||
@@ -247,6 +275,33 @@ pub async fn create_http_client_async_with_url(url: &str) -> AsyncClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_http_client_async_with_url_strict(url: &str) -> ResultType<AsyncClient> {
|
||||||
|
let parsed_url = url::Url::parse(url)?;
|
||||||
|
if parsed_url.scheme() != "https" {
|
||||||
|
bail!("Strict HTTP client requires HTTPS: {}", url);
|
||||||
|
}
|
||||||
|
let proxy_conf = Config::get_socks();
|
||||||
|
let tls_url = get_url_for_tls(url, &proxy_conf);
|
||||||
|
let cached_tls_type = get_cached_tls_type(tls_url);
|
||||||
|
let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url);
|
||||||
|
let can_reuse_cached_probe =
|
||||||
|
cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false);
|
||||||
|
let tls_type = if can_reuse_cached_probe {
|
||||||
|
cached_tls_type.unwrap_or(TlsType::Rustls)
|
||||||
|
} else {
|
||||||
|
TlsType::Rustls
|
||||||
|
};
|
||||||
|
Ok(create_http_client_async_with_url_(
|
||||||
|
url,
|
||||||
|
tls_url,
|
||||||
|
tls_type,
|
||||||
|
can_reuse_cached_probe,
|
||||||
|
Some(false),
|
||||||
|
Some(false),
|
||||||
|
)
|
||||||
|
.await)
|
||||||
|
}
|
||||||
|
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
async fn create_http_client_async_with_url_(
|
async fn create_http_client_async_with_url_(
|
||||||
url: &str,
|
url: &str,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
use crate::flutter;
|
use crate::flutter;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk};
|
use crate::platform::windows::{get_char_from_vk, get_unicode_from_vk};
|
||||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
#[cfg(not(feature = "flutter"))]
|
||||||
use crate::ui::CUR_SESSION;
|
use crate::ui::CUR_SESSION;
|
||||||
use crate::ui_session_interface::{InvokeUiSession, Session};
|
use crate::ui_session_interface::{InvokeUiSession, Session};
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
@@ -469,7 +469,7 @@ static mut IS_LEFT_OPTION_DOWN: bool = false;
|
|||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
fn get_keyboard_mode() -> String {
|
fn get_keyboard_mode() -> String {
|
||||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
#[cfg(not(feature = "flutter"))]
|
||||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||||
return session.get_keyboard_mode();
|
return session.get_keyboard_mode();
|
||||||
}
|
}
|
||||||
@@ -991,7 +991,7 @@ pub fn event_to_key_events(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_key_event(key_event: &KeyEvent) {
|
pub fn send_key_event(key_event: &KeyEvent) {
|
||||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
#[cfg(not(feature = "flutter"))]
|
||||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||||
session.send_key_event(key_event);
|
session.send_key_event(key_event);
|
||||||
}
|
}
|
||||||
@@ -1003,7 +1003,7 @@ pub fn send_key_event(key_event: &KeyEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_peer_platform() -> String {
|
pub fn get_peer_platform() -> String {
|
||||||
#[cfg(not(any(feature = "flutter", feature = "cli")))]
|
#[cfg(not(feature = "flutter"))]
|
||||||
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() {
|
||||||
return session.peer_platform();
|
return session.peer_platform();
|
||||||
}
|
}
|
||||||
@@ -1245,11 +1245,49 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec<KeyEv
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn map_keyboard_mode(_peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
|
pub fn map_keyboard_mode(_peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
|
||||||
|
if let Some(evt) = windows_peer_special_key(_peer, event) {
|
||||||
|
return vec![evt];
|
||||||
|
}
|
||||||
|
|
||||||
_map_keyboard_mode(_peer, event, key_event)
|
_map_keyboard_mode(_peer, event, key_event)
|
||||||
.map(|e| vec![e])
|
.map(|e| vec![e])
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn windows_peer_special_key(peer: &str, event: &Event) -> Option<KeyEvent> {
|
||||||
|
if peer != OS_LOWER_WINDOWS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (key, down) = match event.event_type {
|
||||||
|
EventType::KeyPress(key) => (key, true),
|
||||||
|
EventType::KeyRelease(key) => (key, false),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle only `Pause` for Windows peers for now.
|
||||||
|
// Windows has no normal scan code for `Pause`, so send it as a legacy control key.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let is_pause = {
|
||||||
|
// The Windows scan code can look like `NumLock`; VK_PAUSE distinguishes it.
|
||||||
|
let pause_vk_code = rdev::win_code_from_key(Key::Pause);
|
||||||
|
key == Key::Pause || pause_vk_code == Some(event.platform_code as _)
|
||||||
|
};
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let is_pause = key == Key::Pause;
|
||||||
|
if !is_pause {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut key_event = KeyEvent::new();
|
||||||
|
key_event.mode = KeyboardMode::Legacy.into();
|
||||||
|
key_event.down = down;
|
||||||
|
key_event.set_control_key(ControlKey::Pause);
|
||||||
|
let (alt, ctrl, shift, command) = client::get_modifiers_state(false, false, false, false);
|
||||||
|
client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command);
|
||||||
|
Some(key_event)
|
||||||
|
}
|
||||||
|
|
||||||
fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option<KeyEvent> {
|
fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option<KeyEvent> {
|
||||||
match event.event_type {
|
match event.event_type {
|
||||||
EventType::KeyPress(..) => {
|
EventType::KeyPress(..) => {
|
||||||
@@ -1421,6 +1459,11 @@ fn is_press(event: &Event) -> bool {
|
|||||||
pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
|
pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
|
||||||
let mut events: Vec<KeyEvent> = Vec::new();
|
let mut events: Vec<KeyEvent> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(evt) = windows_peer_special_key(peer, event) {
|
||||||
|
events.push(evt);
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(unicode_info) = &event.unicode {
|
if let Some(unicode_info) = &event.unicode {
|
||||||
if unicode_info.is_dead {
|
if unicode_info.is_dead {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|||||||
74
src/lang.rs
74
src/lang.rs
@@ -103,15 +103,29 @@ pub const LANGS: &[(&str, &str)] = &[
|
|||||||
("gu", "ગુજરાતી"),
|
("gu", "ગુજરાતી"),
|
||||||
];
|
];
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
pub(crate) fn cjk_ui_unavailable() -> bool {
|
||||||
pub fn translate(name: String) -> String {
|
cfg!(all(
|
||||||
let locale = sys_locale::get_locale().unwrap_or_default();
|
target_os = "linux",
|
||||||
translate_locale(name, &locale)
|
target_arch = "aarch64",
|
||||||
|
feature = "flutter"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn translate_locale(name: String, locale: &str) -> String {
|
pub(crate) fn is_cjk_lang(lang_or_locale: &str) -> bool {
|
||||||
|
let lang = lang_or_locale
|
||||||
|
.split(|c| c == '-' || c == '_')
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase();
|
||||||
|
matches!(lang.as_str(), "zh" | "ja" | "ko")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_lang(saved_lang: &str, locale: &str, cjk_fallback: bool) -> String {
|
||||||
let locale = locale.to_lowercase();
|
let locale = locale.to_lowercase();
|
||||||
let mut lang = hbb_common::config::LocalConfig::get_option("lang").to_lowercase();
|
let mut lang = saved_lang.to_lowercase();
|
||||||
|
if cjk_fallback && is_cjk_lang(&lang) {
|
||||||
|
return "en".to_owned();
|
||||||
|
}
|
||||||
if lang.is_empty() {
|
if lang.is_empty() {
|
||||||
// zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android
|
// zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android
|
||||||
if locale.starts_with("zh") {
|
if locale.starts_with("zh") {
|
||||||
@@ -131,7 +145,25 @@ pub fn translate_locale(name: String, locale: &str) -> String {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_owned();
|
.to_owned();
|
||||||
}
|
}
|
||||||
let lang = lang.to_lowercase();
|
if cjk_fallback && is_cjk_lang(&lang) {
|
||||||
|
"en".to_owned()
|
||||||
|
} else {
|
||||||
|
lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
|
pub fn translate(name: String) -> String {
|
||||||
|
let locale = sys_locale::get_locale().unwrap_or_default();
|
||||||
|
translate_locale(name, &locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn translate_locale(name: String, locale: &str) -> String {
|
||||||
|
let lang = resolve_lang(
|
||||||
|
&hbb_common::config::LocalConfig::get_option("lang"),
|
||||||
|
locale,
|
||||||
|
cjk_ui_unavailable(),
|
||||||
|
);
|
||||||
let m = match lang.as_str() {
|
let m = match lang.as_str() {
|
||||||
"fr" => fr::T.deref(),
|
"fr" => fr::T.deref(),
|
||||||
"zh-cn" => cn::T.deref(),
|
"zh-cn" => cn::T.deref(),
|
||||||
@@ -275,4 +307,32 @@ mod test {
|
|||||||
("{} times {4} makes {8}".to_string(), Some("2".to_string()))
|
("{} times {4} makes {8}".to_string(), Some("2".to_string()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_lang_forces_english_for_saved_cjk_when_target_disables_cjk() {
|
||||||
|
use super::resolve_lang as f;
|
||||||
|
|
||||||
|
assert_eq!(f("zh-cn", "en-US", true), "en");
|
||||||
|
assert_eq!(f("zh-tw", "en-US", true), "en");
|
||||||
|
assert_eq!(f("ja", "en-US", true), "en");
|
||||||
|
assert_eq!(f("ko", "en-US", true), "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_lang_forces_english_for_cjk_locale_when_target_disables_cjk() {
|
||||||
|
use super::resolve_lang as f;
|
||||||
|
|
||||||
|
assert_eq!(f("", "zh_CN", true), "en");
|
||||||
|
assert_eq!(f("", "ja-JP", true), "en");
|
||||||
|
assert_eq!(f("", "ko_KR", true), "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_lang_preserves_cjk_when_target_allows_cjk() {
|
||||||
|
use super::resolve_lang as f;
|
||||||
|
|
||||||
|
assert_eq!(f("zh-cn", "en-US", false), "zh-cn");
|
||||||
|
assert_eq!(f("", "zh_TW", false), "zh-tw");
|
||||||
|
assert_eq!(f("", "ja-JP", false), "ja");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "اتصال الوسيط"),
|
("Relay Connection", "اتصال الوسيط"),
|
||||||
("Secure Connection", "اتصال آمن"),
|
("Secure Connection", "اتصال آمن"),
|
||||||
("Insecure Connection", "اتصال غير آمن"),
|
("Insecure Connection", "اتصال غير آمن"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "المقياس الأصلي"),
|
("Scale original", "المقياس الأصلي"),
|
||||||
("Scale adaptive", "مقياس التكيف"),
|
("Scale adaptive", "مقياس التكيف"),
|
||||||
("General", "عام"),
|
("General", "عام"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "إعادة تعيين اختيار إدخال لوحة المفاتيح"),
|
("wayland-keyboard-input-reset-choice-tip", "إعادة تعيين اختيار إدخال لوحة المفاتيح"),
|
||||||
("remember-wayland-keyboard-choice-tip", "لا تسأل مرة أخرى لهذا الكمبيوتر البعيد"),
|
("remember-wayland-keyboard-choice-tip", "لا تسأل مرة أخرى لهذا الكمبيوتر البعيد"),
|
||||||
("Why this happens", "سبب حدوث ذلك"),
|
("Why this happens", "سبب حدوث ذلك"),
|
||||||
|
("Switch display", "تبديل الشاشة"),
|
||||||
|
("Show monitor switch button on the main toolbar", "إظهار زر تبديل الشاشة على شريط الأدوات الرئيسي"),
|
||||||
|
("Show on the minimized toolbar", "الإظهار على شريط الأدوات المُصغّر"),
|
||||||
|
("All monitors", "جميع الشاشات"),
|
||||||
|
("#{} monitor", "الشاشة رقم {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "تعذر التحقق من التشفير من طرف إلى طرف.\nقد يكون الجهاز البعيد ما يزال قيد الإعداد. حاول مرة أخرى لاحقًا.\nإذا استمر حدوث ذلك، فقد يكون الخادم غير موثوق به.\nهل تريد المتابعة على أي حال؟"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Рэтрансляванае падключэнне"),
|
("Relay Connection", "Рэтрансляванае падключэнне"),
|
||||||
("Secure Connection", "Бяспечнае падключэнне"),
|
("Secure Connection", "Бяспечнае падключэнне"),
|
||||||
("Insecure Connection", "Нябяспечнае падключэнне"),
|
("Insecure Connection", "Нябяспечнае падключэнне"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Арыгінальны маштаб"),
|
("Scale original", "Арыгінальны маштаб"),
|
||||||
("Scale adaptive", "Адаптыўны маштаб"),
|
("Scale adaptive", "Адаптыўны маштаб"),
|
||||||
("General", "Агульныя"),
|
("General", "Агульныя"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Скінуць выбар уводу з клавіятуры"),
|
("wayland-keyboard-input-reset-choice-tip", "Скінуць выбар уводу з клавіятуры"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Не пытацца зноў для гэтага аддаленага кампутара"),
|
("remember-wayland-keyboard-choice-tip", "Не пытацца зноў для гэтага аддаленага кампутара"),
|
||||||
("Why this happens", "Чаму гэта адбываецца"),
|
("Why this happens", "Чаму гэта адбываецца"),
|
||||||
|
("Switch display", "Пераключыць дысплэй"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Паказваць кнопку пераключэння манітора на галоўнай панэлі інструментаў"),
|
||||||
|
("Show on the minimized toolbar", "Паказваць на згорнутай панэлі інструментаў"),
|
||||||
|
("All monitors", "Усе манітори"),
|
||||||
|
("#{} monitor", "Манітор {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Не ўдалося праверыць скразное шыфраванне.\nАддаленая прылада, магчыма, яшчэ наладжваецца. Паспрабуйце пазней.\nКалі гэта будзе паўтарацца, сервер можа быць ненадзейным.\nУсё роўна працягнуць?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Релейна връзка"),
|
("Relay Connection", "Релейна връзка"),
|
||||||
("Secure Connection", "Сигурна връзка"),
|
("Secure Connection", "Сигурна връзка"),
|
||||||
("Insecure Connection", "Несигурна връзка"),
|
("Insecure Connection", "Несигурна връзка"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Оригинален мащаб"),
|
("Scale original", "Оригинален мащаб"),
|
||||||
("Scale adaptive", "Приспособимо мащабиране"),
|
("Scale adaptive", "Приспособимо мащабиране"),
|
||||||
("General", "Основен"),
|
("General", "Основен"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Нулиране на избора за въвеждане от клавиатура"),
|
("wayland-keyboard-input-reset-choice-tip", "Нулиране на избора за въвеждане от клавиатура"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Не питай отново за този отдалечен компютър"),
|
("remember-wayland-keyboard-choice-tip", "Не питай отново за този отдалечен компютър"),
|
||||||
("Why this happens", "Защо се случва това"),
|
("Why this happens", "Защо се случва това"),
|
||||||
|
("Switch display", "Превключване на дисплея"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Показване на бутона за превключване на монитора в главната лента с инструменти"),
|
||||||
|
("Show on the minimized toolbar", "Показване в минимизираната лента с инструменти"),
|
||||||
|
("All monitors", "Всички монитори"),
|
||||||
|
("#{} monitor", "Монитор {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Шифроването от край до край не може да бъде проверено.\nОтдалеченото устройство може все още да се настройва. Опитайте отново по-късно.\nАко това продължи, сървърът може да не е надежден.\nДа се продължи ли въпреки това?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Connexió amb repetidor"),
|
("Relay Connection", "Connexió amb repetidor"),
|
||||||
("Secure Connection", "Connexió segura"),
|
("Secure Connection", "Connexió segura"),
|
||||||
("Insecure Connection", "Connexió no segura"),
|
("Insecure Connection", "Connexió no segura"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Escala original"),
|
("Scale original", "Escala original"),
|
||||||
("Scale adaptive", "Escala adaptativa"),
|
("Scale adaptive", "Escala adaptativa"),
|
||||||
("General", "General"),
|
("General", "General"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Restableix l'opció d'entrada de teclat"),
|
("wayland-keyboard-input-reset-choice-tip", "Restableix l'opció d'entrada de teclat"),
|
||||||
("remember-wayland-keyboard-choice-tip", "No tornis a preguntar-ho per a aquest equip remot"),
|
("remember-wayland-keyboard-choice-tip", "No tornis a preguntar-ho per a aquest equip remot"),
|
||||||
("Why this happens", "Per què passa això"),
|
("Why this happens", "Per què passa això"),
|
||||||
|
("Switch display", "Canvia de pantalla"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Mostra el botó de canvi de monitor a la barra d’eines principal"),
|
||||||
|
("Show on the minimized toolbar", "Mostra a la barra d’eines minimitzada"),
|
||||||
|
("All monitors", "Tots els monitors"),
|
||||||
|
("#{} monitor", "Monitor {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "No s'ha pogut verificar el xifratge d'extrem a extrem.\nEl dispositiu remot encara es pot estar configurant. Torneu-ho a provar més tard.\nSi això continua passant, el servidor pot no ser de confiança.\nVoleu continuar igualment?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "中继连接"),
|
("Relay Connection", "中继连接"),
|
||||||
("Secure Connection", "安全连接"),
|
("Secure Connection", "安全连接"),
|
||||||
("Insecure Connection", "非安全连接"),
|
("Insecure Connection", "非安全连接"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "原始尺寸"),
|
("Scale original", "原始尺寸"),
|
||||||
("Scale adaptive", "适应窗口"),
|
("Scale adaptive", "适应窗口"),
|
||||||
("General", "常规"),
|
("General", "常规"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "重置键盘输入选择"),
|
("wayland-keyboard-input-reset-choice-tip", "重置键盘输入选择"),
|
||||||
("remember-wayland-keyboard-choice-tip", "以后对这台远程电脑不再询问"),
|
("remember-wayland-keyboard-choice-tip", "以后对这台远程电脑不再询问"),
|
||||||
("Why this happens", "了解原因"),
|
("Why this happens", "了解原因"),
|
||||||
|
("Switch display", "切换显示器"),
|
||||||
|
("Show monitor switch button on the main toolbar", "在主工具栏上显示显示器切换按钮"),
|
||||||
|
("Show on the minimized toolbar", "在最小化工具栏上显示"),
|
||||||
|
("All monitors", "所有显示器"),
|
||||||
|
("#{} monitor", "{}号显示器"),
|
||||||
|
("conn-e2ee-unavailable-tip", "无法验证端到端加密。\n远程设备可能仍在准备中,请稍后重试。\n如果此问题持续出现,服务器可能不受信任。\n仍要继续吗?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Připojení předávací server"),
|
("Relay Connection", "Připojení předávací server"),
|
||||||
("Secure Connection", "Zabezpečené připojení"),
|
("Secure Connection", "Zabezpečené připojení"),
|
||||||
("Insecure Connection", "Nezabezpečené připojení"),
|
("Insecure Connection", "Nezabezpečené připojení"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Originální měřítko"),
|
("Scale original", "Originální měřítko"),
|
||||||
("Scale adaptive", "Adaptivní měřítko"),
|
("Scale adaptive", "Adaptivní měřítko"),
|
||||||
("General", "Obecné"),
|
("General", "Obecné"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Resetovat volbu vstupu z klávesnice"),
|
("wayland-keyboard-input-reset-choice-tip", "Resetovat volbu vstupu z klávesnice"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Pro tento vzdálený počítač se již neptat"),
|
("remember-wayland-keyboard-choice-tip", "Pro tento vzdálený počítač se již neptat"),
|
||||||
("Why this happens", "Proč k tomu dochází"),
|
("Why this happens", "Proč k tomu dochází"),
|
||||||
|
("Switch display", "Přepnout obrazovku"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Zobrazit tlačítko přepnutí monitoru na hlavním panelu nástrojů"),
|
||||||
|
("Show on the minimized toolbar", "Zobrazit na minimalizovaném panelu nástrojů"),
|
||||||
|
("All monitors", "Všechny monitory"),
|
||||||
|
("#{} monitor", "Monitor č. {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Nepodařilo se ověřit koncové šifrování.\nVzdálené zařízení se možná stále nastavuje. Zkuste to znovu později.\nPokud se to bude opakovat, server nemusí být důvěryhodný.\nPřesto pokračovat?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Viderestillingsforbindelse"),
|
("Relay Connection", "Viderestillingsforbindelse"),
|
||||||
("Secure Connection", "Sikker forbindelse"),
|
("Secure Connection", "Sikker forbindelse"),
|
||||||
("Insecure Connection", "Usikker forbindelse"),
|
("Insecure Connection", "Usikker forbindelse"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Original skalering"),
|
("Scale original", "Original skalering"),
|
||||||
("Scale adaptive", "Adaptiv skalering"),
|
("Scale adaptive", "Adaptiv skalering"),
|
||||||
("General", "Generelt"),
|
("General", "Generelt"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Nulstil valg for tastaturinput"),
|
("wayland-keyboard-input-reset-choice-tip", "Nulstil valg for tastaturinput"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Spørg ikke igen for denne fjerncomputer"),
|
("remember-wayland-keyboard-choice-tip", "Spørg ikke igen for denne fjerncomputer"),
|
||||||
("Why this happens", "Hvorfor dette sker"),
|
("Why this happens", "Hvorfor dette sker"),
|
||||||
|
("Switch display", "Skift skærm"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Vis knap til skærmskift på hovedværktøjslinjen"),
|
||||||
|
("Show on the minimized toolbar", "Vis på den minimerede værktøjslinje"),
|
||||||
|
("All monitors", "Alle skærme"),
|
||||||
|
("#{} monitor", "Skærm {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "End-to-end-kryptering kunne ikke bekræftes.\nDen eksterne enhed er muligvis stadig ved at blive konfigureret. Prøv igen senere.\nHvis dette fortsætter, er serveren muligvis ikke pålidelig.\nFortsæt alligevel?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Relay-Verbindung"),
|
("Relay Connection", "Relay-Verbindung"),
|
||||||
("Secure Connection", "Sichere Verbindung"),
|
("Secure Connection", "Sichere Verbindung"),
|
||||||
("Insecure Connection", "Unsichere Verbindung"),
|
("Insecure Connection", "Unsichere Verbindung"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Keine Skalierung"),
|
("Scale original", "Keine Skalierung"),
|
||||||
("Scale adaptive", "Anpassbare Skalierung"),
|
("Scale adaptive", "Anpassbare Skalierung"),
|
||||||
("General", "Allgemein"),
|
("General", "Allgemein"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Auswahl der Tastatureingabe zurücksetzen"),
|
("wayland-keyboard-input-reset-choice-tip", "Auswahl der Tastatureingabe zurücksetzen"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Für diesen entfernten Computer nicht erneut fragen"),
|
("remember-wayland-keyboard-choice-tip", "Für diesen entfernten Computer nicht erneut fragen"),
|
||||||
("Why this happens", "Warum dies passiert"),
|
("Why this happens", "Warum dies passiert"),
|
||||||
|
("Switch display", "Anzeige wechseln"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Schaltfläche zum Monitorwechsel in der Haupt-Symbolleiste anzeigen"),
|
||||||
|
("Show on the minimized toolbar", "In der minimierten Symbolleiste anzeigen"),
|
||||||
|
("All monitors", "Alle Bildschirme"),
|
||||||
|
("#{} monitor", "Bildschirm {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Ende-zu-Ende-Verschlüsselung konnte nicht verifiziert werden.\nDas entfernte Gerät wird möglicherweise noch eingerichtet. Versuchen Sie es später erneut.\nWenn dies weiterhin auftritt, ist der Server möglicherweise nicht vertrauenswürdig.\nTrotzdem fortfahren?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Αναμεταδιδόμενη σύνδεση"),
|
("Relay Connection", "Αναμεταδιδόμενη σύνδεση"),
|
||||||
("Secure Connection", "Ασφαλής σύνδεση"),
|
("Secure Connection", "Ασφαλής σύνδεση"),
|
||||||
("Insecure Connection", "Μη ασφαλής σύνδεση"),
|
("Insecure Connection", "Μη ασφαλής σύνδεση"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Κλιμάκωση πρωτότυπου"),
|
("Scale original", "Κλιμάκωση πρωτότυπου"),
|
||||||
("Scale adaptive", "Προσαρμοσμένη κλίμακα"),
|
("Scale adaptive", "Προσαρμοσμένη κλίμακα"),
|
||||||
("General", "Γενικά"),
|
("General", "Γενικά"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Επαναφορά επιλογής εισαγωγής από πληκτρολόγιο"),
|
("wayland-keyboard-input-reset-choice-tip", "Επαναφορά επιλογής εισαγωγής από πληκτρολόγιο"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Να μην ερωτηθώ ξανά για αυτόν τον απομακρυσμένο υπολογιστή"),
|
("remember-wayland-keyboard-choice-tip", "Να μην ερωτηθώ ξανά για αυτόν τον απομακρυσμένο υπολογιστή"),
|
||||||
("Why this happens", "Γιατί συμβαίνει αυτό"),
|
("Why this happens", "Γιατί συμβαίνει αυτό"),
|
||||||
|
("Switch display", "Εναλλαγή οθόνης"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Εμφάνιση κουμπιού εναλλαγής οθόνης στην κύρια γραμμή εργαλείων"),
|
||||||
|
("Show on the minimized toolbar", "Εμφάνιση στην ελαχιστοποιημένη γραμμή εργαλείων"),
|
||||||
|
("All monitors", "Όλες οι οθόνες"),
|
||||||
|
("#{} monitor", "Οθόνη {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Δεν ήταν δυνατή η επαλήθευση της κρυπτογράφησης από άκρο σε άκρο.\nΗ απομακρυσμένη συσκευή μπορεί να ρυθμίζεται ακόμα. Δοκιμάστε ξανά αργότερα.\nΑν αυτό συνεχιστεί, ο διακομιστής μπορεί να μην είναι αξιόπιστος.\nΣυνέχεια παρ' όλα αυτά;"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,5 +279,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-soft-keyboard-input-label", "Soft keyboard input"),
|
("wayland-soft-keyboard-input-label", "Soft keyboard input"),
|
||||||
("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"),
|
("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"),
|
("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Could not verify end-to-end encryption.\nThe remote device may still be setting up. Try again later.\nIf this keeps happening, the server may be untrusted.\nContinue anyway?")
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Relajsa Konekto"),
|
("Relay Connection", "Relajsa Konekto"),
|
||||||
("Secure Connection", "Sekura Konekto"),
|
("Secure Connection", "Sekura Konekto"),
|
||||||
("Insecure Connection", "Nesekura Konekto"),
|
("Insecure Connection", "Nesekura Konekto"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Skalo originalo"),
|
("Scale original", "Skalo originalo"),
|
||||||
("Scale adaptive", "Skalo adapta"),
|
("Scale adaptive", "Skalo adapta"),
|
||||||
("General", "Ĝenerala"),
|
("General", "Ĝenerala"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Restarigi la elekton de klavara enigo"),
|
("wayland-keyboard-input-reset-choice-tip", "Restarigi la elekton de klavara enigo"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Ne demandi denove por ĉi tiu fora komputilo"),
|
("remember-wayland-keyboard-choice-tip", "Ne demandi denove por ĉi tiu fora komputilo"),
|
||||||
("Why this happens", "Kial ĉi tio okazas"),
|
("Why this happens", "Kial ĉi tio okazas"),
|
||||||
|
("Switch display", "Ŝalti ekranon"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Montri ekran-ŝaltan butonon en la ĉefa ilobreto"),
|
||||||
|
("Show on the minimized toolbar", "Montri en la minimumigita ilobreto"),
|
||||||
|
("All monitors", "Ĉiuj monitoroj"),
|
||||||
|
("#{} monitor", "Monitoro {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Ne eblis kontroli la fin-al-finan ĉifradon.\nLa fora aparato eble ankoraŭ estas agordata. Provu denove poste.\nSe tio daŭre okazas, la servilo eble estas nefidinda.\nĈu daŭrigi tamen?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Conexión Relay"),
|
("Relay Connection", "Conexión Relay"),
|
||||||
("Secure Connection", "Conexión segura"),
|
("Secure Connection", "Conexión segura"),
|
||||||
("Insecure Connection", "Conexión insegura"),
|
("Insecure Connection", "Conexión insegura"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Escala original"),
|
("Scale original", "Escala original"),
|
||||||
("Scale adaptive", "Escala adaptativa"),
|
("Scale adaptive", "Escala adaptativa"),
|
||||||
("General", "General"),
|
("General", "General"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Restablecer la opción de entrada del teclado"),
|
("wayland-keyboard-input-reset-choice-tip", "Restablecer la opción de entrada del teclado"),
|
||||||
("remember-wayland-keyboard-choice-tip", "No volver a preguntar para este equipo remoto"),
|
("remember-wayland-keyboard-choice-tip", "No volver a preguntar para este equipo remoto"),
|
||||||
("Why this happens", "Por qué ocurre esto"),
|
("Why this happens", "Por qué ocurre esto"),
|
||||||
|
("Switch display", "Cambiar de pantalla"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Mostrar el botón de cambio de monitor en la barra de herramientas principal"),
|
||||||
|
("Show on the minimized toolbar", "Mostrar en la barra de herramientas minimizada"),
|
||||||
|
("All monitors", "Todos los monitores"),
|
||||||
|
("#{} monitor", "Monitor {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "No se pudo verificar el cifrado de extremo a extremo.\nEs posible que el dispositivo remoto aún se esté configurando. Inténtelo de nuevo más tarde.\nSi esto sigue ocurriendo, es posible que el servidor no sea de confianza.\n¿Continuar de todos modos?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Releeühendus"),
|
("Relay Connection", "Releeühendus"),
|
||||||
("Secure Connection", "Turvaline ühendus"),
|
("Secure Connection", "Turvaline ühendus"),
|
||||||
("Insecure Connection", "Ebaturvaline ühendus"),
|
("Insecure Connection", "Ebaturvaline ühendus"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Originaalskaala"),
|
("Scale original", "Originaalskaala"),
|
||||||
("Scale adaptive", "Kohanduv skaala"),
|
("Scale adaptive", "Kohanduv skaala"),
|
||||||
("General", "Üldine"),
|
("General", "Üldine"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Lähtesta klaviatuurisisestuse valik"),
|
("wayland-keyboard-input-reset-choice-tip", "Lähtesta klaviatuurisisestuse valik"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Ära küsi selle kaugarvuti puhul uuesti"),
|
("remember-wayland-keyboard-choice-tip", "Ära küsi selle kaugarvuti puhul uuesti"),
|
||||||
("Why this happens", "Miks see juhtub"),
|
("Why this happens", "Miks see juhtub"),
|
||||||
|
("Switch display", "Vaheta kuva"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Näita monitori vahetamise nuppu peamisel tööriistaribal"),
|
||||||
|
("Show on the minimized toolbar", "Näita minimeeritud tööriistaribal"),
|
||||||
|
("All monitors", "Kõik kuvarid"),
|
||||||
|
("#{} monitor", "Kuvar {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Otspunktkrüptimist ei saanud kontrollida.\nKaugseade võib olla veel seadistamisel. Proovige hiljem uuesti.\nKui see jätkub, ei pruugi server olla usaldusväärne.\nKas jätkata siiski?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Konexio igorria"),
|
("Relay Connection", "Konexio igorria"),
|
||||||
("Secure Connection", "Konexio segurua"),
|
("Secure Connection", "Konexio segurua"),
|
||||||
("Insecure Connection", "Konexio ez-segurua"),
|
("Insecure Connection", "Konexio ez-segurua"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "Jatorrizko eskala"),
|
("Scale original", "Jatorrizko eskala"),
|
||||||
("Scale adaptive", "Eskala moldagarria"),
|
("Scale adaptive", "Eskala moldagarria"),
|
||||||
("General", "Orokorra"),
|
("General", "Orokorra"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "Berrezarri teklatuko sarreraren aukera"),
|
("wayland-keyboard-input-reset-choice-tip", "Berrezarri teklatuko sarreraren aukera"),
|
||||||
("remember-wayland-keyboard-choice-tip", "Ez galdetu berriro urruneko ordenagailu honetarako"),
|
("remember-wayland-keyboard-choice-tip", "Ez galdetu berriro urruneko ordenagailu honetarako"),
|
||||||
("Why this happens", "Zergatik gertatzen den hau"),
|
("Why this happens", "Zergatik gertatzen den hau"),
|
||||||
|
("Switch display", "Aldatu pantaila"),
|
||||||
|
("Show monitor switch button on the main toolbar", "Erakutsi monitorea aldatzeko botoia tresna-barra nagusian"),
|
||||||
|
("Show on the minimized toolbar", "Erakutsi minimizatutako tresna-barran"),
|
||||||
|
("All monitors", "Monitore guztiak"),
|
||||||
|
("#{} monitor", "{}. monitorea"),
|
||||||
|
("conn-e2ee-unavailable-tip", "Ezin izan da muturretik muturrerako enkriptatzea egiaztatu.\nUrruneko gailua oraindik konfiguratzen ari daiteke. Saiatu berriro geroago.\nHonek jarraitzen badu, zerbitzaria fidagaitza izan daiteke.\nHala ere jarraitu?"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("Relay Connection", "Relay ارتباط"),
|
("Relay Connection", "Relay ارتباط"),
|
||||||
("Secure Connection", "ارتباط امن"),
|
("Secure Connection", "ارتباط امن"),
|
||||||
("Insecure Connection", "ارتباط غیر امن"),
|
("Insecure Connection", "ارتباط غیر امن"),
|
||||||
|
("Continue", ""),
|
||||||
("Scale original", "مقیاس اصلی"),
|
("Scale original", "مقیاس اصلی"),
|
||||||
("Scale adaptive", "مقیاس تطبیقی"),
|
("Scale adaptive", "مقیاس تطبیقی"),
|
||||||
("General", "عمومی"),
|
("General", "عمومی"),
|
||||||
@@ -758,5 +759,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("wayland-keyboard-input-reset-choice-tip", "بازنشانی انتخاب ورودی صفحه کلید"),
|
("wayland-keyboard-input-reset-choice-tip", "بازنشانی انتخاب ورودی صفحه کلید"),
|
||||||
("remember-wayland-keyboard-choice-tip", "برای این رایانه از راه دور دوباره نپرس"),
|
("remember-wayland-keyboard-choice-tip", "برای این رایانه از راه دور دوباره نپرس"),
|
||||||
("Why this happens", "چرا این اتفاق میافتد"),
|
("Why this happens", "چرا این اتفاق میافتد"),
|
||||||
|
("Switch display", "تعویض نمایشگر"),
|
||||||
|
("Show monitor switch button on the main toolbar", "نمایش دکمه تعویض نمایشگر در نوار ابزار اصلی"),
|
||||||
|
("Show on the minimized toolbar", "نمایش در نوار ابزار کوچکشده"),
|
||||||
|
("All monitors", "همه نمایشگرها"),
|
||||||
|
("#{} monitor", "نمایشگر {}"),
|
||||||
|
("conn-e2ee-unavailable-tip", "رمزنگاری سرتاسری قابل تأیید نیست.\nدستگاه راه دور ممکن است هنوز در حال آمادهسازی باشد. بعداً دوباره تلاش کنید.\nاگر این مشکل ادامه داشت، سرور ممکن است نامطمئن باشد.\nبا این حال ادامه میدهید؟"),
|
||||||
].iter().cloned().collect();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user