mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-12 10:14:56 +03:00
Compare commits
101 Commits
webrtc
...
b3bd18845d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
bf206dc309 | ||
|
|
6d116cf1c9 | ||
|
|
1fc33218dc | ||
|
|
b73e5bbfa0 | ||
|
|
78533e428e | ||
|
|
cc7fe4efdc | ||
|
|
84af60c07e | ||
|
|
6426269d41 | ||
|
|
7c41f993fe | ||
|
|
0c86d46162 | ||
|
|
e87797418f | ||
|
|
78a3a2aeb9 | ||
|
|
e18cf7a245 | ||
|
|
50d5823ef5 | ||
|
|
518296f257 | ||
|
|
3217125dd3 | ||
|
|
00032854eb | ||
|
|
d99ddf6816 | ||
|
|
32c6e32e04 | ||
|
|
c55b1f3359 | ||
|
|
fabeae4180 | ||
|
|
5eed50961d | ||
|
|
bed0976eb9 | ||
|
|
70d92d9b07 | ||
|
|
fb4ba31504 | ||
|
|
152c5c71b1 | ||
|
|
fa369365a5 | ||
|
|
7345366ba7 | ||
|
|
6151ea7128 | ||
|
|
440ab26b69 | ||
|
|
caadd72ab2 | ||
|
|
d59d543ec1 | ||
|
|
58d1109510 | ||
|
|
9c52e25a6a | ||
|
|
62a44c5a09 | ||
|
|
e5fa40e903 | ||
|
|
8177083992 | ||
|
|
4bfd8e9f61 | ||
|
|
81e7d27ec8 | ||
|
|
f3fc0b5ac2 | ||
|
|
fb7bca436b | ||
|
|
c19a0ceba2 | ||
|
|
1f26e452fc | ||
|
|
0af6b7ede9 | ||
|
|
6ad56075d6 | ||
|
|
b81ae6c894 | ||
|
|
546e9f1702 | ||
|
|
bb51c6aa42 | ||
|
|
78e8134ad5 | ||
|
|
bc2c36215d | ||
|
|
377547fa11 |
@@ -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
|
||||||
40
.github/workflows/bridge.yml
vendored
40
.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,14 +17,25 @@ 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
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -49,28 +59,28 @@ jobs:
|
|||||||
wget
|
wget
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
with:
|
with:
|
||||||
prefix-key: bridge-${{ matrix.job.os }}
|
prefix-key: bridge-${{ matrix.job.os }}
|
||||||
|
|
||||||
- name: Cache Bridge
|
- name: Cache Bridge
|
||||||
id: cache-bridge
|
id: cache-bridge
|
||||||
uses: actions/cache@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@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: |
|
||||||
@@ -86,9 +104,9 @@ jobs:
|
|||||||
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
|
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@master
|
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
|
||||||
|
|||||||
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -29,13 +29,13 @@ jobs:
|
|||||||
# name: Ensure 'cargo fmt' has been run
|
# name: Ensure 'cargo fmt' has been run
|
||||||
# runs-on: ubuntu-20.04
|
# runs-on: ubuntu-20.04
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions-rs/toolchain@v1
|
# - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1
|
||||||
# with:
|
# with:
|
||||||
# toolchain: stable
|
# toolchain: stable
|
||||||
# default: true
|
# default: true
|
||||||
# profile: minimal
|
# profile: minimal
|
||||||
# components: rustfmt
|
# components: rustfmt
|
||||||
# - uses: actions/checkout@v3
|
# - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
# - run: cargo fmt -- --check
|
# - run: cargo fmt -- --check
|
||||||
|
|
||||||
# min_version:
|
# min_version:
|
||||||
@@ -43,24 +43,24 @@ jobs:
|
|||||||
# runs-on: ubuntu-20.04
|
# runs-on: ubuntu-20.04
|
||||||
# steps:
|
# steps:
|
||||||
# - name: Checkout source code
|
# - name: Checkout source code
|
||||||
# uses: actions/checkout@v3
|
# uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
# with:
|
# with:
|
||||||
# submodules: recursive
|
# submodules: recursive
|
||||||
|
|
||||||
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
||||||
# uses: actions-rs/toolchain@v1
|
# uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1
|
||||||
# with:
|
# with:
|
||||||
# toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
|
# toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
|
||||||
# default: true
|
# default: true
|
||||||
# profile: minimal # minimal component installation (ie, no documentation)
|
# profile: minimal # minimal component installation (ie, no documentation)
|
||||||
# components: clippy
|
# components: clippy
|
||||||
# - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
|
# - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
|
||||||
# uses: actions-rs/cargo@v1
|
# uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
|
||||||
# with:
|
# with:
|
||||||
# command: clippy
|
# command: clippy
|
||||||
# args: --locked --all-targets --all-features -- --allow clippy::unknown_clippy_lints
|
# args: --locked --all-targets --all-features -- --allow clippy::unknown_clippy_lints
|
||||||
# - name: Run tests
|
# - name: Run tests
|
||||||
# uses: actions-rs/cargo@v1
|
# uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
|
||||||
# with:
|
# with:
|
||||||
# command: test
|
# command: test
|
||||||
# args: --locked
|
# args: --locked
|
||||||
@@ -81,14 +81,15 @@ 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:
|
||||||
- name: Free Disk Space (Ubuntu)
|
- name: Free Disk Space (Ubuntu)
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
# jlumbroso/free-disk-space@main is used in .github\workflows\flutter-build.yml
|
# jlumbroso/free-disk-space@v1.3.1 is used in .github\workflows\flutter-build.yml
|
||||||
# But pinning to a specific version to avoid unexpected issues is preferred.
|
# But pinning to a specific version to avoid unexpected issues is preferred.
|
||||||
uses: jlumbroso/free-disk-space@v1.3.1
|
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||||
with:
|
with:
|
||||||
tool-cache: false
|
tool-cache: false
|
||||||
android: true
|
android: true
|
||||||
@@ -99,14 +100,14 @@ jobs:
|
|||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
@@ -156,7 +157,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
@@ -172,10 +173,10 @@ jobs:
|
|||||||
cargo -V
|
cargo -V
|
||||||
rustc -V
|
rustc -V
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
|
||||||
with:
|
with:
|
||||||
use-cross: ${{ matrix.job.use-cross }}
|
use-cross: ${{ matrix.job.use-cross }}
|
||||||
command: build
|
command: build
|
||||||
@@ -243,7 +244,7 @@ jobs:
|
|||||||
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
|
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
|
||||||
with:
|
with:
|
||||||
use-cross: ${{ matrix.job.use-cross }}
|
use-cross: ${{ matrix.job.use-cross }}
|
||||||
command: test
|
command: test
|
||||||
|
|||||||
4
.github/workflows/clear-cache.yml
vendored
4
.github/workflows/clear-cache.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clear cache
|
- name: Clear cache
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
console.log("About to clear")
|
console.log("About to clear")
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
console.log("Clear completed")
|
console.log("Clear completed")
|
||||||
|
|
||||||
- name: Purge cache # Above seems not clear thouroughly, so add this to double clear
|
- name: Purge cache # Above seems not clear thouroughly, so add this to double clear
|
||||||
uses: MyAlbum/purge-cache@v2
|
uses: MyAlbum/purge-cache@881eb5957687193fa612bf74c0042adc78ea5e54 # v2
|
||||||
with:
|
with:
|
||||||
accessed: true # Purge caches by their last accessed time (default)
|
accessed: true # Purge caches by their last accessed time (default)
|
||||||
created: false # Purge caches by their created time (default)
|
created: false # Purge caches by their created time (default)
|
||||||
|
|||||||
2
.github/workflows/fdroid.yml
vendored
2
.github/workflows/fdroid.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Publish RustDesk version file
|
- name: Publish RustDesk version file
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: "fdroid-version"
|
tag_name: "fdroid-version"
|
||||||
|
|||||||
338
.github/workflows/flutter-build.yml
vendored
338
.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.6"
|
VERSION: "1.4.8"
|
||||||
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,68 +91,134 @@ 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@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
uses: actions/download-artifact@master
|
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@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
|
||||||
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
|
id: flutter
|
||||||
|
# arm64 builds with FLUTTER_WINDOWS_ARM_VERSION (>=3.44); x64 stays on FLUTTER_VERSION.
|
||||||
|
# subosito only ships an x64 Windows SDK (Flutter's release manifest lists x64 only),
|
||||||
|
# so it installs x64 on both arches. The arm64 runner re-bootstraps Dart to arm64 in
|
||||||
|
# the next step.
|
||||||
|
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
|
||||||
with:
|
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@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.SCITER_RUST_VERSION }}
|
toolchain: ${{ env.SCITER_RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
with:
|
with:
|
||||||
prefix-key: ${{ matrix.job.os }}
|
prefix-key: ${{ matrix.job.os }}
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: C:\vcpkg
|
vcpkgDirectory: C:\vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
@@ -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
|
||||||
@@ -220,15 +309,15 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download RustDeskTempTopMostWindow artifacts
|
- name: Download RustDeskTempTopMostWindow artifacts
|
||||||
uses: actions/download-artifact@master
|
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
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
||||||
path: rustdesk
|
path: rustdesk
|
||||||
@@ -253,16 +342,21 @@ jobs:
|
|||||||
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe
|
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe
|
||||||
|
|
||||||
- name: Add MSBuild to PATH
|
- name: Add MSBuild to PATH
|
||||||
uses: microsoft/setup-msbuild@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
|
||||||
@@ -272,7 +366,7 @@ jobs:
|
|||||||
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
|
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
|
||||||
|
|
||||||
- name: Publish Release
|
- name: Publish Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
@@ -302,35 +396,35 @@ jobs:
|
|||||||
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
|
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install LLVM and Clang
|
- name: Install LLVM and Clang
|
||||||
uses: rustdesk-org/install-llvm-action-32bit@master
|
uses: rustdesk-org/install-llvm-action-32bit@6aa7d9ad3df84dff01cd4596dd0fc880a7f47fce # no release tag; commit 2026-05-26
|
||||||
with:
|
with:
|
||||||
version: ${{ env.LLVM_VERSION }}
|
version: ${{ env.LLVM_VERSION }}
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
|
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
with:
|
with:
|
||||||
prefix-key: ${{ matrix.job.os }}-sciter
|
prefix-key: ${{ matrix.job.os }}-sciter
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: C:\vcpkg
|
vcpkgDirectory: C:\vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
@@ -363,7 +457,8 @@ jobs:
|
|||||||
python3 res/inline-sciter.py
|
python3 res/inline-sciter.py
|
||||||
# Patch sciter x86
|
# Patch sciter x86
|
||||||
sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml
|
sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml
|
||||||
cargo build --features inline,vram,hwcodec --release --bins
|
cargo update -p sciter-rs --precise 674e07d3066ca9a92ced3816203ab6b652629d1e
|
||||||
|
cargo build --locked --features inline,vram,hwcodec --release --bins
|
||||||
mkdir -p ./Release
|
mkdir -p ./Release
|
||||||
mv ./target/release/rustdesk.exe ./Release/rustdesk.exe
|
mv ./target/release/rustdesk.exe ./Release/rustdesk.exe
|
||||||
curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
|
curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
|
||||||
@@ -394,7 +489,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload unsigned
|
- name: Upload unsigned
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
|
||||||
path: Release
|
path: Release
|
||||||
@@ -424,7 +519,7 @@ jobs:
|
|||||||
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
|
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
|
||||||
|
|
||||||
- name: Publish Release
|
- name: Publish Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
@@ -449,7 +544,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
@@ -459,12 +554,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
brew install nasm yasm
|
brew install nasm yasm
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
@@ -475,7 +570,7 @@ jobs:
|
|||||||
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
doNotCache: false
|
doNotCache: false
|
||||||
@@ -499,19 +594,19 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
with:
|
with:
|
||||||
prefix-key: rustdesk-lib-cache-ios
|
prefix-key: rustdesk-lib-cache-ios
|
||||||
key: ${{ matrix.job.target }}
|
key: ${{ matrix.job.target }}
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
@@ -519,10 +614,10 @@ jobs:
|
|||||||
- name: Build rustdesk lib
|
- name: Build rustdesk lib
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.job.target }}
|
rustup target add ${{ matrix.job.target }}
|
||||||
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||||
|
|
||||||
- name: Upload liblibrustdesk.a Artifacts
|
- name: Upload liblibrustdesk.a Artifacts
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: liblibrustdesk.a
|
name: liblibrustdesk.a
|
||||||
path: target/aarch64-apple-ios/release/liblibrustdesk.a
|
path: target/aarch64-apple-ios/release/liblibrustdesk.a
|
||||||
@@ -537,14 +632,14 @@ jobs:
|
|||||||
|
|
||||||
# - name: Upload Artifacts
|
# - name: Upload Artifacts
|
||||||
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
# uses: actions/upload-artifact@master
|
# uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
# with:
|
# with:
|
||||||
# name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
# name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
# path: flutter/build/ios/ipa/*.ipa
|
# path: flutter/build/ios/ipa/*.ipa
|
||||||
|
|
||||||
# - name: Publish ipa package
|
# - name: Publish ipa package
|
||||||
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
# uses: softprops/action-gh-release@v1
|
# uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
# with:
|
# with:
|
||||||
# prerelease: true
|
# prerelease: true
|
||||||
# tag_name: ${{ env.TAG_NAME }}
|
# tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -577,20 +672,20 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Import the codesign cert
|
- name: Import the codesign cert
|
||||||
if: env.MACOS_P12_BASE64 != null
|
if: env.MACOS_P12_BASE64 != null
|
||||||
uses: apple-actions/import-codesign-certs@v1
|
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
||||||
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
||||||
@@ -604,7 +699,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Import notarize key
|
- name: Import notarize key
|
||||||
if: env.MACOS_P12_BASE64 != null
|
if: env.MACOS_P12_BASE64 != null
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2
|
||||||
with:
|
with:
|
||||||
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
||||||
fileName: rustdesk.json
|
fileName: rustdesk.json
|
||||||
@@ -643,7 +738,7 @@ jobs:
|
|||||||
nasm --version
|
nasm --version
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
@@ -662,24 +757,24 @@ jobs:
|
|||||||
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
|
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.MAC_RUST_VERSION }}
|
toolchain: ${{ env.MAC_RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
with:
|
with:
|
||||||
prefix-key: ${{ matrix.job.os }}
|
prefix-key: ${{ matrix.job.os }}
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
doNotCache: false
|
doNotCache: false
|
||||||
@@ -731,7 +826,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload unsigned macOS app
|
- name: Upload unsigned macOS app
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-macos-${{ matrix.job.arch }}
|
name: rustdesk-unsigned-macos-${{ matrix.job.arch }}
|
||||||
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed
|
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed
|
||||||
@@ -763,7 +858,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish DMG package
|
- name: Publish DMG package
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -779,25 +874,25 @@ jobs:
|
|||||||
if: ${{ inputs.upload-artifact }}
|
if: ${{ inputs.upload-artifact }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-macos-x86_64
|
name: rustdesk-unsigned-macos-x86_64
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-macos-aarch64
|
name: rustdesk-unsigned-macos-aarch64
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-windows-x86_64
|
name: rustdesk-unsigned-windows-x86_64
|
||||||
path: ./windows-x86_64/
|
path: ./windows-x86_64/
|
||||||
|
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-unsigned-windows-x86
|
name: rustdesk-unsigned-windows-x86
|
||||||
path: ./windows-x86/
|
path: ./windows-x86/
|
||||||
@@ -807,7 +902,7 @@ jobs:
|
|||||||
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
|
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
|
||||||
|
|
||||||
- name: Publish unsigned app
|
- name: Publish unsigned app
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -844,7 +939,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Free Disk Space (Ubuntu)
|
- name: Free Disk Space (Ubuntu)
|
||||||
uses: jlumbroso/free-disk-space@main
|
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||||
with:
|
with:
|
||||||
tool-cache: false
|
tool-cache: false
|
||||||
android: false
|
android: false
|
||||||
@@ -855,7 +950,7 @@ jobs:
|
|||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
@@ -897,12 +992,12 @@ jobs:
|
|||||||
wget
|
wget
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
||||||
@@ -912,14 +1007,14 @@ jobs:
|
|||||||
cd $(dirname $(dirname $(which flutter)))
|
cd $(dirname $(dirname $(which flutter)))
|
||||||
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
|
||||||
- uses: nttld/setup-ndk@v1
|
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
with:
|
with:
|
||||||
ndk-version: ${{ env.NDK_VERSION }}
|
ndk-version: ${{ env.NDK_VERSION }}
|
||||||
add-to-path: true
|
add-to-path: true
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
@@ -954,18 +1049,18 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
with:
|
with:
|
||||||
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
|
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
|
||||||
key: ${{ matrix.job.target }}
|
key: ${{ matrix.job.target }}
|
||||||
@@ -1001,7 +1096,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Upload Rustdesk library to Artifacts
|
- name: Upload Rustdesk library to Artifacts
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: librustdesk.so.${{ matrix.job.target }}
|
name: librustdesk.so.${{ matrix.job.target }}
|
||||||
path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so
|
path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so
|
||||||
@@ -1066,7 +1161,7 @@ jobs:
|
|||||||
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||||
echo Last build tool version is: $BUILD_TOOL_VERSION
|
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||||
|
|
||||||
- uses: r0adkll/sign-android-release@v1
|
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||||
name: Sign app APK
|
name: Sign app APK
|
||||||
if: env.ANDROID_SIGNING_KEY != null
|
if: env.ANDROID_SIGNING_KEY != null
|
||||||
id: sign-rustdesk
|
id: sign-rustdesk
|
||||||
@@ -1082,14 +1177,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||||
|
|
||||||
- name: Publish signed apk package
|
- name: Publish signed apk package
|
||||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1098,7 +1193,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish unsigned apk package
|
- name: Publish unsigned apk package
|
||||||
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
|
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1116,7 +1211,7 @@ jobs:
|
|||||||
suffix: ""
|
suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Free Disk Space (Ubuntu)
|
- name: Free Disk Space (Ubuntu)
|
||||||
uses: jlumbroso/free-disk-space@main
|
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
|
||||||
with:
|
with:
|
||||||
tool-cache: false
|
tool-cache: false
|
||||||
android: false
|
android: false
|
||||||
@@ -1127,7 +1222,7 @@ jobs:
|
|||||||
swap-storage: false
|
swap-storage: false
|
||||||
|
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
@@ -1169,12 +1264,12 @@ jobs:
|
|||||||
wget
|
wget
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
|
||||||
@@ -1185,32 +1280,32 @@ jobs:
|
|||||||
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
|
||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Download Rustdesk library from Artifacts
|
- name: Download Rustdesk library from Artifacts
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: librustdesk.so.aarch64-linux-android
|
name: librustdesk.so.aarch64-linux-android
|
||||||
path: ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
path: ./flutter/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
|
||||||
- name: Download Rustdesk library from Artifacts
|
- name: Download Rustdesk library from Artifacts
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: librustdesk.so.armv7-linux-androideabi
|
name: librustdesk.so.armv7-linux-androideabi
|
||||||
path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a
|
||||||
|
|
||||||
- name: Download Rustdesk library from Artifacts
|
- name: Download Rustdesk library from Artifacts
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: librustdesk.so.x86_64-linux-android
|
name: librustdesk.so.x86_64-linux-android
|
||||||
path: ./flutter/android/app/src/main/jniLibs/x86_64
|
path: ./flutter/android/app/src/main/jniLibs/x86_64
|
||||||
|
|
||||||
- name: Download Rustdesk library from Artifacts
|
- name: Download Rustdesk library from Artifacts
|
||||||
if: ${{ env.reltype == 'debug' }}
|
if: ${{ env.reltype == 'debug' }}
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: librustdesk.so.i686-linux-android
|
name: librustdesk.so.i686-linux-android
|
||||||
path: ./flutter/android/app/src/main/jniLibs/x86
|
path: ./flutter/android/app/src/main/jniLibs/x86
|
||||||
@@ -1250,7 +1345,7 @@ jobs:
|
|||||||
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||||
echo Last build tool version is: $BUILD_TOOL_VERSION
|
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||||
|
|
||||||
- uses: r0adkll/sign-android-release@v1
|
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||||
name: Sign app APK
|
name: Sign app APK
|
||||||
if: env.ANDROID_SIGNING_KEY != null
|
if: env.ANDROID_SIGNING_KEY != null
|
||||||
id: sign-rustdesk
|
id: sign-rustdesk
|
||||||
@@ -1266,14 +1361,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
|
||||||
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
|
||||||
|
|
||||||
- name: Publish signed apk package
|
- name: Publish signed apk package
|
||||||
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1282,7 +1377,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish unsigned apk package
|
- name: Publish unsigned apk package
|
||||||
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
|
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1316,7 +1411,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
@@ -1334,13 +1429,13 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set Swap Space
|
- name: Set Swap Space
|
||||||
if: ${{ matrix.job.arch == 'x86_64' }}
|
if: ${{ matrix.job.arch == 'x86_64' }}
|
||||||
uses: pierotofy/set-swap-space@master
|
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
|
||||||
with:
|
with:
|
||||||
swap-size-gb: 12
|
swap-size-gb: 12
|
||||||
|
|
||||||
@@ -1350,7 +1445,7 @@ jobs:
|
|||||||
free -m
|
free -m
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
@@ -1369,14 +1464,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
@@ -1404,12 +1499,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Restore bridge files
|
- name: Restore bridge files
|
||||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: bridge-artifact
|
name: bridge-artifact
|
||||||
path: ./
|
path: ./
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
|
||||||
name: Build rustdesk
|
name: Build rustdesk
|
||||||
id: vcpkg
|
id: vcpkg
|
||||||
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
|
||||||
@@ -1491,7 +1586,7 @@ jobs:
|
|||||||
export JOBS=""
|
export JOBS=""
|
||||||
fi
|
fi
|
||||||
echo $JOBS
|
echo $JOBS
|
||||||
cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
|
cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
|
||||||
rm -rf target/release/deps target/release/build
|
rm -rf target/release/deps target/release/build
|
||||||
rm -rf ~/.cargo
|
rm -rf ~/.cargo
|
||||||
|
|
||||||
@@ -1583,7 +1678,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish debian/rpm package
|
- name: Publish debian/rpm package
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1592,7 +1687,7 @@ jobs:
|
|||||||
rustdesk-*.rpm
|
rustdesk-*.rpm
|
||||||
|
|
||||||
- name: Upload deb
|
- name: Upload deb
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||||
@@ -1611,7 +1706,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build archlinux package
|
- name: Build archlinux package
|
||||||
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: rustdesk-org/arch-makepkg-action@master
|
uses: rustdesk-org/arch-makepkg-action@04200739ed1d0bf6f2188b6736b26a767c57a7f9 # no release tag; commit 2026-05-26
|
||||||
with:
|
with:
|
||||||
packages:
|
packages:
|
||||||
scripts: |
|
scripts: |
|
||||||
@@ -1619,7 +1714,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish archlinux package
|
- name: Publish archlinux package
|
||||||
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
|
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1657,14 +1752,14 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -1682,7 +1777,7 @@ jobs:
|
|||||||
free -m
|
free -m
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.SCITER_RUST_VERSION }}
|
toolchain: ${{ env.SCITER_RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
@@ -1693,7 +1788,7 @@ jobs:
|
|||||||
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
|
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
|
||||||
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
|
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
|
||||||
name: Build rustdesk sciter binary for ${{ matrix.job.arch }}
|
name: Build rustdesk sciter binary for ${{ matrix.job.arch }}
|
||||||
id: vcpkg
|
id: vcpkg
|
||||||
with:
|
with:
|
||||||
@@ -1821,7 +1916,7 @@ jobs:
|
|||||||
# build rustdesk
|
# build rustdesk
|
||||||
python3 ./res/inline-sciter.py
|
python3 ./res/inline-sciter.py
|
||||||
export CARGO_INCREMENTAL=0
|
export CARGO_INCREMENTAL=0
|
||||||
cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
|
cargo build --locked --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
|
||||||
# make debian package
|
# make debian package
|
||||||
mkdir -p ./Release
|
mkdir -p ./Release
|
||||||
mv ./target/release/rustdesk ./Release/rustdesk
|
mv ./target/release/rustdesk ./Release/rustdesk
|
||||||
@@ -1839,7 +1934,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish debian package
|
- name: Publish debian package
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1847,7 +1942,7 @@ jobs:
|
|||||||
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
|
|
||||||
- name: Upload deb
|
- name: Upload deb
|
||||||
uses: actions/upload-artifact@master
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
|
||||||
@@ -1866,12 +1961,12 @@ jobs:
|
|||||||
- { target: aarch64-unknown-linux-gnu, arch: aarch64 }
|
- { target: aarch64-unknown-linux-gnu, arch: aarch64 }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Download Binary
|
- name: Download Binary
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||||
path: .
|
path: .
|
||||||
@@ -1889,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
|
||||||
@@ -1896,7 +1992,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish appimage package
|
- name: Publish appimage package
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -1939,12 +2035,12 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Download Binary
|
- name: Download Binary
|
||||||
uses: actions/download-artifact@master
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
|
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
|
||||||
path: .
|
path: .
|
||||||
@@ -1953,7 +2049,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
|
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
|
||||||
|
|
||||||
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
|
||||||
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
||||||
id: flatpak
|
id: flatpak
|
||||||
with:
|
with:
|
||||||
@@ -1981,7 +2077,7 @@ jobs:
|
|||||||
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
|
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
|
||||||
|
|
||||||
- name: Publish flatpak package
|
- name: Publish flatpak package
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -2000,7 +2096,7 @@ jobs:
|
|||||||
RELEASE_NAME: web-basic
|
RELEASE_NAME: web-basic
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
@@ -2010,7 +2106,7 @@ jobs:
|
|||||||
sudo apt-get install -y wget npm
|
sudo apt-get install -y wget npm
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@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: ${{ env.FLUTTER_VERSION }}
|
||||||
@@ -2054,7 +2150,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish web
|
- name: Publish web
|
||||||
if: env.UPLOAD_ARTIFACT == 'true'
|
if: env.UPLOAD_ARTIFACT == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
|||||||
36
.github/workflows/playground.yml
vendored
36
.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.6"
|
VERSION: "1.4.8"
|
||||||
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 }}"
|
||||||
@@ -79,21 +79,21 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Export GitHub Actions cache environment variables
|
- name: Export GitHub Actions cache environment variables
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||||
|
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.job.ref }}
|
ref: ${{ matrix.job.ref }}
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Import the codesign cert
|
- name: Import the codesign cert
|
||||||
if: env.MACOS_P12_BASE64 != null
|
if: env.MACOS_P12_BASE64 != null
|
||||||
uses: apple-actions/import-codesign-certs@v1
|
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
|
||||||
with:
|
with:
|
||||||
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
||||||
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Import notarize key
|
- name: Import notarize key
|
||||||
if: env.MACOS_P12_BASE64 != null
|
if: env.MACOS_P12_BASE64 != null
|
||||||
uses: timheuer/base64-to-file@v1.2
|
uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2
|
||||||
with:
|
with:
|
||||||
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
|
||||||
fileName: rustdesk.json
|
fileName: rustdesk.json
|
||||||
@@ -129,19 +129,19 @@ jobs:
|
|||||||
brew install llvm create-dmg nasm pkg-config
|
brew install llvm create-dmg nasm pkg-config
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ matrix.job.flutter }}
|
flutter-version: ${{ matrix.job.flutter }}
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||||
with:
|
with:
|
||||||
prefix-key: ${{ matrix.job.os }}
|
prefix-key: ${{ matrix.job.os }}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ jobs:
|
|||||||
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
|
||||||
|
|
||||||
- name: Restore from cache and install vcpkg
|
- name: Restore from cache and install vcpkg
|
||||||
uses: lukka/run-vcpkg@v7
|
uses: lukka/run-vcpkg@8a5116de2b552d6fc8894e9774aacaf2e5db4823 # v7 2026-05-26
|
||||||
if: false
|
if: false
|
||||||
with:
|
with:
|
||||||
setupOnly: true
|
setupOnly: true
|
||||||
@@ -222,7 +222,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Publish DMG package
|
- name: Publish DMG package
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
@@ -247,7 +247,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ matrix.job.ref }}
|
ref: ${{ matrix.job.ref }}
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
@@ -290,13 +290,13 @@ jobs:
|
|||||||
wget
|
wget
|
||||||
|
|
||||||
- name: Install flutter
|
- name: Install flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ env.RUST_VERSION }}
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
components: "rustfmt"
|
components: "rustfmt"
|
||||||
@@ -310,14 +310,14 @@ jobs:
|
|||||||
pushd flutter ; flutter pub get ; popd
|
pushd flutter ; flutter pub get ; popd
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||||
|
|
||||||
- uses: nttld/setup-ndk@v1
|
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
with:
|
with:
|
||||||
ndk-version: ${{ env.NDK_VERSION }}
|
ndk-version: ${{ env.NDK_VERSION }}
|
||||||
add-to-path: true
|
add-to-path: true
|
||||||
|
|
||||||
- name: Setup vcpkg with Github Actions binary cache
|
- name: Setup vcpkg with Github Actions binary cache
|
||||||
uses: lukka/run-vcpkg@v11
|
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
|
||||||
with:
|
with:
|
||||||
vcpkgDirectory: /opt/artifacts/vcpkg
|
vcpkgDirectory: /opt/artifacts/vcpkg
|
||||||
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
|
||||||
@@ -395,7 +395,7 @@ jobs:
|
|||||||
mkdir -p signed-apk; pushd signed-apk
|
mkdir -p signed-apk; pushd signed-apk
|
||||||
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
|
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
|
||||||
|
|
||||||
- uses: r0adkll/sign-android-release@v1
|
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
|
||||||
name: Sign app APK
|
name: Sign app APK
|
||||||
if: env.ANDROID_SIGNING_KEY != null
|
if: env.ANDROID_SIGNING_KEY != null
|
||||||
id: sign-rustdesk
|
id: sign-rustdesk
|
||||||
@@ -410,7 +410,7 @@ jobs:
|
|||||||
BUILD_TOOLS_VERSION: "30.0.2"
|
BUILD_TOOLS_VERSION: "30.0.2"
|
||||||
|
|
||||||
- name: Publish signed apk package
|
- name: Publish signed apk package
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tag_name: ${{ env.TAG_NAME }}
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
|||||||
@@ -39,22 +39,21 @@ jobs:
|
|||||||
build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }}
|
build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }}
|
||||||
steps:
|
steps:
|
||||||
- name: Add MSBuild to PATH
|
- name: Add MSBuild to PATH
|
||||||
uses: microsoft/setup-msbuild@v2
|
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
|
||||||
|
|
||||||
- name: Download the source code
|
- name: Download the source code
|
||||||
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@master
|
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
Normal file
85
.github/workflows/wf-cliprdr-ci.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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
|
||||||
24
AGENTS.md
24
AGENTS.md
@@ -60,3 +60,27 @@
|
|||||||
* Do not refactor unrelated code.
|
* Do not refactor unrelated code.
|
||||||
* Do not make formatting-only changes.
|
* Do not make formatting-only changes.
|
||||||
* Keep naming/style consistent with nearby code.
|
* Keep naming/style consistent with nearby code.
|
||||||
|
|
||||||
|
## Localization (`src/lang/*.rs`)
|
||||||
|
|
||||||
|
Each file is a `HashMap<key, translation>`. Layout:
|
||||||
|
|
||||||
|
* `template.rs` is the master list of every key. **Never edit it** as part of translation work.
|
||||||
|
* `en.rs` holds only the keys whose English display text differs from the key itself.
|
||||||
|
* Every other file (`de.rs`, `fr.rs`, …) carries the full key set; an untranslated entry has an empty value: `("key", "")`.
|
||||||
|
|
||||||
|
### Finding the English source for a key
|
||||||
|
|
||||||
|
When filling an empty entry, determine the source English text with this rule:
|
||||||
|
|
||||||
|
* If `key` exists in `en.rs` **with a non-empty value**, that value is the source text (look it up in `en.rs`).
|
||||||
|
* Otherwise the **key string itself is the source text** (the key is already plain English).
|
||||||
|
|
||||||
|
Then translate that source into the file's target language (infer the language from the file's existing non-empty entries / filename).
|
||||||
|
|
||||||
|
### Translation hygiene
|
||||||
|
|
||||||
|
* Only fill empty values. Never change keys, and never touch existing non-empty translations.
|
||||||
|
* Preserve placeholders (`{}`) and escape sequences (`\n`, `\"`) exactly as in the source.
|
||||||
|
* Do not translate brand or technical tokens: `RustDesk`, `Socks5`, `TLS`, `UAC`, `Wayland`, `X11`, `TCP`, `UDP`, `2FA`, `RDP`, `D3D`, etc.
|
||||||
|
* Copy URL values (e.g. `doc_*` keys) verbatim from `en.rs`.
|
||||||
|
|||||||
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -292,7 +292,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
source = "git+https://github.com/rustdesk-org/arboard#85be1218668ff218a7b170c9d424fde73e069914"
|
source = "git+https://github.com/rustdesk-org/arboard#c7d5781f563176df9efd8df6287e823fb1b9bed5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clipboard-win",
|
"clipboard-win",
|
||||||
"core-graphics 0.23.2",
|
"core-graphics 0.23.2",
|
||||||
@@ -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",
|
||||||
@@ -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",
|
||||||
@@ -7270,7 +7270,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.6"
|
version = "1.4.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7385,7 +7385,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.6"
|
version = "1.4.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -7457,7 +7457,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 +7514,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 +9733,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 +10838,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.6"
|
version = "1.4.8"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -47,7 +47,7 @@ screencapturekit = ["cpal/screencapturekit"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
scrap = { path = "libs/scrap", features = ["wayland"] }
|
scrap = { path = "libs/scrap", features = ["wayland"] }
|
||||||
hbb_common = { path = "libs/hbb_common", features = ["webrtc"] }
|
hbb_common = { path = "libs/hbb_common" }
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@@ -213,7 +213,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.6
|
version: 1.4.8
|
||||||
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.6
|
version: 1.4.8
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
35
build.py
35
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:
|
||||||
@@ -172,7 +173,7 @@ def generate_build_script_for_docker():
|
|||||||
# flutter_rust_bridge
|
# flutter_rust_bridge
|
||||||
dart pub global activate ffigen --version 5.0.1
|
dart pub global activate ffigen --version 5.0.1
|
||||||
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
|
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
|
||||||
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
|
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . --locked && popd
|
||||||
pushd flutter && flutter pub get && popd
|
pushd flutter && flutter pub get && popd
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
|
||||||
# install vcpkg
|
# install vcpkg
|
||||||
@@ -299,7 +300,7 @@ Version: %s
|
|||||||
Architecture: %s
|
Architecture: %s
|
||||||
Maintainer: rustdesk <info@rustdesk.com>
|
Maintainer: rustdesk <info@rustdesk.com>
|
||||||
Homepage: https://rustdesk.com
|
Homepage: https://rustdesk.com
|
||||||
Depends: libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
|
||||||
Recommends: libayatana-appindicator3-1
|
Recommends: libayatana-appindicator3-1
|
||||||
Description: A remote control software.
|
Description: A remote control software.
|
||||||
|
|
||||||
@@ -317,7 +318,7 @@ def ffi_bindgen_function_refactor():
|
|||||||
|
|
||||||
def build_flutter_deb(version, features):
|
def build_flutter_deb(version, features):
|
||||||
if not skip_cargo:
|
if not skip_cargo:
|
||||||
system2(f'cargo build --features {features} --lib --release')
|
system2(f'cargo build --locked --features {features} --lib --release')
|
||||||
ffi_bindgen_function_refactor()
|
ffi_bindgen_function_refactor()
|
||||||
os.chdir('flutter')
|
os.chdir('flutter')
|
||||||
system2('flutter build linux --release')
|
system2('flutter build linux --release')
|
||||||
@@ -405,12 +406,17 @@ def build_flutter_dmg(version, features):
|
|||||||
if not skip_cargo:
|
if not skip_cargo:
|
||||||
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
||||||
system2(
|
system2(
|
||||||
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
|
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --locked --features {features} --release')
|
||||||
# copy dylib
|
# copy dylib
|
||||||
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(
|
||||||
@@ -422,7 +428,7 @@ def build_flutter_dmg(version, features):
|
|||||||
|
|
||||||
def build_flutter_arch_manjaro(version, features):
|
def build_flutter_arch_manjaro(version, features):
|
||||||
if not skip_cargo:
|
if not skip_cargo:
|
||||||
system2(f'cargo build --features {features} --lib --release')
|
system2(f'cargo build --locked --features {features} --lib --release')
|
||||||
ffi_bindgen_function_refactor()
|
ffi_bindgen_function_refactor()
|
||||||
os.chdir('flutter')
|
os.chdir('flutter')
|
||||||
system2('flutter build linux --release')
|
system2('flutter build linux --release')
|
||||||
@@ -433,7 +439,7 @@ def build_flutter_arch_manjaro(version, features):
|
|||||||
|
|
||||||
def build_flutter_windows(version, features, skip_portable_pack):
|
def build_flutter_windows(version, features, skip_portable_pack):
|
||||||
if not skip_cargo:
|
if not skip_cargo:
|
||||||
system2(f'cargo build --features {features} --lib --release')
|
system2(f'cargo build --locked --features {features} --lib --release')
|
||||||
if not os.path.exists("target/release/librustdesk.dll"):
|
if not os.path.exists("target/release/librustdesk.dll"):
|
||||||
print("cargo build failed, please check rust source code.")
|
print("cargo build failed, please check rust source code.")
|
||||||
exit(-1)
|
exit(-1)
|
||||||
@@ -489,13 +495,13 @@ def main():
|
|||||||
if windows:
|
if windows:
|
||||||
# build virtual display dynamic library
|
# build virtual display dynamic library
|
||||||
os.chdir('libs/virtual_display/dylib')
|
os.chdir('libs/virtual_display/dylib')
|
||||||
system2('cargo build --release')
|
system2('cargo build --locked --release')
|
||||||
os.chdir('../../..')
|
os.chdir('../../..')
|
||||||
|
|
||||||
if flutter:
|
if flutter:
|
||||||
build_flutter_windows(version, features, args.skip_portable_pack)
|
build_flutter_windows(version, features, args.skip_portable_pack)
|
||||||
return
|
return
|
||||||
system2('cargo build --release --features ' + features)
|
system2('cargo build --locked --release --features ' + features)
|
||||||
# system2('upx.exe target/release/rustdesk.exe')
|
# system2('upx.exe target/release/rustdesk.exe')
|
||||||
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
|
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
|
||||||
pa = os.environ.get('P')
|
pa = os.environ.get('P')
|
||||||
@@ -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')
|
||||||
@@ -519,7 +526,7 @@ def main():
|
|||||||
if flutter:
|
if flutter:
|
||||||
build_flutter_arch_manjaro(version, features)
|
build_flutter_arch_manjaro(version, features)
|
||||||
else:
|
else:
|
||||||
system2('cargo build --release --features ' + features)
|
system2('cargo build --locked --release --features ' + features)
|
||||||
system2('git checkout src/ui/common.tis')
|
system2('git checkout src/ui/common.tis')
|
||||||
system2('strip target/release/rustdesk')
|
system2('strip target/release/rustdesk')
|
||||||
system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
|
system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
|
||||||
@@ -528,7 +535,7 @@ def main():
|
|||||||
version, version))
|
version, version))
|
||||||
# pacman -U ./rustdesk.pkg.tar.zst
|
# pacman -U ./rustdesk.pkg.tar.zst
|
||||||
elif os.path.isfile('/usr/bin/yum'):
|
elif os.path.isfile('/usr/bin/yum'):
|
||||||
system2('cargo build --release --features ' + features)
|
system2('cargo build --locked --release --features ' + features)
|
||||||
system2('strip target/release/rustdesk')
|
system2('strip target/release/rustdesk')
|
||||||
system2(
|
system2(
|
||||||
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
|
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
|
||||||
@@ -538,7 +545,7 @@ def main():
|
|||||||
version, version))
|
version, version))
|
||||||
# yum localinstall rustdesk.rpm
|
# yum localinstall rustdesk.rpm
|
||||||
elif os.path.isfile('/usr/bin/zypper'):
|
elif os.path.isfile('/usr/bin/zypper'):
|
||||||
system2('cargo build --release --features ' + features)
|
system2('cargo build --locked --release --features ' + features)
|
||||||
system2('strip target/release/rustdesk')
|
system2('strip target/release/rustdesk')
|
||||||
system2(
|
system2(
|
||||||
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
|
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
|
||||||
@@ -557,7 +564,7 @@ def main():
|
|||||||
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
|
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
|
||||||
build_flutter_deb(version, features)
|
build_flutter_deb(version, features)
|
||||||
else:
|
else:
|
||||||
system2('cargo bundle --release --features ' + features)
|
system2('cargo --locked bundle --release --features ' + features)
|
||||||
if osx:
|
if osx:
|
||||||
system2(
|
system2(
|
||||||
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
|
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||||
<a href="#빌드를 위한 원시 단계">빌드</a> •
|
<a href="#빌드를_위한_원시_단계">빌드</a> •
|
||||||
<a href="#Docker로 빌드하는 방법">Docker</a> •
|
<a href="#Docker로_빌드하는_방법">Docker</a> •
|
||||||
<a href="#파일 구조">구조</a> •
|
<a href="#파일_구조">구조</a> •
|
||||||
<a href="#스크린샷">스냇샷</a><br>
|
<a href="#스크린샷">스냅샷</a><br>
|
||||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>]<br>
|
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>] | [<a href="README-RO.md">Română</a>]<br>
|
||||||
<b>이 README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> 및 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk 문서</a>를 귀하의 모국어로 번역하는 데 도움이 필요합니다</b>
|
<b>이 README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> 및 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk 문서</a>를 귀하의 모국어로 번역하는 데 도움이 필요합니다</b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -46,9 +46,9 @@ Sciter 동적 라이브러리를 직접 다운로드하세요.
|
|||||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||||
|
|
||||||
## 빌드를 위한 원시 단계
|
## 빌드를_위한_원시_단계
|
||||||
|
|
||||||
- Rust 개발 환경과 C++ 빌드 환경을 준비합니다
|
- Rust 개발 환경과 C++ 빌드 환경 준비
|
||||||
|
|
||||||
- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다
|
- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ mv libsciter-gtk.so target/debug
|
|||||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker로 빌드하는 방법
|
## Docker로_빌드하는_방법
|
||||||
|
|
||||||
먼저 리포지토리를 복제하고 Docker 컨테이너를 빌드합니다:
|
먼저 리포지토리를 복제하고 Docker 컨테이너를 빌드합니다:
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ target/release/rustdesk
|
|||||||
|
|
||||||
RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는지 확인하세요. 그렇지 않으면 응용 프로그램이 필요한 리소스를 찾지 못할 수 있습니다. 또한 `install` 또는 `run` 과 같은 다른 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방법을 통해 지원되지 않는다는 점에 유의하세요.
|
RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는지 확인하세요. 그렇지 않으면 응용 프로그램이 필요한 리소스를 찾지 못할 수 있습니다. 또한 `install` 또는 `run` 과 같은 다른 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방법을 통해 지원되지 않는다는 점에 유의하세요.
|
||||||
|
|
||||||
## 파일 구조
|
## 파일_구조
|
||||||
|
|
||||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 구성, tcp/udp wrapper, protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수
|
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 구성, tcp/udp wrapper, protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수
|
||||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡쳐
|
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡쳐
|
||||||
|
|||||||
@@ -1,55 +1,82 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Seu desktop remoto"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - Seu desktop remoto"><br>
|
||||||
<a href="#servidores-públicos-grátis">Servidores</a> •
|
<a href="#compilar">Compilar</a> •
|
||||||
<a href="#compilação-crua">Compilar</a> •
|
<a href="#como-compilar-com-o-docker">Docker</a> •
|
||||||
<a href="#como-compilar-com-docker">Docker</a> •
|
|
||||||
<a href="#estrutura-de-arquivos">Estrutura</a> •
|
<a href="#estrutura-de-arquivos">Estrutura</a> •
|
||||||
<a href="#screenshots">Screenshots</a><br>
|
<a href="#capturas-de-tela">Capturas de Tela</a><br>
|
||||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
[<a href="../README.md">Inglês</a>] | [<a href="docs/README-UA.md">Ucraniano</a>] | [<a href="docs/README-CS.md">Tcheco</a>] | [<a href="docs/README-ZH.md">Chinês</a>] | [<a href="docs/README-HU.md">Húngaro</a>] | [<a href="docs/README-ES.md">Espanhol</a>] | [<a href="docs/README-FA.md">Persa</a>] | [<a href="docs/README-FR.md">Francês</a>] | [<a href="docs/README-DE.md">Alemão</a>] | [<a href="docs/README-PL.md">Polonês</a>] | [<a href="docs/README-ID.md">Indonésio</a>] | [<a href="docs/README-FI.md">Finlandês</a>] | [<a href="docs/README-ML.md">Malaiala</a>] | [<a href="docs/README-JP.md">Japonês</a>] | [<a href="docs/README-NL.md">Holandês</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Russo</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">Coreano</a>] | [<a href="docs/README-AR.md">Árabe</a>] | [<a href="docs/README-VN.md">Vietnamita</a>] | [<a href="docs/README-DA.md">Dinamarquês</a>] | [<a href="docs/README-GR.md">Grego</a>] | [<a href="docs/README-TR.md">Turco</a>] | [<a href="docs/README-NO.md">Norueguês</a>] | [<a href="docs/README-RO.md">Romeno</a>]<br>
|
||||||
<b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b>
|
<b>Precisamos da sua ajuda para traduzir este README, a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">Interface do RustDesk</a> e a <a href="https://github.com/rustdesk/doc.rustdesk.com">Documentação do RustDesk</a> para o seu idioma nativo</b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
> [!Caution]
|
||||||
|
> **Aviso de Isenção de Responsabilidade por Uso Indevido:** <br>
|
||||||
|
> Os desenvolvedores do RustDesk não toleram ou apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, viola estritamente nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido do aplicativo.
|
||||||
|
|
||||||
|
|
||||||
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
|
||||||
|
|
||||||
[](https://rustdesk.com/pricing.html)
|
[](https://rustdesk.com/pricing.html)
|
||||||
|
|
||||||
Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
Mais uma solução de desktop remoto, escrita em Rust. Funciona imediatamente, sem necessidade de configuração. Você tem controle total dos seus dados, sem preocupações com segurança. Você pode usar nosso servidor de conexão/retransmissão (rendezvous/relay), [configurar o seu próprio](https://rustdesk.com/server) ou [escrever seu próprio servidor de conexão/retransmissão](https://github.com/rustdesk/rustdesk-server-demo).
|
||||||
|
|
||||||
RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ver como começar.
|

|
||||||
|
|
||||||
[**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
|
O RustDesk acolhe a contribuição de todos. Veja [CONTRIBUTING.md](docs/CONTRIBUTING.md) para ajuda em como começar.
|
||||||
|
|
||||||
|
[**Perguntas Frequentes (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||||
|
|
||||||
|
[**DOWNLOAD DOS BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
|
||||||
|
|
||||||
|
[**VERSÕES NIGHTLY (EM DESENVOLVIMENTO)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||||
|
|
||||||
|
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||||
|
alt="Baixe no F-Droid"
|
||||||
|
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
|
||||||
|
[<img src="https://flathub.org/api/badge?svg&locale=en"
|
||||||
|
alt="Baixe no Flathub"
|
||||||
|
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
|
||||||
|
|
||||||
## Dependências
|
## Dependências
|
||||||
|
|
||||||
Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria.
|
As versões de desktop usam Flutter ou Sciter (descontinuado) para a interface gráfica (GUI). Este tutorial é apenas para o Sciter, por ser mais fácil e amigável para começar. Verifique nosso [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) para instruções de compilação da versão em Flutter.
|
||||||
|
|
||||||
|
Por favor, faça o download da biblioteca dinâmica do Sciter por conta própria.
|
||||||
|
|
||||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||||
|
|
||||||
## Compilação crua
|
## Passos básicos para compilar
|
||||||
|
|
||||||
- Prepare seu ambiente de desenvolvimento Rust e ambiente de compilação C++
|
- Prepare seu ambiente de desenvolvimento Rust e o ambiente de compilação C++
|
||||||
|
|
||||||
- Instale [vcpkg](https://github.com/microsoft/vcpkg), e configure a variável de ambiente `VCPKG_ROOT` corretamente
|
- Instale o [vcpkg](https://github.com/microsoft/vcpkg) e configure a variável de ambiente `VCPKG_ROOT` corretamente
|
||||||
|
|
||||||
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
|
||||||
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
- Linux/macOS: `vcpkg install libvpx libyuv opus aom`
|
||||||
|
|
||||||
- Execute `cargo run`
|
- Execute `cargo run`
|
||||||
|
|
||||||
## Como compilar no Linux
|
## [Compilar](https://rustdesk.com/docs/en/dev/build/)
|
||||||
|
|
||||||
|
## Como Compilar no Linux
|
||||||
|
|
||||||
### Ubuntu 18 (Debian 10)
|
### Ubuntu 18 (Debian 10)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
|
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### openSUSE Tumbleweed
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fedora 28 (CentOS 8)
|
### Fedora 28 (CentOS 8)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
|
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arch (Manjaro)
|
### Arch (Manjaro)
|
||||||
@@ -58,7 +85,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
|
|||||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||||
```
|
```
|
||||||
|
|
||||||
### Instale vcpkg
|
### Instalar o vcpkg
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/microsoft/vcpkg
|
git clone https://github.com/microsoft/vcpkg
|
||||||
@@ -70,7 +97,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
|||||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||||
```
|
```
|
||||||
|
|
||||||
### Conserte libvpx (Para o Fedora)
|
### Corrigir o libvpx (Para Fedora)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd vcpkg/buildtrees/libvpx/src
|
cd vcpkg/buildtrees/libvpx/src
|
||||||
@@ -83,12 +110,12 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
|||||||
cd
|
cd
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile
|
### Compilar
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
source $HOME/.cargo/env
|
source $HOME/.cargo/env
|
||||||
git clone https://github.com/rustdesk/rustdesk
|
git clone --recurse-submodules https://github.com/rustdesk/rustdesk
|
||||||
cd rustdesk
|
cd rustdesk
|
||||||
mkdir -p target/debug
|
mkdir -p target/debug
|
||||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||||
@@ -96,57 +123,57 @@ mv libsciter-gtk.so target/debug
|
|||||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Como compilar com Docker
|
## Como compilar com o Docker
|
||||||
|
|
||||||
Comece clonando o repositório e montando o container docker:
|
Comece clonando o repositório e construindo o contêiner Docker:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/rustdesk/rustdesk
|
git clone https://github.com/rustdesk/rustdesk
|
||||||
cd rustdesk
|
cd rustdesk
|
||||||
|
git submodule update --init --recursive
|
||||||
docker build -t "rustdesk-builder" .
|
docker build -t "rustdesk-builder" .
|
||||||
```
|
```
|
||||||
|
|
||||||
Então, sempre que precisar compilar a aplicação, execute este comando:
|
Depois, cada vez que precisar compilar o aplicativo, execute o seguinte comando:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||||
```
|
```
|
||||||
|
|
||||||
Note que a primeira compilação pode demorar mais antes que as dependências sejam armazenadas em cache, as compilações subsequentes serão mais rápidas. Adicionalmente, se você precisar especificar argumentos diferentes para o comando de compilação, você pode fazê-lo ao final do comando na posição do `<OPTIONAL-ARGS>`. Por exemplo, se você gostaria de compilar uma versão de release otimizada, você executaria o comando acima seguido de `--release`. O executável gerado estará disponível no diretório alvo no seu sistema, e pode ser executado com:
|
Note que a primeira compilação pode demorar mais até que as dependências sejam armazenadas em cache; as compilações subsequentes serão mais rápidas. Além disso, se você precisar especificar argumentos diferentes para o comando de compilação, poderá fazê-lo ao final do comando na posição `<ARGUMENTOS-OPCIONAIS>`. Por exemplo, se você quiser compilar uma versão de lançamento (release) otimizada, executaria o comando acima seguido de `--release`. O executável resultante estará disponível na pasta `target` do seu sistema e pode ser executado com:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
target/debug/rustdesk
|
target/debug/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
Ou, se estiver rodando um executável de release:
|
Ou, se estiver executando o executável de lançamento:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
target/release/rustdesk
|
target/release/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
Por favor verifique que está executando estes comandos da raiz do repositório do RustDesk, senão a aplicação pode não encontrar os recursos necessários. Note também que outros subcomandos do cargo como `install` ou `run` não são suportados atualmente via este método, já que eles iriam instalar ou rodar o programa dentro do container ao invés do host.
|
Certifique-se de executar esses comandos a partir da raiz do repositório do RustDesk, do contrário o aplicativo pode não encontrar os recursos necessários. Note também que outros subcomandos do cargo, como `install` ou `run`, não são suportados atualmente por este método, pois instalariam ou executariam o programa dentro do contêiner em vez de no sistema hospedeiro.
|
||||||
|
|
||||||
## Estrutura de arquivos
|
## Estrutura de Arquivos
|
||||||
|
|
||||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de vídeo, configurações, wrapper de tcp/udp, protobuf, funções de sistema de arquivos para transferência de arquivos, e outras funções utilitárias
|
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de vídeo, configuração, encapsulador (wrapper) tcp/udp, protobuf, funções de sistema de arquivos para transferência de arquivos e algumas outras funções utilitárias.
|
||||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de tela
|
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de tela.
|
||||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controle de teclado/mouse específico a cada plataforma
|
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controle de teclado/mouse específico de cada plataforma.
|
||||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
|
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementação de copiar e colar arquivos para Windows, Linux e macOS.
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: serviços de áudio/área de transferência/entrada/vídeo, e conexões de rede
|
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interface Sciter antiga (descontinuada).
|
||||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar uma conexão "peer to peer"
|
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: serviços de áudio/área de transferência/entrada/vídeo e conexões de rede.
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed)
|
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inicia uma conexão direta (peer connection).
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
|
||||||
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
|
||||||
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
|
||||||
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript para o cliente web do Flutter.
|
||||||
|
|
||||||
> [!Cuidadob]
|
## Capturas de Tela
|
||||||
> **Aviso de uso indevido:** <br>
|
|
||||||
> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação.
|
|
||||||
|
|
||||||
## Screenshots
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ if [ -z $release ]; then
|
|||||||
fi
|
fi
|
||||||
set -f
|
set -f
|
||||||
#shellcheck disable=2086
|
#shellcheck disable=2086
|
||||||
VCPKG_ROOT=/vcpkg cargo build $argv
|
VCPKG_ROOT=/vcpkg cargo build --locked $argv
|
||||||
|
|||||||
@@ -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
|
||||||
|
Companion.flutterMethodChannel?.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
InputService.ctx = null
|
|
||||||
Companion.flutterMethodChannel?.invokeMethod(
|
|
||||||
"on_state_changed",
|
|
||||||
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 |
@@ -460,6 +460,7 @@ build)
|
|||||||
--target "${RUST_TARGET}" \
|
--target "${RUST_TARGET}" \
|
||||||
--bindgen \
|
--bindgen \
|
||||||
build \
|
build \
|
||||||
|
--locked \
|
||||||
--release \
|
--release \
|
||||||
--features "${RUSTDESK_FEATURES}"
|
--features "${RUSTDESK_FEATURES}"
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo build --features flutter --release --target x86_64-apple-ios --lib
|
cargo build --locked --features flutter --release --target x86_64-apple-ios --lib
|
||||||
|
|||||||
@@ -3713,14 +3713,54 @@ Widget loadPowered(BuildContext context) {
|
|||||||
).marginOnly(top: 6);
|
).marginOnly(top: 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// max 300 x 60
|
const _kDefaultLogoAsset = 'assets/logo.png';
|
||||||
Widget loadLogo() {
|
const _kLightLogoAsset = 'assets/logo_light.png';
|
||||||
return FutureBuilder<ByteData>(
|
const _kDarkLogoAsset = 'assets/logo_dark.png';
|
||||||
future: rootBundle.load('assets/logo.png'),
|
|
||||||
builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
|
List<String> _logoAssetCandidatesForBrightness(Brightness brightness) {
|
||||||
if (snapshot.hasData) {
|
return brightness == Brightness.dark
|
||||||
|
? [_kDarkLogoAsset, _kDefaultLogoAsset]
|
||||||
|
: [_kLightLogoAsset, _kDefaultLogoAsset];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _resolveLogoAsset(Brightness brightness) async {
|
||||||
|
for (final asset in _logoAssetCandidatesForBrightness(brightness)) {
|
||||||
|
try {
|
||||||
|
await rootBundle.load(asset);
|
||||||
|
return asset;
|
||||||
|
} on FlutterError {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Logo extends StatefulWidget {
|
||||||
|
const _Logo();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_Logo> createState() => _LogoState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LogoState extends State<_Logo> {
|
||||||
|
final Map<Brightness, Future<String?>> _logoFutures = {};
|
||||||
|
|
||||||
|
Future<String?> _logoFutureFor(Brightness brightness) {
|
||||||
|
return _logoFutures.putIfAbsent(
|
||||||
|
brightness,
|
||||||
|
() => _resolveLogoAsset(brightness),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<String?>(
|
||||||
|
future: _logoFutureFor(Theme.of(context).brightness),
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<String?> snapshot) {
|
||||||
|
final asset = snapshot.data;
|
||||||
|
if (asset != null) {
|
||||||
final image = Image.asset(
|
final image = Image.asset(
|
||||||
'assets/logo.png',
|
asset,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (ctx, error, stackTrace) {
|
errorBuilder: (ctx, error, stackTrace) {
|
||||||
return Container();
|
return Container();
|
||||||
@@ -3732,9 +3772,14 @@ Widget loadLogo() {
|
|||||||
).marginOnly(left: 12, right: 12, top: 12);
|
).marginOnly(left: 12, right: 12, top: 12);
|
||||||
}
|
}
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// max 300 x 60
|
||||||
|
Widget loadLogo() => const _Logo();
|
||||||
|
|
||||||
Widget loadIcon(double size) {
|
Widget loadIcon(double size) {
|
||||||
return Image.asset('assets/icon.png',
|
return Image.asset('assets/icon.png',
|
||||||
width: size,
|
width: size,
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
|
peers = mergeAutocompletePeers(
|
||||||
if (!combinedPeers.containsKey(p.id)) {
|
addressBookPeers: gFFI.abModel.allPeers(),
|
||||||
combinedPeers[p.id] = p.toJson();
|
groupPeers: gFFI.groupModel.peers,
|
||||||
}
|
lanPeers: gFFI.lanPeersModel.peers,
|
||||||
}
|
recentPeers: gFFI.recentPeersModel.peers,
|
||||||
|
restRecentPeerIds: gFFI.recentPeersModel.restPeerIds,
|
||||||
List<Peer> parsedPeers = [];
|
);
|
||||||
for (var peer in combinedPeers.values) {
|
_applyLastOnlineState(peers);
|
||||||
parsedPeers.add(Peer.fromJson(peer));
|
_scheduleSetState(() {
|
||||||
}
|
|
||||||
|
|
||||||
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}"))),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -532,9 +532,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
// Official
|
// Official
|
||||||
TapGestureRecognizer:
|
TapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
() => TapGestureRecognizer(
|
() => TapGestureRecognizer(), (instance) {
|
||||||
supportedDevices: kTouchBasedDeviceKinds,
|
|
||||||
), (instance) {
|
|
||||||
instance
|
instance
|
||||||
..onTapDown = onTapDown
|
..onTapDown = onTapDown
|
||||||
..onTapUp = onTapUp
|
..onTapUp = onTapUp
|
||||||
@@ -542,18 +540,14 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
}),
|
}),
|
||||||
DoubleTapGestureRecognizer:
|
DoubleTapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
||||||
() => DoubleTapGestureRecognizer(
|
() => DoubleTapGestureRecognizer(), (instance) {
|
||||||
supportedDevices: kTouchBasedDeviceKinds,
|
|
||||||
), (instance) {
|
|
||||||
instance
|
instance
|
||||||
..onDoubleTapDown = onDoubleTapDown
|
..onDoubleTapDown = onDoubleTapDown
|
||||||
..onDoubleTap = onDoubleTap;
|
..onDoubleTap = onDoubleTap;
|
||||||
}),
|
}),
|
||||||
LongPressGestureRecognizer:
|
LongPressGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||||
() => LongPressGestureRecognizer(
|
() => LongPressGestureRecognizer(), (instance) {
|
||||||
supportedDevices: kTouchBasedDeviceKinds,
|
|
||||||
), (instance) {
|
|
||||||
instance
|
instance
|
||||||
..onLongPressDown = onLongPressDown
|
..onLongPressDown = onLongPressDown
|
||||||
..onLongPressUp = onLongPressUp
|
..onLongPressUp = onLongPressUp
|
||||||
@@ -563,9 +557,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
// Customized
|
// Customized
|
||||||
HoldTapMoveGestureRecognizer:
|
HoldTapMoveGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
||||||
() => HoldTapMoveGestureRecognizer(
|
() => HoldTapMoveGestureRecognizer(),
|
||||||
supportedDevices: kTouchBasedDeviceKinds,
|
|
||||||
),
|
|
||||||
(instance) => instance
|
(instance) => instance
|
||||||
..onHoldDragStart = onHoldDragStart
|
..onHoldDragStart = onHoldDragStart
|
||||||
..onHoldDragUpdate = onHoldDragUpdate
|
..onHoldDragUpdate = onHoldDragUpdate
|
||||||
@@ -573,18 +565,14 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
..onHoldDragEnd = onHoldDragEnd),
|
..onHoldDragEnd = onHoldDragEnd),
|
||||||
DoubleFinerTapGestureRecognizer:
|
DoubleFinerTapGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
||||||
() => DoubleFinerTapGestureRecognizer(
|
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
||||||
supportedDevices: kTouchBasedDeviceKinds,
|
|
||||||
), (instance) {
|
|
||||||
instance
|
instance
|
||||||
..onDoubleFinerTap = onDoubleFinerTap
|
..onDoubleFinerTap = onDoubleFinerTap
|
||||||
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
..onDoubleFinerTapDown = onDoubleFinerTapDown;
|
||||||
}),
|
}),
|
||||||
CustomTouchGestureRecognizer:
|
CustomTouchGestureRecognizer:
|
||||||
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
||||||
() => CustomTouchGestureRecognizer(
|
() => CustomTouchGestureRecognizer(), (instance) {
|
||||||
supportedDevices: kTouchBasedDeviceKinds,
|
|
||||||
), (instance) {
|
|
||||||
instance.onOneFingerPanStart =
|
instance.onOneFingerPanStart =
|
||||||
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
||||||
instance
|
instance
|
||||||
|
|||||||
@@ -13,13 +13,83 @@ import 'package:flutter_hbb/models/model.dart';
|
|||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
bool isEditOsPassword = false;
|
bool isEditOsPassword = false;
|
||||||
|
const String kPeerOptionAllowWaylandKeyboard = 'allow-wayland-keyboard';
|
||||||
|
const String kWaylandKeyboardIssueUrl =
|
||||||
|
'https://github.com/rustdesk/rustdesk/issues/14586';
|
||||||
|
final Set<String> _waylandKeyboardPromptSuppressedConnectionIds = <String>{};
|
||||||
|
|
||||||
// macOS privacy mode blacks out all online displays, so switching the remote
|
Future<bool> openWaylandKeyboardIssueUrl() {
|
||||||
// display does not weaken the local privacy protection.
|
return launchUrl(
|
||||||
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
|
Uri.parse(kWaylandKeyboardIssueUrl),
|
||||||
return pi.platform == kPeerPlatformMacOS;
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
|
||||||
|
return _waylandKeyboardPromptSuppressedConnectionIds.contains(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setWaylandKeyboardPromptSuppressedForConnection(
|
||||||
|
String connectionId, bool suppressed) {
|
||||||
|
if (suppressed) {
|
||||||
|
_waylandKeyboardPromptSuppressedConnectionIds.add(connectionId);
|
||||||
|
} else {
|
||||||
|
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
|
||||||
|
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldShowWaylandKeyboardPrompt({
|
||||||
|
required String connectionId,
|
||||||
|
required bool isWaylandPeer,
|
||||||
|
required bool allowWaylandKeyboardRemembered,
|
||||||
|
}) {
|
||||||
|
return isWaylandPeer &&
|
||||||
|
!allowWaylandKeyboardRemembered &&
|
||||||
|
!isWaylandKeyboardPromptSuppressedForConnection(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget waylandKeyboardScopeChip(BuildContext context, String text) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
border: Border.all(color: colorScheme.primary.withOpacity(0.35)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isWindowsMode1PrivacyImpl(String privacyModeImpl) {
|
||||||
|
return privacyModeImpl == kPrivacyModeImplMag ||
|
||||||
|
privacyModeImpl == kPrivacyModeImplExcludeFromCapture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS privacy mode blacks out all online displays. Windows Mode 1 also
|
||||||
|
// covers every local monitor with privacy overlay windows, so remote display
|
||||||
|
// switching does not weaken local privacy protection.
|
||||||
|
//
|
||||||
|
// Keep this separate from the capture backend capability. The legacy Windows
|
||||||
|
// magnifier capturer is not reliable for multi-monitor capture; WebRTC's
|
||||||
|
// screen_capturer_win_magnifier also disables it when SM_CMONITORS != 1:
|
||||||
|
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
|
||||||
|
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi, String privacyModeImpl) {
|
||||||
|
return pi.platform == kPeerPlatformMacOS ||
|
||||||
|
(pi.platform == kPeerPlatformWindows &&
|
||||||
|
_isWindowsMode1PrivacyImpl(privacyModeImpl) &&
|
||||||
|
versionCmp(pi.version, '1.4.8') >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TTextMenu {
|
class TTextMenu {
|
||||||
@@ -93,12 +163,179 @@ handleOsPasswordAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showWaylandKeyboardInputWarningDialog(
|
||||||
|
{required String id,
|
||||||
|
required String connectionId,
|
||||||
|
required FFI ffi,
|
||||||
|
required Future<void> Function() onEnable}) {
|
||||||
|
bool remember = false;
|
||||||
|
bool consentInProgress = false;
|
||||||
|
bool dialogClosed = false;
|
||||||
|
|
||||||
|
final dialogFuture = ffi.dialogManager.show((setState, close, context) {
|
||||||
|
void safeSetState(VoidCallback fn) {
|
||||||
|
if (dialogClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setState(fn);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Ignore setState after dialog disposal: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeDialog() {
|
||||||
|
if (dialogClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialogClosed = true;
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> enableAndContinue() async {
|
||||||
|
if (consentInProgress || dialogClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
consentInProgress = true;
|
||||||
|
safeSetState(() {});
|
||||||
|
try {
|
||||||
|
await onEnable();
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('Failed to enable Wayland keyboard input consent: $e');
|
||||||
|
debugPrintStack(stackTrace: st);
|
||||||
|
consentInProgress = false;
|
||||||
|
safeSetState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ffi.inputModel.keyboardInputAllowed = true;
|
||||||
|
var rememberPersisted = true;
|
||||||
|
if (remember) {
|
||||||
|
try {
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id,
|
||||||
|
key: kPeerOptionAllowWaylandKeyboard,
|
||||||
|
value: bool2option(kPeerOptionAllowWaylandKeyboard, true));
|
||||||
|
} catch (e) {
|
||||||
|
rememberPersisted = false;
|
||||||
|
debugPrint('Failed to persist Wayland keyboard input consent: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always suppress prompt for current connection after explicit consent.
|
||||||
|
setWaylandKeyboardPromptSuppressedForConnection(connectionId, true);
|
||||||
|
closeDialog();
|
||||||
|
if (remember && !rememberPersisted) {
|
||||||
|
// It's a rare edge case that persisting the user's choice fails.
|
||||||
|
// Failed to persist the user's choice, but still allow keyboard input for current session.
|
||||||
|
showToast(translate('Failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancel() {
|
||||||
|
if (consentInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: null,
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
msgboxContent(
|
||||||
|
'',
|
||||||
|
'wayland-keyboard-input-disabled-tip',
|
||||||
|
'wayland-keyboard-input-consent-tip',
|
||||||
|
),
|
||||||
|
SizedBox(height: isMobile ? 2 : 6),
|
||||||
|
if (isMobile) ...[
|
||||||
|
Text(
|
||||||
|
translate('wayland-keyboard-input-applies-to-tip'),
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
).marginOnly(bottom: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
waylandKeyboardScopeChip(
|
||||||
|
context, translate('Send clipboard keystrokes')),
|
||||||
|
waylandKeyboardScopeChip(
|
||||||
|
context, translate('wayland-soft-keyboard-input-label')),
|
||||||
|
],
|
||||||
|
).marginOnly(bottom: 10),
|
||||||
|
],
|
||||||
|
TextButton(
|
||||||
|
onPressed: consentInProgress
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
try {
|
||||||
|
final opened = await openWaylandKeyboardIssueUrl();
|
||||||
|
if (!opened) {
|
||||||
|
// Opening this optional help link almost never fails in
|
||||||
|
// normal desktop environments. Keep the result handled
|
||||||
|
// for review hygiene, but avoid a low-value user toast.
|
||||||
|
debugPrint('Failed to open Wayland keyboard issue URL');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
'Failed to open Wayland keyboard issue URL: $e');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Colors.blue,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
translate('Why this happens'),
|
||||||
|
style: const TextStyle(decoration: TextDecoration.underline),
|
||||||
|
),
|
||||||
|
).marginOnly(bottom: 6),
|
||||||
|
CheckboxListTile(
|
||||||
|
value: remember,
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
title: Text(translate('remember-wayland-keyboard-choice-tip')),
|
||||||
|
onChanged: consentInProgress
|
||||||
|
? null
|
||||||
|
: (v) {
|
||||||
|
safeSetState(() => remember = v == true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
dialogButton(
|
||||||
|
'Cancel',
|
||||||
|
onPressed: consentInProgress ? null : cancel,
|
||||||
|
isOutline: true,
|
||||||
|
),
|
||||||
|
dialogButton(
|
||||||
|
'OK',
|
||||||
|
onPressed:
|
||||||
|
consentInProgress ? null : () => unawaited(enableAndContinue()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onCancel: consentInProgress ? null : cancel,
|
||||||
|
onSubmit: consentInProgress ? null : () => unawaited(enableAndContinue()),
|
||||||
|
);
|
||||||
|
}, clickMaskDismiss: false, backDismiss: false);
|
||||||
|
unawaited(dialogFuture.whenComplete(() => dialogClosed = true));
|
||||||
|
}
|
||||||
|
|
||||||
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final perms = ffiModel.permissions;
|
final perms = ffiModel.permissions;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
||||||
|
final isWaylandPeer = pi.platform == kPeerPlatformLinux && pi.isWayland;
|
||||||
|
|
||||||
List<TTextMenu> v = [];
|
List<TTextMenu> v = [];
|
||||||
// elevation
|
// elevation
|
||||||
@@ -148,11 +385,60 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Send clipboard keystrokes')),
|
child: Text(translate('Send clipboard keystrokes')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
Future<void> sendClipboardKeystrokes() async {
|
||||||
if (data != null && data.text != null) {
|
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
bind.sessionInputString(
|
if (data != null && data.text != null) {
|
||||||
sessionId: sessionId, value: data.text ?? "");
|
bind.sessionInputString(
|
||||||
|
sessionId: sessionId, value: data.text ?? "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final allowWaylandKeyboard =
|
||||||
|
mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard);
|
||||||
|
if (shouldShowWaylandKeyboardPrompt(
|
||||||
|
connectionId: sessionId.toString(),
|
||||||
|
isWaylandPeer: isWaylandPeer,
|
||||||
|
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
|
||||||
|
)) {
|
||||||
|
ffi.inputModel.keyboardInputAllowed = false;
|
||||||
|
showWaylandKeyboardInputWarningDialog(
|
||||||
|
id: id,
|
||||||
|
connectionId: sessionId.toString(),
|
||||||
|
ffi: ffi,
|
||||||
|
onEnable: sendClipboardKeystrokes,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sendClipboardKeystrokes();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (isDefaultConn &&
|
||||||
|
isWaylandPeer &&
|
||||||
|
(mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard) ||
|
||||||
|
isWaylandKeyboardPromptSuppressedForConnection(
|
||||||
|
sessionId.toString()))) {
|
||||||
|
v.add(TTextMenu(
|
||||||
|
child: Text(translate('wayland-keyboard-input-reset-choice-tip')),
|
||||||
|
onPressed: () async {
|
||||||
|
var persistedCleared = false;
|
||||||
|
try {
|
||||||
|
await bind.mainSetPeerOption(
|
||||||
|
id: id,
|
||||||
|
key: kPeerOptionAllowWaylandKeyboard,
|
||||||
|
value: bool2option(kPeerOptionAllowWaylandKeyboard, false));
|
||||||
|
persistedCleared = true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
'Failed to clear persisted Wayland keyboard permission: $e');
|
||||||
|
} finally {
|
||||||
|
clearWaylandKeyboardPromptSuppressedForConnection(
|
||||||
|
sessionId.toString());
|
||||||
|
ffi.inputModel.keyboardInputAllowed = false;
|
||||||
|
if (isMobile) {
|
||||||
|
await ffi.invokeMethod("enable_soft_keyboard", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(translate(persistedCleared ? 'Successful' : 'Failed'));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// reset canvas
|
// reset canvas
|
||||||
@@ -692,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 =
|
||||||
@@ -766,7 +1053,8 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false;
|
final hasPrivacyModePermission =
|
||||||
|
ffiModel.permissions['privacy_mode'] != false;
|
||||||
|
|
||||||
// Backend revocation already attempts to turn privacy mode off.
|
// Backend revocation already attempts to turn privacy mode off.
|
||||||
// Still keep this menu when privacy mode is active, so users can turn it off
|
// Still keep this menu when privacy mode is active, so users can turn it off
|
||||||
@@ -775,24 +1063,28 @@ 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) {
|
||||||
final enabled =
|
if (!turnOn ||
|
||||||
!ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
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 &&
|
||||||
|
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
||||||
return TToggleMenu(
|
return TToggleMenu(
|
||||||
value: privacyModeState.isNotEmpty,
|
value: privacyModeState.isNotEmpty,
|
||||||
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';
|
||||||
@@ -810,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) {
|
||||||
@@ -824,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
|
||||||
@@ -845,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";
|
||||||
@@ -142,6 +146,10 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
|
|||||||
const String kOptionCodecPreference = "codec-preference";
|
const String kOptionCodecPreference = "codec-preference";
|
||||||
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
|
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
|
||||||
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
|
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
|
||||||
|
const String kOptionRemoteMenubarEdge = "remote-menubar-edge";
|
||||||
|
const String kOptionRemoteMenubarFraction = "remote-menubar-frac";
|
||||||
|
const String kOptionAllowMultiEdgeToolbarDock =
|
||||||
|
"allow-multi-edge-toolbar-dock";
|
||||||
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
|
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
|
||||||
const String kOptionRemoteMenubarState = "remoteMenubarState";
|
const String kOptionRemoteMenubarState = "remoteMenubarState";
|
||||||
const String kOptionPeerSorting = "peer-sorting";
|
const String kOptionPeerSorting = "peer-sorting";
|
||||||
@@ -166,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,15 +483,28 @@ 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),
|
||||||
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
|
if (!incomingOnly)
|
||||||
|
_OptionCheckBox(
|
||||||
|
context,
|
||||||
|
'allow-remote-toolbar-docking-any-edge',
|
||||||
|
kOptionAllowMultiEdgeToolbarDock,
|
||||||
|
isServer: false,
|
||||||
|
update: (_) {
|
||||||
|
reloadAllWindows();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!isWeb && !outgoingOnly)
|
||||||
|
_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',
|
||||||
@@ -529,40 +543,40 @@ class _GeneralState extends State<_General> {
|
|||||||
isServer: false,
|
isServer: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!isWeb && !bind.isCustomClient())
|
],
|
||||||
_OptionCheckBox(
|
if (!isWeb && !bind.isCustomClient())
|
||||||
context,
|
_OptionCheckBox(
|
||||||
'Check for software update on startup',
|
context,
|
||||||
kOptionEnableCheckUpdate,
|
'Check for software update on startup',
|
||||||
isServer: false,
|
kOptionEnableCheckUpdate,
|
||||||
),
|
isServer: false,
|
||||||
if (showAutoUpdate)
|
),
|
||||||
_OptionCheckBox(
|
if (showAutoUpdate)
|
||||||
context,
|
_OptionCheckBox(
|
||||||
'Auto update',
|
context,
|
||||||
kOptionAllowAutoUpdate,
|
'Auto update',
|
||||||
isServer: true,
|
kOptionAllowAutoUpdate,
|
||||||
),
|
isServer: true,
|
||||||
if (isWindows && !bind.isOutgoingOnly())
|
),
|
||||||
_OptionCheckBox(
|
if (isWindows && !outgoingOnly)
|
||||||
context,
|
_OptionCheckBox(
|
||||||
'Capture screen using DirectX',
|
context,
|
||||||
kOptionDirectxCapture,
|
'Capture screen using DirectX',
|
||||||
),
|
kOptionDirectxCapture,
|
||||||
if (!bind.isIncomingOnly()) ...[
|
),
|
||||||
_OptionCheckBox(
|
if (!isWeb && !incomingOnly) ...[
|
||||||
context,
|
_OptionCheckBox(
|
||||||
'Enable UDP hole punching',
|
context,
|
||||||
kOptionEnableUdpPunch,
|
'Enable UDP hole punching',
|
||||||
isServer: false,
|
kOptionEnableUdpPunch,
|
||||||
),
|
isServer: false,
|
||||||
_OptionCheckBox(
|
),
|
||||||
context,
|
_OptionCheckBox(
|
||||||
'Enable IPv6 P2P connection',
|
context,
|
||||||
kOptionEnableIpv6Punch,
|
'Enable IPv6 P2P connection',
|
||||||
isServer: false,
|
kOptionEnableIpv6Punch,
|
||||||
),
|
isServer: false,
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -595,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2419,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(
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
|||||||
late final TextEditingController controller;
|
late final TextEditingController controller;
|
||||||
final RxBool startmenu = true.obs;
|
final RxBool startmenu = true.obs;
|
||||||
final RxBool desktopicon = true.obs;
|
final RxBool desktopicon = true.obs;
|
||||||
final RxBool printer = true.obs;
|
final RxBool printer = false.obs;
|
||||||
final RxBool showProgress = false.obs;
|
final RxBool showProgress = false.obs;
|
||||||
final RxBool btnEnabled = true.obs;
|
final RxBool btnEnabled = true.obs;
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
|
|||||||
final installOptions = jsonDecode(bind.installInstallOptions());
|
final installOptions = jsonDecode(bind.installInstallOptions());
|
||||||
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
|
startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0';
|
||||||
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
|
desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0';
|
||||||
printer.value = installOptions['PRINTER'] != '0';
|
printer.value = installOptions['PRINTER'] == '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
|
Worker? _waylandKeyboardModeWorker;
|
||||||
|
bool _waylandKeyboardModeNormalized = false;
|
||||||
|
bool _waylandKeyboardModeNormalizing = false;
|
||||||
|
|
||||||
SessionID get sessionId => _ffi.sessionId;
|
SessionID get sessionId => _ffi.sessionId;
|
||||||
|
|
||||||
@@ -178,6 +181,48 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// Register callback to cancel debounce timer when relative mouse mode is disabled
|
// Register callback to cancel debounce timer when relative mouse mode is disabled
|
||||||
_ffi.inputModel.onRelativeMouseModeDisabled =
|
_ffi.inputModel.onRelativeMouseModeDisabled =
|
||||||
_cancelPointerLockCenterDebounceTimer;
|
_cancelPointerLockCenterDebounceTimer;
|
||||||
|
|
||||||
|
_waylandKeyboardModeWorker = ever(_ffi.ffiModel.pi.isSet, (bool isSet) {
|
||||||
|
if (isSet) {
|
||||||
|
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (_ffi.ffiModel.pi.isSet.value) {
|
||||||
|
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _normalizeWaylandKeyboardModeIfNeeded() async {
|
||||||
|
if (!mounted ||
|
||||||
|
_waylandKeyboardModeNormalized ||
|
||||||
|
_waylandKeyboardModeNormalizing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_waylandKeyboardModeNormalizing = true;
|
||||||
|
try {
|
||||||
|
final pi = _ffi.ffiModel.pi;
|
||||||
|
if (pi.platform != kPeerPlatformLinux || !pi.isWayland) return;
|
||||||
|
final mapSupported = bind.sessionIsKeyboardModeSupported(
|
||||||
|
sessionId: sessionId, mode: kKeyMapMode);
|
||||||
|
if (!mapSupported) return;
|
||||||
|
final current = await bind.sessionGetKeyboardMode(sessionId: sessionId);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (current == kKeyMapMode) {
|
||||||
|
_waylandKeyboardModeNormalized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await bind.sessionSetKeyboardMode(
|
||||||
|
sessionId: sessionId, value: kKeyMapMode);
|
||||||
|
if (!mounted) return;
|
||||||
|
await _ffi.inputModel.updateKeyboardMode();
|
||||||
|
if (!mounted) return;
|
||||||
|
_waylandKeyboardModeNormalized = true;
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint('Failed to normalize Wayland keyboard mode: $e');
|
||||||
|
debugPrintStack(stackTrace: st);
|
||||||
|
} finally {
|
||||||
|
_waylandKeyboardModeNormalizing = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancel the pointer lock center debounce timer
|
/// Cancel the pointer lock center debounce timer
|
||||||
@@ -318,6 +363,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
_pointerLockCenterDebounceTimer?.cancel();
|
_pointerLockCenterDebounceTimer?.cancel();
|
||||||
_pointerLockCenterDebounceTimer = null;
|
_pointerLockCenterDebounceTimer = null;
|
||||||
|
_waylandKeyboardModeWorker?.dispose();
|
||||||
// Clear callback reference to prevent memory leaks and stale references
|
// Clear callback reference to prevent memory leaks and stale references
|
||||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||||
@@ -331,6 +377,9 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.imageModel.disposeImage();
|
_ffi.imageModel.disposeImage();
|
||||||
_ffi.cursorModel.disposeImages();
|
_ffi.cursorModel.disposeImages();
|
||||||
_rawKeyFocusNode.dispose();
|
_rawKeyFocusNode.dispose();
|
||||||
|
if (closeSession) {
|
||||||
|
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
|
||||||
|
}
|
||||||
await _ffi.close(closeSession: closeSession);
|
await _ffi.close(closeSession: closeSession);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_ffi.dialogManager.dismissAll();
|
_ffi.dialogManager.dismissAll();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ import 'common.dart';
|
|||||||
import 'consts.dart';
|
import 'consts.dart';
|
||||||
import 'mobile/pages/home_page.dart';
|
import 'mobile/pages/home_page.dart';
|
||||||
import 'mobile/pages/server_page.dart';
|
import 'mobile/pages/server_page.dart';
|
||||||
|
import 'mobile/widgets/deploy_dialog.dart';
|
||||||
import 'models/platform_model.dart';
|
import 'models/platform_model.dart';
|
||||||
|
|
||||||
import 'package:flutter_hbb/plugin/handlers.dart'
|
import 'package:flutter_hbb/plugin/handlers.dart'
|
||||||
@@ -575,6 +576,14 @@ _registerEventHandler() {
|
|||||||
NativeUiHandler.instance.onEvent(evt);
|
NativeUiHandler.instance.onEvent(evt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (isAndroid) {
|
||||||
|
platformFFI.registerEventHandler(
|
||||||
|
'android_needs_deploy', 'android_needs_deploy', (_) async {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
showDeployPromptDialog();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget keyListenerBuilder(BuildContext context, Widget? child) {
|
Widget keyListenerBuilder(BuildContext context, Widget? child) {
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
final FocusNode _physicalFocusNode = FocusNode();
|
final FocusNode _physicalFocusNode = FocusNode();
|
||||||
var _showEdit = false; // use soft keyboard
|
var _showEdit = false; // use soft keyboard
|
||||||
|
|
||||||
|
Worker? _waylandKeyboardGateWorker;
|
||||||
|
bool _waylandKeyboardGateInitialized = false;
|
||||||
|
|
||||||
InputModel get inputModel => gFFI.inputModel;
|
InputModel get inputModel => gFFI.inputModel;
|
||||||
SessionID get sessionId => gFFI.sessionId;
|
SessionID get sessionId => gFFI.sessionId;
|
||||||
|
|
||||||
@@ -121,11 +124,33 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
isKeyboardVisible: keyboardVisibilityController.isVisible);
|
isKeyboardVisible: keyboardVisibilityController.isVisible);
|
||||||
});
|
});
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
inputModel.keyboardInputAllowed = true;
|
||||||
|
|
||||||
|
// Wayland sessions may use clipboard-based text input on the controlled side.
|
||||||
|
// Require explicit user confirmation before allowing soft-keyboard and
|
||||||
|
// clipboard-assisted text input. Physical keyboard events are not gated here.
|
||||||
|
_waylandKeyboardGateWorker = ever(gFFI.ffiModel.pi.isSet, (bool isSet) {
|
||||||
|
if (isSet) {
|
||||||
|
_initWaylandKeyboardGateIfNeeded();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (gFFI.ffiModel.pi.isSet.value) {
|
||||||
|
_initWaylandKeyboardGateIfNeeded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
// Close the session up-front. `gFFI.close()` below only calls `sessionClose`
|
||||||
|
// after several awaits (canvas save, image update, the `enable_soft_keyboard`
|
||||||
|
// platform call), so if the app is backgrounded while this page is disposing,
|
||||||
|
// dispose can be suspended before reaching it and the connection is never torn
|
||||||
|
// down. The reconnect then re-attaches to the leaked session and is stuck on
|
||||||
|
// "Connecting...". Dispatching it here makes teardown happen synchronously on
|
||||||
|
// pop; the `sessionClose` in `gFFI.close()` becomes a no-op once removed.
|
||||||
|
unawaited(bind.sessionClose(sessionId: sessionId));
|
||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
|
||||||
@@ -135,6 +160,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
_mobileFocusNode.dispose();
|
_mobileFocusNode.dispose();
|
||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
|
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
|
||||||
|
_waylandKeyboardGateWorker?.dispose();
|
||||||
|
inputModel.keyboardInputAllowed = true;
|
||||||
await gFFI.close();
|
await gFFI.close();
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_iosKeyboardWorkaroundTimer?.cancel();
|
_iosKeyboardWorkaroundTimer?.cancel();
|
||||||
@@ -163,6 +191,40 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
gFFI.invokeMethod("try_sync_clipboard");
|
gFFI.invokeMethod("try_sync_clipboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _shouldGateKeyboardForWayland() {
|
||||||
|
if (!(isAndroid || isIOS)) return false;
|
||||||
|
final pi = gFFI.ffiModel.pi;
|
||||||
|
return pi.platform == kPeerPlatformLinux && pi.isWayland;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initWaylandKeyboardGateIfNeeded() {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_waylandKeyboardGateInitialized) return;
|
||||||
|
if (!_shouldGateKeyboardForWayland()) return;
|
||||||
|
|
||||||
|
_waylandKeyboardGateInitialized = true;
|
||||||
|
|
||||||
|
final allowWaylandKeyboard =
|
||||||
|
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
|
||||||
|
if (!shouldShowWaylandKeyboardPrompt(
|
||||||
|
connectionId: sessionId.toString(),
|
||||||
|
isWaylandPeer: _shouldGateKeyboardForWayland(),
|
||||||
|
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
|
||||||
|
)) {
|
||||||
|
inputModel.keyboardInputAllowed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputModel.keyboardInputAllowed = false;
|
||||||
|
|
||||||
|
// Ensure soft keyboard is not active before user confirms.
|
||||||
|
_showEdit = false;
|
||||||
|
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||||
|
_mobileFocusNode.unfocus();
|
||||||
|
_physicalFocusNode.requestFocus();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
// But for now, the transparent color will cause the canvas to be white.
|
// But for now, the transparent color will cause the canvas to be white.
|
||||||
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
||||||
@@ -294,7 +356,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
content == '【】')) {
|
content == '【】')) {
|
||||||
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
|
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
|
||||||
bind.sessionInputString(sessionId: sessionId, value: content);
|
bind.sessionInputString(sessionId: sessionId, value: content);
|
||||||
openKeyboard();
|
_openKeyboardUnlocked();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bind.sessionInputString(sessionId: sessionId, value: content);
|
bind.sessionInputString(sessionId: sessionId, value: content);
|
||||||
@@ -306,6 +368,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
// handle mobile virtual keyboard
|
// handle mobile virtual keyboard
|
||||||
void handleSoftKeyboardInput(String newValue) {
|
void handleSoftKeyboardInput(String newValue) {
|
||||||
|
if (!inputModel.keyboardInputAllowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
_handleIOSSoftKeyboardInput(newValue);
|
_handleIOSSoftKeyboardInput(newValue);
|
||||||
} else {
|
} else {
|
||||||
@@ -314,6 +379,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void inputChar(String char) {
|
void inputChar(String char) {
|
||||||
|
if (!inputModel.keyboardInputAllowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (char == '\n') {
|
if (char == '\n') {
|
||||||
char = 'VK_RETURN';
|
char = 'VK_RETURN';
|
||||||
} else if (char == ' ') {
|
} else if (char == ' ') {
|
||||||
@@ -323,6 +391,29 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void openKeyboard() {
|
void openKeyboard() {
|
||||||
|
final allowWaylandKeyboard =
|
||||||
|
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
|
||||||
|
if (shouldShowWaylandKeyboardPrompt(
|
||||||
|
connectionId: sessionId.toString(),
|
||||||
|
isWaylandPeer: _shouldGateKeyboardForWayland(),
|
||||||
|
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
|
||||||
|
)) {
|
||||||
|
inputModel.keyboardInputAllowed = false;
|
||||||
|
showWaylandKeyboardInputWarningDialog(
|
||||||
|
id: widget.id,
|
||||||
|
connectionId: sessionId.toString(),
|
||||||
|
ffi: gFFI,
|
||||||
|
onEnable: () async {
|
||||||
|
_openKeyboardUnlocked();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_openKeyboardUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openKeyboardUnlocked() {
|
||||||
|
inputModel.keyboardInputAllowed = true;
|
||||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
// destroy first, so that our _value trick can work
|
// destroy first, so that our _value trick can work
|
||||||
_value = initText;
|
_value = initText;
|
||||||
@@ -426,10 +517,12 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor,
|
color: MyTheme.canvasColor,
|
||||||
child: RawTouchGestureDetectorRegion(
|
child: inputModel.isPhysicalMouse.value
|
||||||
child: getBodyForMobile(),
|
? getBodyForMobile()
|
||||||
ffi: gFFI,
|
: RawTouchGestureDetectorRegion(
|
||||||
),
|
child: getBodyForMobile(),
|
||||||
|
ffi: gFFI,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -1127,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;
|
||||||
@@ -1181,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);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import '../../common/widgets/login.dart';
|
|||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../widgets/deploy_dialog.dart';
|
||||||
import '../widgets/dialog.dart';
|
import '../widgets/dialog.dart';
|
||||||
import 'home_page.dart';
|
import 'home_page.dart';
|
||||||
import 'scan_page.dart';
|
import 'scan_page.dart';
|
||||||
@@ -728,6 +729,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onPressed: (context) {
|
onPressed: (context) {
|
||||||
changeSocks5Proxy();
|
changeSocks5Proxy();
|
||||||
}),
|
}),
|
||||||
|
if (isAndroid && !bind.isOutgoingOnly())
|
||||||
|
SettingsTile(
|
||||||
|
title: Text(translate('Deploy')),
|
||||||
|
leading: Icon(Icons.cloud_upload),
|
||||||
|
onPressed: (context) {
|
||||||
|
showDeployDialog();
|
||||||
|
}),
|
||||||
if (!disabledSettings && !_hideNetwork && !_hideWebSocket)
|
if (!disabledSettings && !_hideNetwork && !_hideWebSocket)
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Use WebSocket')),
|
title: Text(translate('Use WebSocket')),
|
||||||
|
|||||||
@@ -259,11 +259,13 @@ class _ViewCameraPageState extends State<ViewCameraPage>
|
|||||||
}
|
}
|
||||||
return Container(
|
return Container(
|
||||||
color: MyTheme.canvasColor,
|
color: MyTheme.canvasColor,
|
||||||
child: RawTouchGestureDetectorRegion(
|
child: inputModel.isPhysicalMouse.value
|
||||||
child: getBodyForMobile(),
|
? getBodyForMobile()
|
||||||
ffi: gFFI,
|
: RawTouchGestureDetectorRegion(
|
||||||
isCamera: true,
|
child: getBodyForMobile(),
|
||||||
),
|
ffi: gFFI,
|
||||||
|
isCamera: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
114
flutter/lib/mobile/widgets/deploy_dialog.dart
Normal file
114
flutter/lib/mobile/widgets/deploy_dialog.dart
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import '../../common.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
|
|
||||||
|
const _deployDialogTag = 'android-deploy-device';
|
||||||
|
|
||||||
|
void showDeployPromptDialog() {
|
||||||
|
gFFI.dialogManager.dismissByTag(_deployDialogTag);
|
||||||
|
gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||||
|
submit() => close(true);
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Deploy")),
|
||||||
|
content: Text(translate("server_requires_deployment_tip")),
|
||||||
|
actions: [
|
||||||
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
|
dialogButton("OK", onPressed: submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: close,
|
||||||
|
);
|
||||||
|
}, tag: _deployDialogTag).then((deploy) {
|
||||||
|
if (deploy == true) {
|
||||||
|
showDeployDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showDeployDialog() {
|
||||||
|
gFFI.dialogManager.dismissByTag(_deployDialogTag);
|
||||||
|
final tokenController = TextEditingController();
|
||||||
|
final idController = TextEditingController();
|
||||||
|
var errorText = "";
|
||||||
|
var isInProgress = false;
|
||||||
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
|
submit() async {
|
||||||
|
if (isInProgress) return;
|
||||||
|
final token = tokenController.text.trim();
|
||||||
|
if (token.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
errorText = translate("token is required!");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
errorText = "";
|
||||||
|
isInProgress = true;
|
||||||
|
});
|
||||||
|
String res;
|
||||||
|
try {
|
||||||
|
res = await bind.mainDeployDevice(
|
||||||
|
token: token, id: idController.text.trim());
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
errorText = translate(e.toString());
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.isEmpty) {
|
||||||
|
close();
|
||||||
|
await gFFI.serverModel.fetchID();
|
||||||
|
showToast(translate("Successful"));
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
errorText = translate(res.toString());
|
||||||
|
isInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomAlertDialog(
|
||||||
|
title: Text(translate("Deploy")),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: tokenController,
|
||||||
|
decoration: InputDecoration(labelText: translate("API Token")),
|
||||||
|
obscureText: true,
|
||||||
|
enableSuggestions: false,
|
||||||
|
autocorrect: false,
|
||||||
|
autofocus: true,
|
||||||
|
).workaroundFreezeLinuxMint(),
|
||||||
|
TextField(
|
||||||
|
controller: idController,
|
||||||
|
decoration:
|
||||||
|
InputDecoration(labelText: translate("Custom ID (optional)")),
|
||||||
|
).workaroundFreezeLinuxMint(),
|
||||||
|
if (errorText.isNotEmpty)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: SelectableText(
|
||||||
|
errorText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).paddingOnly(top: 8),
|
||||||
|
),
|
||||||
|
if (isInProgress) const LinearProgressIndicator().paddingOnly(top: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
dialogButton("Cancel",
|
||||||
|
onPressed: isInProgress ? null : close, isOutline: true),
|
||||||
|
dialogButton("OK", onPressed: isInProgress ? null : submit),
|
||||||
|
],
|
||||||
|
onSubmit: submit,
|
||||||
|
onCancel: isInProgress ? null : close,
|
||||||
|
);
|
||||||
|
}, tag: _deployDialogTag);
|
||||||
|
}
|
||||||
@@ -117,13 +117,13 @@ void showServerSettingsWithValue(
|
|||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: serverSettingsTextFormField(
|
||||||
|
label: label,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
errorMsg: errorMsg,
|
||||||
errorText: errorMsg.isEmpty ? null : errorMsg,
|
contentPadding:
|
||||||
contentPadding:
|
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
showLabelText: false,
|
||||||
),
|
|
||||||
validator: validator,
|
validator: validator,
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
).workaroundFreezeLinuxMint(),
|
).workaroundFreezeLinuxMint(),
|
||||||
@@ -132,12 +132,10 @@ void showServerSettingsWithValue(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TextFormField(
|
return serverSettingsTextFormField(
|
||||||
|
label: label,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: InputDecoration(
|
errorMsg: errorMsg,
|
||||||
labelText: label,
|
|
||||||
errorText: errorMsg.isEmpty ? null : errorMsg,
|
|
||||||
),
|
|
||||||
validator: validator,
|
validator: validator,
|
||||||
).workaroundFreezeLinuxMint();
|
).workaroundFreezeLinuxMint();
|
||||||
}
|
}
|
||||||
@@ -209,6 +207,35 @@ void showServerSettingsWithValue(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextFormField serverSettingsTextFormField({
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String errorMsg,
|
||||||
|
String? Function(String?)? validator,
|
||||||
|
bool autofocus = false,
|
||||||
|
bool showLabelText = true,
|
||||||
|
EdgeInsetsGeometry? contentPadding,
|
||||||
|
}) {
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: showLabelText ? label : null,
|
||||||
|
errorText: errorMsg.isEmpty ? null : errorMsg,
|
||||||
|
contentPadding: contentPadding,
|
||||||
|
),
|
||||||
|
validator: validator,
|
||||||
|
autofocus: autofocus,
|
||||||
|
keyboardType: TextInputType.visiblePassword,
|
||||||
|
textCapitalization: TextCapitalization.none,
|
||||||
|
autocorrect: false,
|
||||||
|
enableSuggestions: false,
|
||||||
|
smartDashesType: SmartDashesType.disabled,
|
||||||
|
smartQuotesType: SmartQuotesType.disabled,
|
||||||
|
enableIMEPersonalizedLearning: false,
|
||||||
|
spellCheckConfiguration: const SpellCheckConfiguration.disabled(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void setPrivacyModeDialog(
|
void setPrivacyModeDialog(
|
||||||
OverlayDialogManager dialogManager,
|
OverlayDialogManager dialogManager,
|
||||||
List<TToggleMenu> privacyModeList,
|
List<TToggleMenu> privacyModeList,
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
debugPrint("Ignore override confirm result for inactive job: $evt");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (false == resp) {
|
if (false == resp) {
|
||||||
final jobIndex = jobController.getJob(id);
|
await jobController.cancelTransferConflictBatch(id);
|
||||||
if (jobIndex != -1) {
|
|
||||||
await jobController.cancelJob(id);
|
|
||||||
final job = jobController.jobTable[jobIndex];
|
|
||||||
job.state = JobState.done;
|
|
||||||
jobController.jobTable.refresh();
|
|
||||||
}
|
|
||||||
} 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();
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ class InputModel {
|
|||||||
/// which runs per-engine, so each isolate registers its own handler tied
|
/// which runs per-engine, so each isolate registers its own handler tied
|
||||||
/// to its own set of InputModels.
|
/// to its own set of InputModels.
|
||||||
static void initSideButtonChannel() {
|
static void initSideButtonChannel() {
|
||||||
if (!Platform.isLinux) return;
|
if (!isLinux) return;
|
||||||
if (_sideButtonChannelInitialized) return;
|
if (_sideButtonChannelInitialized) return;
|
||||||
_sideButtonChannelInitialized = true;
|
_sideButtonChannelInitialized = true;
|
||||||
|
|
||||||
@@ -474,6 +474,10 @@ class InputModel {
|
|||||||
|
|
||||||
late final SessionID sessionId;
|
late final SessionID sessionId;
|
||||||
|
|
||||||
|
// Local gate for clipboard-assisted input flows on mobile Wayland dialogs.
|
||||||
|
// It should not block physical keyboard events.
|
||||||
|
bool keyboardInputAllowed = true;
|
||||||
|
|
||||||
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
||||||
String get id => parent.target?.id ?? '';
|
String get id => parent.target?.id ?? '';
|
||||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||||
@@ -1303,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1544,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1566,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1588,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 = {};
|
||||||
@@ -119,6 +121,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;
|
||||||
@@ -250,6 +253,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
_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 +345,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 +927,28 @@ 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);
|
||||||
|
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 +974,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(
|
||||||
@@ -1374,6 +1404,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
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 +3697,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,6 +3911,7 @@ class FFI {
|
|||||||
}
|
}
|
||||||
if (ffiModel.waitForFirstImage.value == true) {
|
if (ffiModel.waitForFirstImage.value == true) {
|
||||||
ffiModel.waitForFirstImage.value = false;
|
ffiModel.waitForFirstImage.value = false;
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
await canvasModel.updateViewStyle();
|
await canvasModel.updateViewStyle();
|
||||||
await canvasModel.updateScrollStyle();
|
await canvasModel.updateScrollStyle();
|
||||||
|
|||||||
@@ -145,23 +145,26 @@ 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,
|
||||||
username: other.username,
|
username: other.username,
|
||||||
hostname: other.hostname,
|
hostname: other.hostname,
|
||||||
platform: other.platform,
|
platform: other.platform,
|
||||||
alias: other.alias,
|
alias: other.alias,
|
||||||
tags: other.tags.toList(),
|
tags: other.tags.toList(),
|
||||||
forceAlwaysRelay: other.forceAlwaysRelay,
|
forceAlwaysRelay: other.forceAlwaysRelay,
|
||||||
rdpPort: other.rdpPort,
|
rdpPort: other.rdpPort,
|
||||||
rdpUsername: other.rdpUsername,
|
rdpUsername: other.rdpUsername,
|
||||||
loginName: other.loginName,
|
loginName: other.loginName,
|
||||||
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 }
|
||||||
|
|||||||
@@ -2034,7 +2034,14 @@ class RustdeskImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
|
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
|
||||||
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
|
return js.context.callMethod(
|
||||||
|
'getByName', ['resolve_avatar_url', avatar])?.toString() ??
|
||||||
|
avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> mainDeployDevice(
|
||||||
|
{required String token, required String id, dynamic hint}) {
|
||||||
|
throw UnimplementedError("mainDeployDevice");
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter,hwcodec
|
cargo ndk --platform 21 --target armv7-linux-androideabi build --locked --release --features flutter,hwcodec
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter,hwcodec
|
cargo ndk --platform 21 --target aarch64-linux-android build --locked --release --features flutter,hwcodec
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo ndk --platform 21 --target x86_64-linux-android build --release --features flutter
|
cargo ndk --platform 21 --target x86_64-linux-android build --locked --release --features flutter
|
||||||
|
|||||||
@@ -7,4 +7,4 @@
|
|||||||
export CFLAGS="-DBROKEN_CLANG_ATOMICS"
|
export CFLAGS="-DBROKEN_CLANG_ATOMICS"
|
||||||
export CXXFLAGS="-DBROKEN_CLANG_ATOMICS"
|
export CXXFLAGS="-DBROKEN_CLANG_ATOMICS"
|
||||||
|
|
||||||
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter
|
cargo ndk --platform 21 --target i686-linux-android build --locked --release --features flutter
|
||||||
|
|||||||
@@ -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.6+64
|
version: 1.4.8+66
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid
|
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid --locked
|
||||||
flutter pub get
|
flutter pub get
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
|
||||||
# call `flutter clean` if cargo build fails
|
# call `flutter clean` if cargo build fails
|
||||||
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
|
||||||
cargo build --features flutter
|
cargo build --locked --features flutter
|
||||||
flutter run $@
|
flutter run $@
|
||||||
|
|||||||
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)?;
|
||||||
.arg(mount_point)
|
|
||||||
.status()
|
let recovered_stale_mount = if let Err(e) = fs::create_dir_all(mount_point) {
|
||||||
{
|
log::warn!(
|
||||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
"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 uninit_fuse_context_(is_client: bool) {
|
fn inspect_mount_point_state_with<T>(
|
||||||
if is_client {
|
mount_point: &Path,
|
||||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
metadata_result: io::Result<T>,
|
||||||
} else {
|
mountinfo_result: io::Result<String>,
|
||||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
) -> 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)
|
||||||
|
.status()
|
||||||
|
{
|
||||||
|
Ok(status) if status.success() => {}
|
||||||
|
Ok(status) => {
|
||||||
|
log::debug!(
|
||||||
|
"{} {:?} exited with status {:?}",
|
||||||
|
program,
|
||||||
|
mount_point,
|
||||||
|
status.code()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uninit_fuse_context_(is_client: bool) {
|
||||||
|
let ctx = {
|
||||||
|
let mut fuse_context_lock = if is_client {
|
||||||
|
FUSE_CONTEXT_CLIENT.lock()
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,28 @@
|
|||||||
|
|
||||||
#define CLIPRDR_SVC_CHANNEL_NAME "cliprdr"
|
#define CLIPRDR_SVC_CHANNEL_NAME "cliprdr"
|
||||||
|
|
||||||
|
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
|
||||||
|
#define WF_CLIPRDR_MAX_STREAMS 16384
|
||||||
|
|
||||||
|
/* Validates the remote descriptor array size after cItems has been read safely. */
|
||||||
|
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
|
||||||
|
{
|
||||||
|
SIZE_T header_size = offsetof(FILEGROUPDESCRIPTORW, fgd);
|
||||||
|
SIZE_T descriptors_size;
|
||||||
|
|
||||||
|
if (count == 0 || count > WF_CLIPRDR_MAX_STREAMS)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if (size < header_size)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
if ((SIZE_T)count > (((SIZE_T)-1) - header_size) / sizeof(FILEDESCRIPTORW))
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
descriptors_size = header_size + (SIZE_T)count * sizeof(FILEDESCRIPTORW);
|
||||||
|
return size >= descriptors_size;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clipboard Formats
|
* Clipboard Formats
|
||||||
*/
|
*/
|
||||||
@@ -224,6 +246,7 @@ struct wf_clipboard
|
|||||||
|
|
||||||
HWND hwnd;
|
HWND hwnd;
|
||||||
HANDLE hmem;
|
HANDLE hmem;
|
||||||
|
SIZE_T hmem_data_len;
|
||||||
HANDLE thread;
|
HANDLE thread;
|
||||||
HANDLE formatDataRespEvent;
|
HANDLE formatDataRespEvent;
|
||||||
BOOL formatDataRespReceived;
|
BOOL formatDataRespReceived;
|
||||||
@@ -629,6 +652,50 @@ void CliprdrStream_Delete(CliprdrStream *instance)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void wf_cliprdr_release_streams(IStream **streams, ULONG count)
|
||||||
|
{
|
||||||
|
ULONG i;
|
||||||
|
|
||||||
|
if (!streams)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (streams[i])
|
||||||
|
CliprdrStream_Release(streams[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(streams);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wf_cliprdr_reset_streams(CliprdrDataObject *instance)
|
||||||
|
{
|
||||||
|
if (!instance)
|
||||||
|
return;
|
||||||
|
|
||||||
|
wf_cliprdr_release_streams(instance->m_pStream, instance->m_nStreams);
|
||||||
|
instance->m_pStream = NULL;
|
||||||
|
instance->m_nStreams = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only call after clipboard->hmem has been locked by GlobalLock. */
|
||||||
|
static HRESULT wf_cliprdr_fail_locked_file_descriptor_data(wfClipboard *clipboard,
|
||||||
|
STGMEDIUM *medium,
|
||||||
|
CliprdrDataObject *instance,
|
||||||
|
IStream **streams,
|
||||||
|
ULONG stream_count,
|
||||||
|
HRESULT error)
|
||||||
|
{
|
||||||
|
GlobalUnlock(clipboard->hmem);
|
||||||
|
GlobalFree(clipboard->hmem);
|
||||||
|
clipboard->hmem = NULL;
|
||||||
|
clipboard->hmem_data_len = 0;
|
||||||
|
medium->hGlobal = NULL;
|
||||||
|
wf_cliprdr_release_streams(streams, stream_count);
|
||||||
|
wf_cliprdr_reset_streams(instance);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IDataObject
|
* IDataObject
|
||||||
*/
|
*/
|
||||||
@@ -747,6 +814,9 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO
|
|||||||
{
|
{
|
||||||
// FILEGROUPDESCRIPTOR *dsc;
|
// FILEGROUPDESCRIPTOR *dsc;
|
||||||
FILEGROUPDESCRIPTORW *dsc;
|
FILEGROUPDESCRIPTORW *dsc;
|
||||||
|
IStream **streams = NULL;
|
||||||
|
UINT stream_count = 0;
|
||||||
|
SIZE_T hmem_size;
|
||||||
// DWORD remote_format_id = get_remote_format_id(clipboard, instance->m_pFormatEtc[idx].cfFormat);
|
// DWORD remote_format_id = get_remote_format_id(clipboard, instance->m_pFormatEtc[idx].cfFormat);
|
||||||
// FIXME: origin code may be failed here???
|
// FIXME: origin code may be failed here???
|
||||||
if (cliprdr_send_data_request(instance->m_connID, clipboard, instance->m_pFormatEtc[idx].cfFormat) != 0)
|
if (cliprdr_send_data_request(instance->m_connID, clipboard, instance->m_pFormatEtc[idx].cfFormat) != 0)
|
||||||
@@ -764,40 +834,48 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO
|
|||||||
* is the number of FILEDESCRIPTOR's */
|
* is the number of FILEDESCRIPTOR's */
|
||||||
// dsc = (FILEGROUPDESCRIPTOR *)GlobalLock(clipboard->hmem);
|
// dsc = (FILEGROUPDESCRIPTOR *)GlobalLock(clipboard->hmem);
|
||||||
dsc = (FILEGROUPDESCRIPTORW *)GlobalLock(clipboard->hmem);
|
dsc = (FILEGROUPDESCRIPTORW *)GlobalLock(clipboard->hmem);
|
||||||
instance->m_nStreams = dsc->cItems;
|
if (!dsc)
|
||||||
GlobalUnlock(clipboard->hmem);
|
|
||||||
|
|
||||||
if (instance->m_nStreams > 0)
|
|
||||||
{
|
{
|
||||||
if (!instance->m_pStream)
|
|
||||||
{
|
|
||||||
instance->m_pStream = (LPSTREAM *)calloc(instance->m_nStreams, sizeof(LPSTREAM));
|
|
||||||
|
|
||||||
if (instance->m_pStream)
|
|
||||||
{
|
|
||||||
for (i = 0; i < instance->m_nStreams; i++)
|
|
||||||
{
|
|
||||||
instance->m_pStream[i] =
|
|
||||||
(IStream *)CliprdrStream_New(instance->m_connID, i, clipboard, &dsc->fgd[i]);
|
|
||||||
|
|
||||||
if (!instance->m_pStream[i])
|
|
||||||
return E_OUTOFMEMORY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!instance->m_pStream)
|
|
||||||
{
|
|
||||||
if (clipboard->hmem)
|
|
||||||
{
|
|
||||||
GlobalFree(clipboard->hmem);
|
|
||||||
clipboard->hmem = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
pMedium->hGlobal = NULL;
|
pMedium->hGlobal = NULL;
|
||||||
return E_OUTOFMEMORY;
|
GlobalFree(clipboard->hmem);
|
||||||
|
clipboard->hmem = NULL;
|
||||||
|
clipboard->hmem_data_len = 0;
|
||||||
|
wf_cliprdr_reset_streams(instance);
|
||||||
|
return E_UNEXPECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hmem_size = clipboard->hmem_data_len;
|
||||||
|
/* cItems is remote-controlled; verify the fixed header exists before reading it. */
|
||||||
|
if (hmem_size < offsetof(FILEGROUPDESCRIPTORW, fgd))
|
||||||
|
return wf_cliprdr_fail_locked_file_descriptor_data(
|
||||||
|
clipboard, pMedium, instance, NULL, 0, E_UNEXPECTED);
|
||||||
|
|
||||||
|
stream_count = dsc->cItems;
|
||||||
|
if (!wf_cliprdr_file_group_descriptor_size_valid(hmem_size, stream_count))
|
||||||
|
return wf_cliprdr_fail_locked_file_descriptor_data(
|
||||||
|
clipboard, pMedium, instance, NULL, 0, E_UNEXPECTED);
|
||||||
|
|
||||||
|
streams = (IStream **)calloc(stream_count, sizeof(IStream *));
|
||||||
|
if (!streams)
|
||||||
|
return wf_cliprdr_fail_locked_file_descriptor_data(
|
||||||
|
clipboard, pMedium, instance, NULL, 0, E_OUTOFMEMORY);
|
||||||
|
|
||||||
|
for (i = 0; i < stream_count; i++)
|
||||||
|
{
|
||||||
|
streams[i] =
|
||||||
|
(IStream *)CliprdrStream_New(instance->m_connID, i, clipboard, &dsc->fgd[i]);
|
||||||
|
if (!streams[i])
|
||||||
|
{
|
||||||
|
return wf_cliprdr_fail_locked_file_descriptor_data(
|
||||||
|
clipboard, pMedium, instance, streams, i, E_OUTOFMEMORY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalUnlock(clipboard->hmem);
|
||||||
|
wf_cliprdr_reset_streams(instance);
|
||||||
|
instance->m_pStream = streams;
|
||||||
|
instance->m_nStreams = stream_count;
|
||||||
|
return S_OK;
|
||||||
}
|
}
|
||||||
else if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS))
|
else if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS))
|
||||||
{
|
{
|
||||||
@@ -2161,16 +2239,16 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
/* add to name array */
|
/* add to name array */
|
||||||
clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR));
|
|
||||||
|
|
||||||
if (!clipboard->file_names[clipboard->nFiles])
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
// `MAX_PATH` is long enough for the file name.
|
// `MAX_PATH` is long enough for the file name.
|
||||||
// So we just return FALSE if the file name is too long, which is not a normal case.
|
// So we just return FALSE if the file name is too long, which is not a normal case.
|
||||||
if ((wcslen(full_file_name) + 1) > MAX_PATH)
|
if ((wcslen(full_file_name) + 1) > MAX_PATH)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
|
clipboard->file_names[clipboard->nFiles] = (LPWSTR)calloc(MAX_PATH, sizeof(WCHAR));
|
||||||
|
|
||||||
|
if (!clipboard->file_names[clipboard->nFiles])
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
|
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
|
||||||
/* add to descriptor array */
|
/* add to descriptor array */
|
||||||
clipboard->fileDescriptor[clipboard->nFiles] =
|
clipboard->fileDescriptor[clipboard->nFiles] =
|
||||||
@@ -2778,6 +2856,7 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
clipboard->hmem = NULL;
|
clipboard->hmem = NULL;
|
||||||
|
clipboard->hmem_data_len = 0;
|
||||||
|
|
||||||
if (formatDataResponse->msgFlags != CB_RESPONSE_OK)
|
if (formatDataResponse->msgFlags != CB_RESPONSE_OK)
|
||||||
{
|
{
|
||||||
@@ -2811,6 +2890,7 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clipboard->hmem_data_len = formatDataResponse->dataLen;
|
||||||
clipboard->hmem = hMem;
|
clipboard->hmem = hMem;
|
||||||
rc = CHANNEL_RC_OK;
|
rc = CHANNEL_RC_OK;
|
||||||
} while (0);
|
} while (0);
|
||||||
|
|||||||
@@ -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: 5a78ec4230...a920d00945
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.6"
|
version = "1.4.8"
|
||||||
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"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import optparse
|
import optparse
|
||||||
|
import subprocess
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import brotli
|
import brotli
|
||||||
import datetime
|
import datetime
|
||||||
@@ -65,11 +66,15 @@ def write_app_metadata(output_folder: str):
|
|||||||
print(f"App metadata has been written to {output_path}")
|
print(f"App metadata has been written to {output_path}")
|
||||||
|
|
||||||
def build_portable(output_folder: str, target: str):
|
def build_portable(output_folder: str, target: str):
|
||||||
os.chdir(output_folder)
|
current_dir = os.getcwd()
|
||||||
if target:
|
try:
|
||||||
os.system("cargo build --release --target " + target)
|
os.chdir(output_folder)
|
||||||
else:
|
cmd = ["cargo", "build", "--locked", "--release"]
|
||||||
os.system("cargo build --release")
|
if target:
|
||||||
|
cmd.extend(["--target", target])
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
finally:
|
||||||
|
os.chdir(current_dir)
|
||||||
|
|
||||||
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
|
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
|
||||||
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
|
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
|
||||||
|
self.excluded_window_target = Some((cls.to_owned(), name.to_owned()));
|
||||||
|
if hwnds.is_empty() {
|
||||||
|
self.excluded_windows.clear();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.exclude_windows(&mut hwnds)?;
|
||||||
|
self.excluded_windows = hwnds;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_excluded_windows(&mut self) -> Result<()> {
|
||||||
|
let Some((cls, name)) = self.excluded_window_target.as_ref() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let mut hwnds = find_windows(cls, name)?;
|
||||||
|
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
|
||||||
|
// This runs from frame() because refreshed privacy overlays get new
|
||||||
|
// HWNDs. It is only used on the legacy magnifier backend while privacy
|
||||||
|
// mode is active; if it shows up as hot-path cost, throttle this check.
|
||||||
|
// Keep the previous filter list while privacy windows are being recreated.
|
||||||
|
if hwnds.is_empty() || hwnds == self.excluded_windows {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.exclude_windows(&mut hwnds)?;
|
||||||
|
self.excluded_windows = hwnds;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exclude_windows(&mut self, hwnds: &mut [HWND]) -> Result<bool> {
|
||||||
|
let count = hwnds.len() as _;
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut hwnd = if cls.len() == 0 {
|
|
||||||
FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr())
|
|
||||||
} else {
|
|
||||||
let cls_c = CString::new(cls).unwrap();
|
|
||||||
FindWindowExA(NULL as _, NULL as _, cls_c.as_ptr(), name_c.as_ptr())
|
|
||||||
};
|
|
||||||
|
|
||||||
if hwnd.is_null() {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(set_window_filter_list_func) =
|
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 {
|
||||||
|
|||||||
@@ -276,12 +276,21 @@ impl PipeWireRecorder {
|
|||||||
// see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982
|
// see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982
|
||||||
src.set_property("always-copy", &true)?;
|
src.set_property("always-copy", &true)?;
|
||||||
|
|
||||||
|
// COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink.
|
||||||
|
// xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream
|
||||||
|
// format set is too narrow (appsink only accepts BGRx/RGBx), producing
|
||||||
|
// "no more output formats" / not-negotiated (-4). videoconvert accepts any
|
||||||
|
// system-memory video/x-raw format, widening negotiation so the portal can
|
||||||
|
// settle on a format it can deliver via its SHM path.
|
||||||
|
let convert = gst::ElementFactory::make("videoconvert", None)?;
|
||||||
|
|
||||||
let sink = gst::ElementFactory::make("appsink", None)?;
|
let sink = gst::ElementFactory::make("appsink", None)?;
|
||||||
sink.set_property("drop", &true)?;
|
sink.set_property("drop", &true)?;
|
||||||
sink.set_property("max-buffers", &1u32)?;
|
sink.set_property("max-buffers", &1u32)?;
|
||||||
|
|
||||||
pipeline.add_many(&[&src, &sink])?;
|
pipeline.add_many(&[&src, &convert, &sink])?;
|
||||||
src.link(&sink)?;
|
src.link(&convert)?;
|
||||||
|
convert.link(&sink)?;
|
||||||
|
|
||||||
let appsink = sink
|
let appsink = sink
|
||||||
.dynamic_cast::<AppSink>()
|
.dynamic_cast::<AppSink>()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.4.6
|
pkgver=1.4.8
|
||||||
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" />
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
Some msi packages reset the `VersionNT` value to 1000 on Windows 10.
|
Some msi packages reset the `VersionNT` value to 1000 on Windows 10.
|
||||||
https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html -->
|
https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html -->
|
||||||
<!-- Remote printer also works on Win8.1 in my test. -->
|
<!-- Remote printer also works on Win8.1 in my test. -->
|
||||||
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT >= 603 AND PRINTER = 1 OR PRINTER = "Y" OR PRINTER = "y"" />
|
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT >= 603 AND (PRINTER = 1 OR PRINTER = "Y" OR PRINTER = "y")" />
|
||||||
<Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT >= 603" />
|
<Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT >= 603" />
|
||||||
|
|
||||||
<!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.-->
|
<!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.-->
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
<?include ..\Includes.wxi?>
|
<?include ..\Includes.wxi?>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Properties and related actions for specifying whether to install start menu/desktop shortcuts.
|
Properties and related actions for specifying whether to install shortcuts and the printer.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- These are the actual properties that get used in conditions to determine whether to
|
<!-- These are the actual properties that get used in conditions to determine whether to
|
||||||
install start menu shortcuts, they are initialized with a default value to install shortcuts.
|
install start menu shortcuts or the printer. Shortcut properties default to install;
|
||||||
They should not be set directly from the command line or registry, instead the CREATE* properties
|
PRINTER defaults to not install. The CREATE* properties below update shortcut
|
||||||
below should be set, then they will update these properties with their values only if set. -->
|
properties from command line, bundle, or registry values. -->
|
||||||
<Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property>
|
<Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property>
|
||||||
<Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property>
|
<Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property>
|
||||||
<Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property>
|
<Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property>
|
||||||
<Property Id="PRINTER" Value="1" Secure="yes"></Property>
|
<Property Id="PRINTER" Secure="yes"></Property>
|
||||||
|
|
||||||
<!-- These properties get set from either the command line, bundle or registry value,
|
<!-- These properties get set from either the command line, bundle or registry value,
|
||||||
if set they update the properties above with their value. -->
|
if set they update the properties above with their value. -->
|
||||||
@@ -77,7 +77,11 @@
|
|||||||
<!-- If a command line value or registry value was set, update the main properties with the value -->
|
<!-- If a command line value or registry value was set, update the main properties with the value -->
|
||||||
<SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = "Y" OR CREATESTARTMENUSHORTCUTS = "y")" />
|
<SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = "Y" OR CREATESTARTMENUSHORTCUTS = "y")" />
|
||||||
<SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = "Y" OR CREATEDESKTOPSHORTCUTS = "y")" />
|
<SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = "Y" OR CREATEDESKTOPSHORTCUTS = "y")" />
|
||||||
<SetProperty Id="PRINTER" Value="" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = "Y" OR INSTALLPRINTER = "y")" />
|
<!-- PRINTER defaults to empty now, so a saved or command-line INSTALLPRINTER=1
|
||||||
|
must explicitly enable the main PRINTER property. Non-truthy INSTALLPRINTER
|
||||||
|
values still clear PRINTER so upgrades preserve an explicit disabled choice. -->
|
||||||
|
<SetProperty Action="SetPrinterValueEnabled" Id="PRINTER" Value="1" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER = 1 OR INSTALLPRINTER = "Y" OR INSTALLPRINTER = "y"" />
|
||||||
|
<SetProperty Action="SetPrinterValueDisabled" Id="PRINTER" Value="" After="SetPrinterValueEnabled" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = "Y" OR INSTALLPRINTER = "y")" />
|
||||||
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
</Wix>
|
</Wix>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -300,4 +300,4 @@ b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a6
|
|||||||
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
|
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
|
||||||
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
|
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
|
||||||
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
|
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
|
||||||
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
|
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
echo $MACOS_CODESIGN_IDENTITY
|
echo $MACOS_CODESIGN_IDENTITY
|
||||||
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid
|
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid --locked
|
||||||
cd flutter; flutter pub get; cd -
|
cd flutter; flutter pub get; cd -
|
||||||
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
|
||||||
./build.py --flutter
|
./build.py --flutter
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.6
|
Version: 1.4.8
|
||||||
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.6
|
Version: 1.4.8
|
||||||
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.6
|
Version: 1.4.8
|
||||||
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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
21
src/cli.rs
21
src/cli.rs
@@ -3,7 +3,7 @@ use async_trait::async_trait;
|
|||||||
use hbb_common::{
|
use hbb_common::{
|
||||||
config::PeerConfig,
|
config::PeerConfig,
|
||||||
config::READ_TIMEOUT,
|
config::READ_TIMEOUT,
|
||||||
futures::StreamExt,
|
futures::{SinkExt, StreamExt},
|
||||||
log,
|
log,
|
||||||
message_proto::*,
|
message_proto::*,
|
||||||
protobuf::Message as _,
|
protobuf::Message as _,
|
||||||
@@ -46,7 +46,6 @@ impl Session {
|
|||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
);
|
);
|
||||||
session
|
session
|
||||||
}
|
}
|
||||||
@@ -54,7 +53,7 @@ impl Session {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Interface for Session {
|
impl Interface for Session {
|
||||||
fn get_lch(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
||||||
return self.lc.clone();
|
return self.lc.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,20 +61,14 @@ impl Interface for Session {
|
|||||||
match msgtype {
|
match msgtype {
|
||||||
"input-password" => {
|
"input-password" => {
|
||||||
self.sender
|
self.sender
|
||||||
.send(Data::Login((
|
.send(Data::Login((self.password.clone(), true)))
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
self.password.clone(),
|
|
||||||
true,
|
|
||||||
)))
|
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
"re-input-password" => {
|
"re-input-password" => {
|
||||||
log::error!("{}: {}", title, text);
|
log::error!("{}: {}", title, text);
|
||||||
match rpassword::prompt_password("Enter password: ") {
|
match rpassword::prompt_password("Enter password: ") {
|
||||||
Ok(password) => {
|
Ok(password) => {
|
||||||
let login_data =
|
let login_data = Data::Login((password, true));
|
||||||
Data::Login((String::new(), String::new(), password, true));
|
|
||||||
self.sender.send(login_data).ok();
|
self.sender.send(login_data).ok();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -100,8 +93,6 @@ impl Interface for Session {
|
|||||||
self.lc.write().unwrap().handle_peer_info(&pi);
|
self.lc.write().unwrap().handle_peer_info(&pi);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_multiple_windows_session(&self, _sessions: Vec<WindowsSession>) {}
|
|
||||||
|
|
||||||
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
|
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
|
||||||
log::info!(
|
log::info!(
|
||||||
"password={}",
|
"password={}",
|
||||||
@@ -146,8 +137,8 @@ pub async fn connect_test(id: &str, key: String, token: String) {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Failed to connect {}: {}", &id, err);
|
log::error!("Failed to connect {}: {}", &id, err);
|
||||||
}
|
}
|
||||||
Ok(((mut stream, _direct, _secure, _kcp, _typ), direct)) => {
|
Ok((mut stream, direct)) => {
|
||||||
log::info!("direct: {:?}", direct);
|
log::info!("direct: {}", direct);
|
||||||
// rpassword::prompt_password("Input anything to exit").ok();
|
// rpassword::prompt_password("Input anything to exit").ok();
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|||||||
313
src/client.rs
313
src/client.rs
@@ -65,12 +65,11 @@ use hbb_common::{
|
|||||||
self,
|
self,
|
||||||
net::UdpSocket,
|
net::UdpSocket,
|
||||||
sync::{
|
sync::{
|
||||||
mpsc::{error::TryRecvError, unbounded_channel, UnboundedReceiver},
|
mpsc::{unbounded_channel, UnboundedReceiver},
|
||||||
oneshot,
|
oneshot,
|
||||||
},
|
},
|
||||||
time::{interval, Duration, Instant},
|
time::{interval, Duration, Instant},
|
||||||
},
|
},
|
||||||
webrtc::WebRTCStream,
|
|
||||||
AddrMangle, ResultType, Stream,
|
AddrMangle, ResultType, Stream,
|
||||||
};
|
};
|
||||||
pub use helper::*;
|
pub use helper::*;
|
||||||
@@ -97,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;
|
||||||
|
|
||||||
@@ -331,19 +332,6 @@ impl Client {
|
|||||||
} else {
|
} else {
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
let ipv6 = if crate::get_ipv6_punch_enabled() {
|
|
||||||
crate::get_ipv6_socket().await
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let webrtc_offerer =
|
|
||||||
match WebRTCStream::new("", interface.is_force_relay(), CONNECT_TIMEOUT).await {
|
|
||||||
Ok(stream) => Some(stream),
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("webrtc offerer setup failed: {}", err);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let fut = Self::_start_inner(
|
let fut = Self::_start_inner(
|
||||||
peer.to_owned(),
|
peer.to_owned(),
|
||||||
key.to_owned(),
|
key.to_owned(),
|
||||||
@@ -352,8 +340,6 @@ impl Client {
|
|||||||
interface.clone(),
|
interface.clone(),
|
||||||
udp.clone(),
|
udp.clone(),
|
||||||
Some(stop_udp_tx),
|
Some(stop_udp_tx),
|
||||||
ipv6,
|
|
||||||
webrtc_offerer,
|
|
||||||
rendezvous_server.clone(),
|
rendezvous_server.clone(),
|
||||||
servers.clone(),
|
servers.clone(),
|
||||||
contained,
|
contained,
|
||||||
@@ -371,8 +357,6 @@ impl Client {
|
|||||||
interface,
|
interface,
|
||||||
(None, None),
|
(None, None),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
None,
|
|
||||||
rendezvous_server,
|
rendezvous_server,
|
||||||
servers,
|
servers,
|
||||||
contained,
|
contained,
|
||||||
@@ -384,67 +368,6 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_expected_webrtc_ice_candidate(ice: &IceCandidate, session_key: &str) -> bool {
|
|
||||||
!session_key.is_empty() && ice.session_key == session_key && !ice.candidate.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_webrtc_ice_bridge(
|
|
||||||
mut socket: Stream,
|
|
||||||
mut local_ice_rx: Option<UnboundedReceiver<String>>,
|
|
||||||
webrtc: WebRTCStream,
|
|
||||||
peer: String,
|
|
||||||
session_key: String,
|
|
||||||
) -> oneshot::Sender<()> {
|
|
||||||
let (stop_tx, mut stop_rx) = oneshot::channel::<()>();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
match stop_rx.try_recv() {
|
|
||||||
Ok(_) | Err(tokio::sync::oneshot::error::TryRecvError::Closed) => break,
|
|
||||||
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(rx) = local_ice_rx.as_mut() {
|
|
||||||
loop {
|
|
||||||
match rx.try_recv() {
|
|
||||||
Ok(candidate) => {
|
|
||||||
let mut msg = RendezvousMessage::new();
|
|
||||||
msg.set_ice_candidate(IceCandidate {
|
|
||||||
id: peer.clone(),
|
|
||||||
session_key: session_key.clone(),
|
|
||||||
candidate,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
if let Err(err) = socket.send(&msg).await {
|
|
||||||
log::warn!("failed to send WebRTC ICE candidate: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(TryRecvError::Empty) => break,
|
|
||||||
Err(TryRecvError::Disconnected) => {
|
|
||||||
local_ice_rx = None;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(msg_in) =
|
|
||||||
crate::get_next_nonkeyexchange_msg(&mut socket, Some(100)).await
|
|
||||||
{
|
|
||||||
if let Some(rendezvous_message::Union::IceCandidate(ice)) = msg_in.union {
|
|
||||||
if Self::is_expected_webrtc_ice_candidate(&ice, &session_key) {
|
|
||||||
if let Err(err) = webrtc.add_remote_ice_candidate(&ice.candidate).await
|
|
||||||
{
|
|
||||||
log::warn!("failed to add WebRTC ICE candidate: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stop_tx
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _start_inner(
|
async fn _start_inner(
|
||||||
peer: String,
|
peer: String,
|
||||||
key: String,
|
key: String,
|
||||||
@@ -453,8 +376,6 @@ impl Client {
|
|||||||
interface: impl Interface,
|
interface: impl Interface,
|
||||||
mut udp: (Option<Arc<UdpSocket>>, Option<Arc<Mutex<u16>>>),
|
mut udp: (Option<Arc<UdpSocket>>, Option<Arc<Mutex<u16>>>),
|
||||||
stop_udp_tx: Option<oneshot::Sender<()>>,
|
stop_udp_tx: Option<oneshot::Sender<()>>,
|
||||||
mut ipv6: Option<(Arc<UdpSocket>, bytes::Bytes)>,
|
|
||||||
mut webrtc_offerer: Option<WebRTCStream>,
|
|
||||||
mut rendezvous_server: String,
|
mut rendezvous_server: String,
|
||||||
servers: Vec<String>,
|
servers: Vec<String>,
|
||||||
contained: bool,
|
contained: bool,
|
||||||
@@ -527,20 +448,14 @@ impl Client {
|
|||||||
// Stop UDP NAT test task if still running
|
// Stop UDP NAT test task if still running
|
||||||
stop_udp_tx.map(|tx| tx.send(()));
|
stop_udp_tx.map(|tx| tx.send(()));
|
||||||
let mut msg_out = RendezvousMessage::new();
|
let mut msg_out = RendezvousMessage::new();
|
||||||
let mut ipv6 = ipv6
|
let mut ipv6 = if crate::get_ipv6_punch_enabled() {
|
||||||
.take()
|
if let Some((socket, addr)) = crate::get_ipv6_socket().await {
|
||||||
.map(|(socket, addr)| (Some(socket), Some(addr)))
|
(Some(socket), Some(addr))
|
||||||
.unwrap_or((None, None));
|
} else {
|
||||||
let webrtc_sdp_offer = if let Some(webrtc) = webrtc_offerer.as_ref() {
|
(None, None)
|
||||||
match webrtc.get_local_endpoint().await {
|
|
||||||
Ok(endpoint) => endpoint,
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("failed to read local WebRTC offer: {}", err);
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
(None, None)
|
||||||
};
|
};
|
||||||
let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0);
|
let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0);
|
||||||
let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" };
|
let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" };
|
||||||
@@ -554,16 +469,9 @@ impl Client {
|
|||||||
udp_port: udp_nat_port as _,
|
udp_port: udp_nat_port as _,
|
||||||
force_relay: interface.is_force_relay(),
|
force_relay: interface.is_force_relay(),
|
||||||
socket_addr_v6: ipv6.1.unwrap_or_default(),
|
socket_addr_v6: ipv6.1.unwrap_or_default(),
|
||||||
webrtc_sdp_offer: webrtc_sdp_offer.clone(),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
let webrtc_session_key = webrtc_offerer
|
for i in 1..=3 {
|
||||||
.as_ref()
|
|
||||||
.map(|webrtc| webrtc.session_key().to_owned())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let mut webrtc_sdp_answer = String::new();
|
|
||||||
let mut pending_webrtc_ice = Vec::<String>::new();
|
|
||||||
'punch_attempts: for i in 1..=3 {
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"#{} {} punch attempt with {}, id: {}",
|
"#{} {} punch attempt with {}, id: {}",
|
||||||
i,
|
i,
|
||||||
@@ -573,20 +481,9 @@ impl Client {
|
|||||||
);
|
);
|
||||||
socket.send(&msg_out).await?;
|
socket.send(&msg_out).await?;
|
||||||
// below timeout should not bigger than hbbs's connection timeout.
|
// below timeout should not bigger than hbbs's connection timeout.
|
||||||
let attempt_deadline = Instant::now() + Duration::from_millis((i * 3000) as u64);
|
if let Some(msg_in) =
|
||||||
loop {
|
crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await
|
||||||
let remaining = attempt_deadline.saturating_duration_since(Instant::now());
|
{
|
||||||
if remaining.is_zero() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let timeout_ms = remaining
|
|
||||||
.as_millis()
|
|
||||||
.clamp(1, u64::MAX as u128) as u64;
|
|
||||||
let Some(msg_in) =
|
|
||||||
crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout_ms)).await
|
|
||||||
else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
match msg_in.union {
|
match msg_in.union {
|
||||||
Some(rendezvous_message::Union::PunchHoleResponse(ph)) => {
|
Some(rendezvous_message::Union::PunchHoleResponse(ph)) => {
|
||||||
if ph.socket_addr.is_empty() {
|
if ph.socket_addr.is_empty() {
|
||||||
@@ -615,7 +512,6 @@ impl Client {
|
|||||||
relay_server = ph.relay_server;
|
relay_server = ph.relay_server;
|
||||||
peer_addr = AddrMangle::decode(&ph.socket_addr);
|
peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||||
feedback = ph.feedback;
|
feedback = ph.feedback;
|
||||||
webrtc_sdp_answer = ph.webrtc_sdp_answer;
|
|
||||||
let s = udp.0.take();
|
let s = udp.0.take();
|
||||||
if ph.is_udp && s.is_some() {
|
if ph.is_udp && s.is_some() {
|
||||||
if let Some(s) = s {
|
if let Some(s) = s {
|
||||||
@@ -634,7 +530,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr);
|
log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr);
|
||||||
break 'punch_attempts;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(rendezvous_message::Union::RelayResponse(rr)) => {
|
Some(rendezvous_message::Union::RelayResponse(rr)) => {
|
||||||
@@ -655,38 +551,6 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
signed_id_pk = rr.pk().into();
|
signed_id_pk = rr.pk().into();
|
||||||
let mut webrtc_bridge_stop = None;
|
|
||||||
let mut webrtc_for_connect = None;
|
|
||||||
if !rr.webrtc_sdp_answer.is_empty() {
|
|
||||||
if let Some(webrtc) = webrtc_offerer.take() {
|
|
||||||
if let Err(err) =
|
|
||||||
webrtc.set_remote_endpoint(&rr.webrtc_sdp_answer).await
|
|
||||||
{
|
|
||||||
log::warn!("failed to set WebRTC relay answer: {}", err);
|
|
||||||
} else {
|
|
||||||
for candidate in pending_webrtc_ice.drain(..) {
|
|
||||||
if let Err(err) =
|
|
||||||
webrtc.add_remote_ice_candidate(&candidate).await
|
|
||||||
{
|
|
||||||
log::warn!(
|
|
||||||
"failed to add buffered WebRTC ICE candidate: {}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let session_key = webrtc.session_key().to_owned();
|
|
||||||
let local_ice_rx = webrtc.take_local_ice_rx();
|
|
||||||
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
|
|
||||||
socket,
|
|
||||||
local_ice_rx,
|
|
||||||
webrtc.clone(),
|
|
||||||
peer.clone(),
|
|
||||||
session_key,
|
|
||||||
));
|
|
||||||
webrtc_for_connect = Some(webrtc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let fut = Self::create_relay(
|
let fut = Self::create_relay(
|
||||||
&peer,
|
&peer,
|
||||||
rr.uuid,
|
rr.uuid,
|
||||||
@@ -702,81 +566,30 @@ impl Client {
|
|||||||
}
|
}
|
||||||
.boxed(),
|
.boxed(),
|
||||||
);
|
);
|
||||||
if let Some(mut webrtc) = webrtc_for_connect {
|
|
||||||
connect_futures.push(
|
|
||||||
async move {
|
|
||||||
webrtc.wait_connected(CONNECT_TIMEOUT).await?;
|
|
||||||
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
|
|
||||||
}
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Run all connection attempts concurrently, return the first successful one
|
// Run all connection attempts concurrently, return the first successful one
|
||||||
let (conn, kcp, typ) = match select_ok(connect_futures).await {
|
let (conn, kcp, typ) = match select_ok(connect_futures).await {
|
||||||
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
|
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
|
||||||
|
|
||||||
Err(e) => (Err(e), None, ""),
|
Err(e) => (Err(e), None, ""),
|
||||||
};
|
};
|
||||||
if let Some(stop) = webrtc_bridge_stop {
|
|
||||||
let _ = stop.send(());
|
|
||||||
}
|
|
||||||
let mut conn = conn?;
|
let mut conn = conn?;
|
||||||
feedback = rr.feedback;
|
feedback = rr.feedback;
|
||||||
log::info!("{:?} used to establish {typ} connection", start.elapsed());
|
log::info!("{:?} used to establish {typ} connection", start.elapsed());
|
||||||
let pk =
|
let pk =
|
||||||
Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?;
|
Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?;
|
||||||
return Ok((
|
return Ok((
|
||||||
(conn, typ == "IPv6" || typ == "WebRTC", pk, kcp, typ),
|
(conn, typ == "IPv6", pk, kcp, typ),
|
||||||
(feedback, rendezvous_server),
|
(feedback, rendezvous_server),
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Some(rendezvous_message::Union::IceCandidate(ice)) => {
|
|
||||||
if Self::is_expected_webrtc_ice_candidate(&ice, &webrtc_session_key) {
|
|
||||||
pending_webrtc_ice.push(ice.candidate);
|
|
||||||
} else {
|
|
||||||
log::debug!(
|
|
||||||
"dropping ICE candidate for unexpected WebRTC session key {}",
|
|
||||||
ice.session_key,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
|
log::error!("Unexpected protobuf msg received: {:?}", msg_in);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut webrtc_bridge_stop = None;
|
drop(socket);
|
||||||
let mut webrtc_for_connect = None;
|
|
||||||
if !webrtc_sdp_answer.is_empty() {
|
|
||||||
if let Some(webrtc) = webrtc_offerer.take() {
|
|
||||||
if let Err(err) = webrtc.set_remote_endpoint(&webrtc_sdp_answer).await {
|
|
||||||
log::warn!("failed to set WebRTC answer: {}", err);
|
|
||||||
drop(socket);
|
|
||||||
} else {
|
|
||||||
for candidate in pending_webrtc_ice.drain(..) {
|
|
||||||
if let Err(err) = webrtc.add_remote_ice_candidate(&candidate).await {
|
|
||||||
log::warn!("failed to add buffered WebRTC ICE candidate: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let session_key = webrtc.session_key().to_owned();
|
|
||||||
let local_ice_rx = webrtc.take_local_ice_rx();
|
|
||||||
webrtc_bridge_stop = Some(Self::spawn_webrtc_ice_bridge(
|
|
||||||
socket,
|
|
||||||
local_ice_rx,
|
|
||||||
webrtc.clone(),
|
|
||||||
peer.clone(),
|
|
||||||
session_key,
|
|
||||||
));
|
|
||||||
webrtc_for_connect = Some(webrtc);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drop(socket);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
drop(socket);
|
|
||||||
}
|
|
||||||
if peer_addr.port() == 0 {
|
if peer_addr.port() == 0 {
|
||||||
bail!("Failed to connect via rendezvous server");
|
bail!("Failed to connect via rendezvous server");
|
||||||
}
|
}
|
||||||
@@ -810,8 +623,6 @@ impl Client {
|
|||||||
interface,
|
interface,
|
||||||
udp.0,
|
udp.0,
|
||||||
ipv6.0,
|
ipv6.0,
|
||||||
webrtc_for_connect,
|
|
||||||
webrtc_bridge_stop,
|
|
||||||
punch_type,
|
punch_type,
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
@@ -838,8 +649,6 @@ impl Client {
|
|||||||
interface: impl Interface,
|
interface: impl Interface,
|
||||||
udp_socket_nat: Option<Arc<UdpSocket>>,
|
udp_socket_nat: Option<Arc<UdpSocket>>,
|
||||||
udp_socket_v6: Option<Arc<UdpSocket>>,
|
udp_socket_v6: Option<Arc<UdpSocket>>,
|
||||||
webrtc_offerer: Option<WebRTCStream>,
|
|
||||||
webrtc_bridge_stop: Option<oneshot::Sender<()>>,
|
|
||||||
punch_type: &str,
|
punch_type: &str,
|
||||||
) -> ResultType<(
|
) -> ResultType<(
|
||||||
Stream,
|
Stream,
|
||||||
@@ -898,23 +707,11 @@ impl Client {
|
|||||||
if let Some(udp_socket_v6) = udp_socket_v6 {
|
if let Some(udp_socket_v6) = udp_socket_v6 {
|
||||||
connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed());
|
connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed());
|
||||||
}
|
}
|
||||||
if let Some(mut webrtc) = webrtc_offerer {
|
|
||||||
connect_futures.push(
|
|
||||||
async move {
|
|
||||||
webrtc.wait_connected(connect_timeout).await?;
|
|
||||||
Ok((Stream::WebRTC(webrtc), None, "WebRTC"))
|
|
||||||
}
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Run all connection attempts concurrently, return the first successful one
|
// Run all connection attempts concurrently, return the first successful one
|
||||||
let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await {
|
let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await {
|
||||||
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
|
Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2),
|
||||||
Err(e) => (Err(e), None, ""),
|
Err(e) => (Err(e), None, ""),
|
||||||
};
|
};
|
||||||
if let Some(stop) = webrtc_bridge_stop {
|
|
||||||
let _ = stop.send(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut direct = !conn.is_err();
|
let mut direct = !conn.is_err();
|
||||||
if interface.is_force_relay() || conn.is_err() {
|
if interface.is_force_relay() || conn.is_err() {
|
||||||
@@ -1144,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1945,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,
|
||||||
@@ -2054,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
|
||||||
@@ -2984,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;
|
||||||
@@ -3923,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";
|
||||||
@@ -4325,22 +4160,8 @@ pub mod peer_online {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::client::Client;
|
|
||||||
use hbb_common::rendezvous_proto::IceCandidate;
|
|
||||||
use hbb_common::tokio;
|
use hbb_common::tokio;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn accepts_webrtc_ice_by_session_key_only() {
|
|
||||||
let ice = IceCandidate {
|
|
||||||
session_key: "session-a".to_owned(),
|
|
||||||
candidate: "candidate-json".to_owned(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(Client::is_expected_webrtc_ice_candidate(&ice, "session-a"));
|
|
||||||
assert!(!Client::is_expected_webrtc_ice_candidate(&ice, "session-b"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_query_onlines() {
|
async fn test_query_onlines() {
|
||||||
super::query_online_states(
|
super::query_online_states(
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ 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);
|
||||||
#[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 +157,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
|
||||||
@@ -219,6 +222,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 +248,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 +283,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;
|
||||||
@@ -350,6 +360,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1426,7 +1438,11 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
self.handler.set_cursor_position(cp);
|
self.handler.set_cursor_position(cp);
|
||||||
}
|
}
|
||||||
Some(message::Union::Clipboard(cb)) => {
|
Some(message::Union::Clipboard(cb)) => {
|
||||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
let clipboard_allowed = {
|
||||||
|
let lc = self.handler.lc.read().unwrap();
|
||||||
|
!lc.disable_clipboard.v && !lc.view_only.v
|
||||||
|
};
|
||||||
|
if clipboard_allowed {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
update_clipboard(vec![cb], ClipboardSide::Client);
|
update_clipboard(vec![cb], ClipboardSide::Client);
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
@@ -1445,7 +1461,11 @@ impl<T: InvokeUiSession> Remote<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(message::Union::MultiClipboards(_mcb)) => {
|
Some(message::Union::MultiClipboards(_mcb)) => {
|
||||||
if !self.handler.lc.read().unwrap().disable_clipboard.v {
|
let clipboard_allowed = {
|
||||||
|
let lc = self.handler.lc.read().unwrap();
|
||||||
|
!lc.disable_clipboard.v && !lc.view_only.v
|
||||||
|
};
|
||||||
|
if clipboard_allowed {
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
update_clipboard(_mcb.clipboards, ClipboardSide::Client);
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
|
|||||||
162
src/clipboard.rs
162
src/clipboard.rs
@@ -1,5 +1,7 @@
|
|||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use arboard::{ClipboardData, ClipboardFormat};
|
use arboard::{ClipboardData, ClipboardFormat};
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use arboard::{LinuxClipboardKind, SetExtLinux};
|
||||||
use hbb_common::{bail, log, message_proto::*, ResultType};
|
use hbb_common::{bail, log, message_proto::*, ResultType};
|
||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
@@ -54,6 +56,27 @@ pub fn check_clipboard(
|
|||||||
side: ClipboardSide,
|
side: ClipboardSide,
|
||||||
force: bool,
|
force: bool,
|
||||||
) -> Option<Message> {
|
) -> Option<Message> {
|
||||||
|
let (msg, clipboards) = read_clipboard_message(ctx, side, force)?;
|
||||||
|
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
|
||||||
|
Some(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn peek_clipboard(
|
||||||
|
ctx: &mut Option<ClipboardContext>,
|
||||||
|
side: ClipboardSide,
|
||||||
|
force: bool,
|
||||||
|
) -> Option<Message> {
|
||||||
|
let (msg, _) = read_clipboard_message(ctx, side, force)?;
|
||||||
|
Some(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
fn read_clipboard_message(
|
||||||
|
ctx: &mut Option<ClipboardContext>,
|
||||||
|
side: ClipboardSide,
|
||||||
|
force: bool,
|
||||||
|
) -> Option<(Message, MultiClipboards)> {
|
||||||
if ctx.is_none() {
|
if ctx.is_none() {
|
||||||
*ctx = ClipboardContext::new().ok();
|
*ctx = ClipboardContext::new().ok();
|
||||||
}
|
}
|
||||||
@@ -64,8 +87,7 @@ pub fn check_clipboard(
|
|||||||
let mut msg = Message::new();
|
let mut msg = Message::new();
|
||||||
let clipboards = proto::create_multi_clipboards(content);
|
let clipboards = proto::create_multi_clipboards(content);
|
||||||
msg.set_multi_clipboards(clipboards.clone());
|
msg.set_multi_clipboards(clipboards.clone());
|
||||||
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
|
return Some((msg, clipboards));
|
||||||
return Some(msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -129,41 +151,50 @@ 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 || {
|
||||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) {
|
||||||
if ctx.is_none() {
|
log::error!("Failed to empty clipboard files: {}", e);
|
||||||
match ClipboardContext::new() {
|
|
||||||
Ok(x) => {
|
|
||||||
*ctx = Some(x);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to create clipboard context: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
if let Some(mut ctx) = ctx.as_mut() {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
use clipboard::platform::unix;
|
|
||||||
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
|
||||||
ctx.try_empty_clipboard_files(_side);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
ctx.try_empty_clipboard_files(_side);
|
|
||||||
// No need to make sure the context is enabled.
|
|
||||||
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
|
||||||
context.empty_clipboard(_conn_id).ok();
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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();
|
||||||
|
if ctx.is_none() {
|
||||||
|
match ClipboardContext::new() {
|
||||||
|
Ok(x) => {
|
||||||
|
*ctx = Some(x);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to create clipboard context: {}", e);
|
||||||
|
bail!("Failed to create clipboard context: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
if let Some(mut ctx) = ctx.as_mut() {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use clipboard::platform::unix;
|
||||||
|
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
|
||||||
|
ctx.try_empty_clipboard_files(_side);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
ctx.try_empty_clipboard_files(_side);
|
||||||
|
// No need to make sure the context is enabled.
|
||||||
|
clipboard::ContextSend::proc(|context| -> ResultType<()> {
|
||||||
|
if !context.empty_clipboard(_conn_id)? {
|
||||||
|
bail!("Failed to empty clipboard files for conn_id {}", _conn_id);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) {
|
pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) {
|
||||||
log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id);
|
log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id);
|
||||||
@@ -219,10 +250,7 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(ctx) = ctx.as_mut() {
|
if let Some(ctx) = ctx.as_mut() {
|
||||||
to_update_data.push(ClipboardData::Special((
|
to_update_data = append_owner_marker(to_update_data, side);
|
||||||
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
|
|
||||||
side.get_owner_data(),
|
|
||||||
)));
|
|
||||||
if let Err(e) = ctx.set(&to_update_data) {
|
if let Err(e) = ctx.set(&to_update_data) {
|
||||||
log::debug!("Failed to set clipboard: {}", e);
|
log::debug!("Failed to set clipboard: {}", e);
|
||||||
} else {
|
} else {
|
||||||
@@ -231,6 +259,29 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
fn append_owner_marker(mut data: Vec<ClipboardData>, side: ClipboardSide) -> Vec<ClipboardData> {
|
||||||
|
data.push(ClipboardData::Special((
|
||||||
|
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
|
||||||
|
side.get_owner_data(),
|
||||||
|
)));
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn set_text_clipboard_with_owner_sync(text: &str, side: ClipboardSide) -> ResultType<()> {
|
||||||
|
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||||
|
if ctx.is_none() {
|
||||||
|
*ctx = Some(ClipboardContext::new()?);
|
||||||
|
}
|
||||||
|
let clipboard_ctx = match ctx.as_mut() {
|
||||||
|
Some(ctx) => ctx,
|
||||||
|
None => bail!("Failed to create clipboard context"),
|
||||||
|
};
|
||||||
|
let data = append_owner_marker(vec![ClipboardData::Text(text.to_owned())], side);
|
||||||
|
clipboard_ctx.set_with_owner_marker_for_linux(&data)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -382,6 +433,24 @@ impl ClipboardContext {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn set_with_owner_marker_for_linux(&mut self, data: &[ClipboardData]) -> ResultType<()> {
|
||||||
|
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||||
|
self.inner
|
||||||
|
.set()
|
||||||
|
.clipboard(LinuxClipboardKind::Clipboard)
|
||||||
|
.formats(data)?;
|
||||||
|
if let Err(e) = self
|
||||||
|
.inner
|
||||||
|
.set()
|
||||||
|
.clipboard(LinuxClipboardKind::Primary)
|
||||||
|
.formats(data)
|
||||||
|
{
|
||||||
|
log::warn!("Failed to set PRIMARY clipboard with owner marker: {}", e);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
|
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
|
||||||
fn get_file_urls_set_by_rustdesk(
|
fn get_file_urls_set_by_rustdesk(
|
||||||
data: Vec<ClipboardData>,
|
data: Vec<ClipboardData>,
|
||||||
@@ -808,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 {
|
||||||
@@ -833,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 {
|
||||||
|
|||||||
192
src/core_main.rs
192
src/core_main.rs
@@ -199,6 +199,20 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
std::thread::spawn(move || crate::start_server(false, no_server));
|
std::thread::spawn(move || crate::start_server(false, no_server));
|
||||||
} else {
|
} else {
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
// Root CLI management commands must talk to the user `--server` main IPC.
|
||||||
|
// Example: `sudo rustdesk --option custom-rendezvous-server` should query the
|
||||||
|
// user's IPC instead of root's `/tmp/<app>-0/ipc`; `connect()` still limits this
|
||||||
|
// routing to empty-postfix main IPC only.
|
||||||
|
let _user_main_ipc_scope = if crate::platform::is_installed()
|
||||||
|
&& is_root()
|
||||||
|
&& is_user_main_ipc_scope_cli_command(&args)
|
||||||
|
{
|
||||||
|
Some(crate::ipc::UserMainIpcScope::new())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
use crate::platform;
|
use crate::platform;
|
||||||
@@ -248,11 +262,9 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
if config::is_disable_installation() {
|
if config::is_disable_installation() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
let (printer_override, debug) = parse_silent_install_args(&args);
|
||||||
let options = "desktopicon startmenu";
|
let options = platform::get_silent_install_options(printer_override);
|
||||||
#[cfg(windows)]
|
let res = platform::install_me(options, "".to_owned(), true, debug);
|
||||||
let options = "desktopicon startmenu printer";
|
|
||||||
let res = platform::install_me(options, "".to_owned(), true, args.len() > 1);
|
|
||||||
let text = match res {
|
let text = match res {
|
||||||
Ok(_) => translate("Installation Successful!".to_string()),
|
Ok(_) => translate("Installation Successful!".to_string()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -418,7 +430,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
} else if args[0] == "--password" {
|
} else if args[0] == "--password" {
|
||||||
if config::is_disable_settings() {
|
if is_cli_setting_change_disabled() {
|
||||||
println!("Settings are disabled!");
|
println!("Settings are disabled!");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -460,7 +472,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
println!("{}", crate::ipc::get_id());
|
println!("{}", crate::ipc::get_id());
|
||||||
return None;
|
return None;
|
||||||
} else if args[0] == "--set-id" {
|
} else if args[0] == "--set-id" {
|
||||||
if config::is_disable_settings() {
|
if is_cli_setting_change_disabled() {
|
||||||
println!("Settings are disabled!");
|
println!("Settings are disabled!");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -507,7 +519,7 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
} else if args[0] == "--option" {
|
} else if args[0] == "--option" {
|
||||||
if config::is_disable_settings() {
|
if is_cli_setting_change_disabled() {
|
||||||
println!("Settings are disabled!");
|
println!("Settings are disabled!");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -630,6 +642,8 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
} else if args[0] == "--deploy" {
|
} else if args[0] == "--deploy" {
|
||||||
if config::Config::no_register_device() {
|
if config::Config::no_register_device() {
|
||||||
println!("Cannot deploy an unregistrable device!");
|
println!("Cannot deploy an unregistrable device!");
|
||||||
|
} else if config::is_outgoing_only() {
|
||||||
|
println!("Cannot deploy Outgoing-only clients.");
|
||||||
} else if crate::platform::is_installed() && is_root() {
|
} else if crate::platform::is_installed() && is_root() {
|
||||||
let max = args.len() - 1;
|
let max = args.len() - 1;
|
||||||
let pos = args.iter().position(|x| x == "--token").unwrap_or(max);
|
let pos = args.iter().position(|x| x == "--token").unwrap_or(max);
|
||||||
@@ -647,72 +661,28 @@ pub fn core_main() -> Option<Vec<String>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let new_id = get_value("--id");
|
let new_id = get_value("--id");
|
||||||
let local_id = crate::ipc::get_id();
|
match crate::ui_interface::deploy_device(token, new_id) {
|
||||||
let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone());
|
crate::ui_interface::DeployResult::Ok => {
|
||||||
let uuid = crate::encode64(hbb_common::get_uuid());
|
println!("Device deployed.");
|
||||||
let pk = crate::encode64(
|
|
||||||
hbb_common::config::Config::get_key_pair().1,
|
|
||||||
);
|
|
||||||
let body = serde_json::json!({
|
|
||||||
"id": id_to_deploy,
|
|
||||||
"uuid": uuid,
|
|
||||||
"pk": pk,
|
|
||||||
});
|
|
||||||
let header = "Authorization: Bearer ".to_owned() + &token;
|
|
||||||
let url = crate::ui_interface::get_api_server() + "/api/devices/deploy";
|
|
||||||
match crate::post_request_sync(url, body.to_string(), &header) {
|
|
||||||
Err(err) => {
|
|
||||||
println!("Request failed: {}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
}
|
||||||
Ok(text) => {
|
crate::ui_interface::DeployResult::NotEnabled => {
|
||||||
let parsed: serde_json::Value =
|
println!("Server does not require deployment.");
|
||||||
serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
|
std::process::exit(3);
|
||||||
let result = parsed["result"].as_str().unwrap_or("");
|
}
|
||||||
match result {
|
crate::ui_interface::DeployResult::InvalidInput => {
|
||||||
"OK" => {
|
println!("Invalid input.");
|
||||||
if let Some(ref new_id) = new_id {
|
std::process::exit(5);
|
||||||
if *new_id != local_id {
|
}
|
||||||
if let Err(err) =
|
crate::ui_interface::DeployResult::IdTaken(id) => {
|
||||||
crate::ipc::set_config("id", new_id.clone())
|
println!(
|
||||||
{
|
"Id `{}` is already used by another machine on the server.",
|
||||||
println!(
|
id
|
||||||
"Failed to persist deployed id locally: {}",
|
);
|
||||||
err
|
std::process::exit(6);
|
||||||
);
|
}
|
||||||
std::process::exit(1);
|
crate::ui_interface::DeployResult::Error(err) => {
|
||||||
}
|
println!("{}", err);
|
||||||
}
|
std::process::exit(1);
|
||||||
}
|
|
||||||
if let Err(err) = crate::ipc::notify_deployed() {
|
|
||||||
log::warn!("Failed to notify deployed state: {}", err);
|
|
||||||
}
|
|
||||||
println!("Device deployed.");
|
|
||||||
}
|
|
||||||
"NOT_ENABLED" => {
|
|
||||||
println!("Server does not require deployment.");
|
|
||||||
std::process::exit(3);
|
|
||||||
}
|
|
||||||
"INVALID_INPUT" => {
|
|
||||||
println!("Invalid input.");
|
|
||||||
std::process::exit(5);
|
|
||||||
}
|
|
||||||
"ID_TAKEN" => {
|
|
||||||
println!(
|
|
||||||
"Id `{}` is already used by another machine on the server.",
|
|
||||||
id_to_deploy
|
|
||||||
);
|
|
||||||
std::process::exit(6);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if text.is_empty() {
|
|
||||||
println!("Unknown response.");
|
|
||||||
} else {
|
|
||||||
println!("{}", text);
|
|
||||||
}
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -938,6 +908,82 @@ fn is_root() -> bool {
|
|||||||
crate::platform::is_root()
|
crate::platform::is_root()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos", test))]
|
||||||
|
fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool {
|
||||||
|
matches!(
|
||||||
|
args.first().map(String::as_str),
|
||||||
|
Some("--password")
|
||||||
|
| Some("--set-unlock-pin")
|
||||||
|
| Some("--get-id")
|
||||||
|
| Some("--set-id")
|
||||||
|
| Some("--config")
|
||||||
|
| Some("--option")
|
||||||
|
| Some("--assign")
|
||||||
|
| Some("--deploy")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_cli_setting_change_disabled() -> bool {
|
||||||
|
let option = config::keys::OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED;
|
||||||
|
let allow_command_line_settings =
|
||||||
|
config::option2bool(option, &crate::get_builtin_option(option));
|
||||||
|
config::is_disable_settings() && !allow_command_line_settings
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn parse_silent_install_args(args: &[String]) -> (Option<bool>, bool) {
|
||||||
|
let mut printer_override = None;
|
||||||
|
let mut debug = false;
|
||||||
|
|
||||||
|
for arg in args.iter().skip(1) {
|
||||||
|
match arg.as_str() {
|
||||||
|
"printer=1" => printer_override = Some(true),
|
||||||
|
"printer=0" => printer_override = Some(false),
|
||||||
|
"debug" => debug = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(printer_override, debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn args(values: &[&str]) -> Vec<String> {
|
||||||
|
values.iter().map(|value| value.to_string()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_main_ipc_scope_cli_command_matches_management_commands_only() {
|
||||||
|
for command in [
|
||||||
|
"--password",
|
||||||
|
"--set-unlock-pin",
|
||||||
|
"--get-id",
|
||||||
|
"--set-id",
|
||||||
|
"--config",
|
||||||
|
"--option",
|
||||||
|
"--assign",
|
||||||
|
"--deploy",
|
||||||
|
] {
|
||||||
|
assert!(is_user_main_ipc_scope_cli_command(&args(&[command])));
|
||||||
|
}
|
||||||
|
|
||||||
|
for command in [
|
||||||
|
"--service",
|
||||||
|
"--server",
|
||||||
|
"--tray",
|
||||||
|
"--cm",
|
||||||
|
"--check-hwcodec-config",
|
||||||
|
"--connect",
|
||||||
|
] {
|
||||||
|
assert!(!is_user_main_ipc_scope_cli_command(&args(&[command])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if the executable is a Quick Support version.
|
/// Check if the executable is a Quick Support version.
|
||||||
/// Note: This function must be kept in sync with `libs/portable/src/main.rs`.
|
/// Note: This function must be kept in sync with `libs/portable/src/main.rs`.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
@@ -2297,6 +2297,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 {
|
||||||
|
|||||||
@@ -326,12 +326,14 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
|
|||||||
try_sync_peer_option(&session, &session_id, &value, None);
|
try_sync_peer_option(&session, &session_id, &value, None);
|
||||||
}
|
}
|
||||||
#[cfg(not(target_os = "ios"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" {
|
if sessions::get_session_by_session_id(&session_id).is_some()
|
||||||
|
&& (value == "disable-clipboard" || value == "view-only")
|
||||||
|
{
|
||||||
crate::flutter::update_text_clipboard_required();
|
crate::flutter::update_text_clipboard_required();
|
||||||
}
|
}
|
||||||
#[cfg(feature = "unix-file-copy-paste")]
|
#[cfg(feature = "unix-file-copy-paste")]
|
||||||
if sessions::get_session_by_session_id(&session_id).is_some()
|
if sessions::get_session_by_session_id(&session_id).is_some()
|
||||||
&& value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE
|
&& (value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE || value == "view-only")
|
||||||
{
|
{
|
||||||
crate::flutter::update_file_clipboard_required();
|
crate::flutter::update_file_clipboard_required();
|
||||||
}
|
}
|
||||||
@@ -1153,6 +1155,22 @@ pub fn main_get_api_server() -> String {
|
|||||||
get_api_server()
|
get_api_server()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn main_deploy_device(token: String, id: String) -> String {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
let new_id = match id.trim() {
|
||||||
|
"" => None,
|
||||||
|
id => Some(id.to_owned()),
|
||||||
|
};
|
||||||
|
ui_interface::deploy_device(token, new_id).message()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
let _ = (token, id);
|
||||||
|
"Deployment is not supported on this platform.".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
|
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
|
||||||
SyncReturn(resolve_avatar_url(avatar))
|
SyncReturn(resolve_avatar_url(avatar))
|
||||||
}
|
}
|
||||||
@@ -2116,6 +2134,7 @@ pub fn main_start_service() {
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
{
|
{
|
||||||
config::Config::set_option("stop-service".into(), "".into());
|
config::Config::set_option("stop-service".into(), "".into());
|
||||||
|
crate::rendezvous_mediator::reset_needs_deploy_notification();
|
||||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2471,23 +2490,13 @@ pub fn is_disable_installation() -> SyncReturn<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_preset_password() -> bool {
|
pub fn is_preset_password() -> bool {
|
||||||
let hard = config::HARD_SETTINGS
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.get("password")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
if hard.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// On desktop, service owns the authoritative config; query it via IPC and return only a boolean.
|
// On desktop, service owns the authoritative config; query it via IPC and return only a boolean.
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
return crate::ipc::is_permanent_password_preset();
|
return crate::ipc::is_permanent_password_preset();
|
||||||
|
|
||||||
// On mobile, we have no service IPC; verify against local storage.
|
// On mobile, we have no service IPC; verify against local storage.
|
||||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||||
return config::Config::matches_permanent_password_plain(&hard);
|
return config::Config::is_using_preset_password();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't call this function for desktop version.
|
// Don't call this function for desktop version.
|
||||||
@@ -2843,8 +2852,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")
|
||||||
@@ -3065,6 +3082,7 @@ pub mod server_side {
|
|||||||
pub unsafe extern "system" fn Java_ffi_FFI_startService(_env: JNIEnv, _class: JClass) {
|
pub unsafe extern "system" fn Java_ffi_FFI_startService(_env: JNIEnv, _class: JClass) {
|
||||||
log::debug!("startService from jvm");
|
log::debug!("startService from jvm");
|
||||||
config::Config::set_option("stop-service".into(), "".into());
|
config::Config::set_option("stop-service".into(), "".into());
|
||||||
|
crate::rendezvous_mediator::reset_needs_deploy_notification();
|
||||||
crate::rendezvous_mediator::RendezvousMediator::restart();
|
crate::rendezvous_mediator::RendezvousMediator::restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
227
src/ipc.rs
227
src/ipc.rs
@@ -33,25 +33,25 @@ use hbb_common::{
|
|||||||
tokio_util::codec::Framed,
|
tokio_util::codec::Framed,
|
||||||
ResultType,
|
ResultType,
|
||||||
};
|
};
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
||||||
use ipc_auth::authorize_service_scoped_ipc_connection;
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
|
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
|
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
|
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
pub(crate) use ipc_auth::{
|
use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection};
|
||||||
active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
|
|
||||||
log_rejected_uinput_connection, peer_uid_from_fd,
|
|
||||||
};
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use ipc_auth::{
|
use ipc_auth::{
|
||||||
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
|
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
|
||||||
should_allow_everyone_create_on_windows,
|
should_allow_everyone_create_on_windows,
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
pub(crate) use ipc_auth::{
|
||||||
|
ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
|
||||||
|
log_rejected_uinput_connection, peer_uid_from_fd,
|
||||||
|
};
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
use ipc_fs::terminal_count_candidate_uids;
|
use ipc_fs::terminal_count_candidate_uids;
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
use ipc_fs::{
|
use ipc_fs::{
|
||||||
@@ -63,6 +63,8 @@ use parity_tokio_ipc::{
|
|||||||
};
|
};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
use std::cell::Cell;
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@@ -71,12 +73,47 @@ use std::{
|
|||||||
|
|
||||||
// IPC actions here.
|
// IPC actions here.
|
||||||
pub const IPC_ACTION_CLOSE: &str = "close";
|
pub const IPC_ACTION_CLOSE: &str = "close";
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
|
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) const IPC_TOKEN_LEN: usize = 64;
|
pub(crate) const IPC_TOKEN_LEN: usize = 64;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
|
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
|
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
|
||||||
pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
|
pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
thread_local! {
|
||||||
|
static USE_USER_MAIN_IPC: Cell<bool> = Cell::new(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use = "bind this guard to a local variable to keep the IPC scope active"]
|
||||||
|
/// Thread-local guard for routing root main IPC to the active user on Linux/macOS.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
pub(crate) struct UserMainIpcScope {
|
||||||
|
previous: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
impl UserMainIpcScope {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let previous = USE_USER_MAIN_IPC.with(|use_user_main| {
|
||||||
|
let previous = use_user_main.get();
|
||||||
|
use_user_main.set(true);
|
||||||
|
previous
|
||||||
|
});
|
||||||
|
Self { previous }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
impl Drop for UserMainIpcScope {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.set(self.previous));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
|
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
|
||||||
connect(ms_timeout, crate::POSTFIX_SERVICE).await
|
connect(ms_timeout, crate::POSTFIX_SERVICE).await
|
||||||
@@ -802,15 +839,7 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
"N".to_owned()
|
"N".to_owned()
|
||||||
});
|
});
|
||||||
} else if name == "permanent-password-is-preset" {
|
} else if name == "permanent-password-is-preset" {
|
||||||
let hard = config::HARD_SETTINGS
|
value = Some(if Config::is_using_preset_password() {
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.get("password")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
let is_preset =
|
|
||||||
!hard.is_empty() && Config::matches_permanent_password_plain(&hard);
|
|
||||||
value = Some(if is_preset {
|
|
||||||
"Y".to_owned()
|
"Y".to_owned()
|
||||||
} else {
|
} else {
|
||||||
"N".to_owned()
|
"N".to_owned()
|
||||||
@@ -861,7 +890,7 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
log::warn!("Changing permanent password is disabled");
|
log::warn!("Changing permanent password is disabled");
|
||||||
updated = false;
|
updated = false;
|
||||||
} else {
|
} else {
|
||||||
Config::set_permanent_password(&value);
|
updated = Config::set_permanent_password(&value);
|
||||||
}
|
}
|
||||||
// Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to
|
// Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to
|
||||||
// distinguish "accepted by daemon" vs "IPC send succeeded" without
|
// distinguish "accepted by daemon" vs "IPC send succeeded" without
|
||||||
@@ -1112,11 +1141,7 @@ async fn handle(data: Data, stream: &mut Connection) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
|
#[cfg(target_os = "windows")]
|
||||||
let path = Config::ipc_path(postfix);
|
|
||||||
connect_with_path(ms_timeout, &path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
|
pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
|
||||||
use hbb_common::rand::{rngs::OsRng, RngCore as _};
|
use hbb_common::rand::{rngs::OsRng, RngCore as _};
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
@@ -1137,6 +1162,7 @@ pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
|
|||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
|
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
|
||||||
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
|
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
|
||||||
return false;
|
return false;
|
||||||
@@ -1149,6 +1175,7 @@ pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> boo
|
|||||||
== 0
|
== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
|
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
|
||||||
stream: &mut ConnectionTmpl<T>,
|
stream: &mut ConnectionTmpl<T>,
|
||||||
token: &str,
|
token: &str,
|
||||||
@@ -1173,6 +1200,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
|
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
|
||||||
stream: &mut ConnectionTmpl<T>,
|
stream: &mut ConnectionTmpl<T>,
|
||||||
mut validate_token: F,
|
mut validate_token: F,
|
||||||
@@ -1209,6 +1237,103 @@ async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType<Connection
|
|||||||
Ok(ConnectionTmpl::new(client))
|
Ok(ConnectionTmpl::new(client))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
#[inline]
|
||||||
|
fn select_server_uid_for_user_main_ipc(
|
||||||
|
server_uids: &[u32],
|
||||||
|
active_uid: Option<u32>,
|
||||||
|
prefer_root: bool,
|
||||||
|
) -> ResultType<u32> {
|
||||||
|
let mut server_uids = server_uids.to_vec();
|
||||||
|
server_uids.sort_unstable();
|
||||||
|
server_uids.dedup();
|
||||||
|
|
||||||
|
match server_uids.as_slice() {
|
||||||
|
[] => {
|
||||||
|
if let Some(uid) = active_uid {
|
||||||
|
// If no `--server` processes are found but the active user is identifiable,
|
||||||
|
// try the active user anyway because the main process may also listen on "" IPC.
|
||||||
|
return Ok(uid);
|
||||||
|
} else {
|
||||||
|
bail!("No --server process found for user main IPC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[uid] => return Ok(*uid),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefer_root && server_uids.contains(&0) {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
if let Some(active_uid) = active_uid.filter(|uid| server_uids.contains(uid)) {
|
||||||
|
return Ok(active_uid);
|
||||||
|
}
|
||||||
|
bail!("Multiple --server processes found for user main IPC");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
fn running_server_uids_for_current_exe() -> ResultType<Vec<u32>> {
|
||||||
|
let current_exe = std::env::current_exe()?;
|
||||||
|
let current_exe_path = std::fs::canonicalize(¤t_exe)?;
|
||||||
|
let current_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id());
|
||||||
|
let mut sys = hbb_common::sysinfo::System::new();
|
||||||
|
sys.refresh_processes();
|
||||||
|
let mut server_uids = Vec::new();
|
||||||
|
for process in sys.processes().values() {
|
||||||
|
if process.pid() == current_pid {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if process.cmd().get(1).map_or(true, |arg| arg != "--server") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(process_path) = std::fs::canonicalize(process.exe()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if process_path != current_exe_path {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(uid) = process.user_id().map(|uid| **uid as u32) else {
|
||||||
|
// Root CLI management commands need a stable matching `--server` target.
|
||||||
|
// If this key process races during enumeration, failing the command is clearer
|
||||||
|
// than silently skipping it; `--server` is not expected to exit frequently.
|
||||||
|
bail!("Failed to read --server process uid");
|
||||||
|
};
|
||||||
|
server_uids.push(uid);
|
||||||
|
}
|
||||||
|
Ok(server_uids)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
fn user_main_ipc_server_uid() -> ResultType<u32> {
|
||||||
|
let server_uids = running_server_uids_for_current_exe()?;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let prefer_root = crate::platform::linux::is_login_screen_wayland();
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let prefer_root = false;
|
||||||
|
select_server_uid_for_user_main_ipc(&server_uids, active_uid(), prefer_root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
{
|
||||||
|
let use_user_main_ipc = USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.get());
|
||||||
|
let is_root_main_ipc =
|
||||||
|
unsafe { hbb_common::libc::geteuid() == 0 } && postfix.is_empty() && use_user_main_ipc;
|
||||||
|
if is_root_main_ipc {
|
||||||
|
let uid = user_main_ipc_server_uid()?;
|
||||||
|
let path = Config::ipc_path_for_uid(uid, postfix);
|
||||||
|
return connect_with_path(ms_timeout, &path).await;
|
||||||
|
}
|
||||||
|
let path = Config::ipc_path(postfix);
|
||||||
|
return connect_with_path(ms_timeout, &path).await;
|
||||||
|
}
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let path = Config::ipc_path(postfix);
|
||||||
|
connect_with_path(ms_timeout, &path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub async fn connect_for_uid(
|
pub async fn connect_for_uid(
|
||||||
ms_timeout: u64,
|
ms_timeout: u64,
|
||||||
@@ -1417,11 +1542,6 @@ fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> R
|
|||||||
bail!("Invalid permanent-password-storage-and-salt payload");
|
bail!("Invalid permanent-password-storage-and-salt payload");
|
||||||
};
|
};
|
||||||
|
|
||||||
if storage.is_empty() {
|
|
||||||
Config::set_permanent_password_storage_for_sync("", "")?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
Config::set_permanent_password_storage_for_sync(storage, salt)?;
|
Config::set_permanent_password_storage_for_sync(storage, salt)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2002,7 +2122,16 @@ mod test {
|
|||||||
assert!(std::mem::size_of::<Data>() <= 120);
|
assert!(std::mem::size_of::<Data>() <= 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn test_service_ipc_path_is_shared_across_uids() {
|
||||||
|
assert_eq!(
|
||||||
|
Config::ipc_path_for_uid(0, crate::POSTFIX_SERVICE),
|
||||||
|
Config::ipc_path_for_uid(501, crate::POSTFIX_SERVICE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ipc_path_differs_by_uid_for_cm() {
|
fn test_ipc_path_differs_by_uid_for_cm() {
|
||||||
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
|
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
|
||||||
@@ -2021,4 +2150,46 @@ mod test {
|
|||||||
Config::ipc_path_for_uid(other_uid, postfix)
|
Config::ipc_path_for_uid(other_uid, postfix)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn test_select_server_uid_uses_active_uid_when_no_server_found() {
|
||||||
|
assert_eq!(
|
||||||
|
select_server_uid_for_user_main_ipc(&[], Some(501), false).unwrap(),
|
||||||
|
501
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn test_select_server_uid_uses_single_server_uid() {
|
||||||
|
assert_eq!(
|
||||||
|
select_server_uid_for_user_main_ipc(&[501], None, false).unwrap(),
|
||||||
|
501
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn test_select_server_uid_prefers_active_uid_with_multiple_servers() {
|
||||||
|
assert_eq!(
|
||||||
|
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), false).unwrap(),
|
||||||
|
501
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn test_select_server_uid_prefers_root_on_wayland_login_screen() {
|
||||||
|
assert_eq!(
|
||||||
|
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), true).unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn test_select_server_uid_fails_when_multiple_servers_are_ambiguous() {
|
||||||
|
assert!(select_server_uid_for_user_main_ipc(&[501, 502], None, false).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -607,27 +607,30 @@ pub(crate) fn log_rejected_windows_ipc_connection(
|
|||||||
peer_session_id: Option<u32>,
|
peer_session_id: Option<u32>,
|
||||||
expected_session_id: Option<u32>,
|
expected_session_id: Option<u32>,
|
||||||
peer_is_system: Option<bool>,
|
peer_is_system: Option<bool>,
|
||||||
|
peer_is_elevated: Option<bool>,
|
||||||
) {
|
) {
|
||||||
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
|
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
|
||||||
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
|
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
|
||||||
if suppressed > 0 {
|
if suppressed > 0 {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?} (suppressed {} similar events)",
|
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?} (suppressed {} similar events)",
|
||||||
postfix,
|
postfix,
|
||||||
peer_pid,
|
peer_pid,
|
||||||
peer_session_id,
|
peer_session_id,
|
||||||
expected_session_id,
|
expected_session_id,
|
||||||
peer_is_system,
|
peer_is_system,
|
||||||
|
peer_is_elevated,
|
||||||
suppressed
|
suppressed
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}",
|
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?}",
|
||||||
postfix,
|
postfix,
|
||||||
peer_pid,
|
peer_pid,
|
||||||
peer_session_id,
|
peer_session_id,
|
||||||
expected_session_id,
|
expected_session_id,
|
||||||
peer_is_system
|
peer_is_system,
|
||||||
|
peer_is_elevated
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -655,8 +658,14 @@ pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postf
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool {
|
pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool {
|
||||||
let (authorized, peer_pid, peer_session_id, server_session_id, peer_is_system) =
|
let (
|
||||||
stream.server_authorization_status();
|
authorized,
|
||||||
|
peer_pid,
|
||||||
|
peer_session_id,
|
||||||
|
server_session_id,
|
||||||
|
peer_is_system,
|
||||||
|
peer_is_elevated,
|
||||||
|
) = stream.server_authorization_status();
|
||||||
if !authorized {
|
if !authorized {
|
||||||
log_rejected_windows_ipc_connection(
|
log_rejected_windows_ipc_connection(
|
||||||
postfix,
|
postfix,
|
||||||
@@ -664,6 +673,7 @@ pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix
|
|||||||
peer_session_id,
|
peer_session_id,
|
||||||
server_session_id,
|
server_session_id,
|
||||||
peer_is_system,
|
peer_is_system,
|
||||||
|
peer_is_elevated,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -776,7 +786,14 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
|
|||||||
|
|
||||||
fn server_authorization_status(
|
fn server_authorization_status(
|
||||||
&self,
|
&self,
|
||||||
) -> (bool, Option<u32>, Option<u32>, Option<u32>, Option<bool>) {
|
) -> (
|
||||||
|
bool,
|
||||||
|
Option<u32>,
|
||||||
|
Option<u32>,
|
||||||
|
Option<u32>,
|
||||||
|
Option<bool>,
|
||||||
|
Option<bool>,
|
||||||
|
) {
|
||||||
let peer_pid = self.peer_pid();
|
let peer_pid = self.peer_pid();
|
||||||
let server_session_id = crate::platform::windows::get_current_process_session_id();
|
let server_session_id = crate::platform::windows::get_current_process_session_id();
|
||||||
let peer_session_id =
|
let peer_session_id =
|
||||||
@@ -786,20 +803,34 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
|
|||||||
let peer_is_system = peer_is_system_result
|
let peer_is_system = peer_is_system_result
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|r| r.as_ref().ok().copied());
|
.and_then(|r| r.as_ref().ok().copied());
|
||||||
if server_session_id.is_none() && !peer_is_system.unwrap_or(false) {
|
let session_authorized = is_allowed_windows_session_scoped_peer(
|
||||||
// When the server session id cannot be determined, the session-id allow-path is
|
|
||||||
// disabled and only SYSTEM peers can be authorized.
|
|
||||||
log::debug!(
|
|
||||||
"IPC authorization: server session id unavailable; rejecting non-SYSTEM peer, peer_pid={:?}, peer_session_id={:?}",
|
|
||||||
peer_pid,
|
|
||||||
peer_session_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let authorized = is_allowed_windows_session_scoped_peer(
|
|
||||||
peer_is_system.unwrap_or(false),
|
peer_is_system.unwrap_or(false),
|
||||||
peer_session_id,
|
peer_session_id,
|
||||||
server_session_id,
|
server_session_id,
|
||||||
);
|
);
|
||||||
|
let peer_is_elevated_result = if session_authorized {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
peer_pid.map(|pid| crate::platform::windows::is_elevated(Some(pid)))
|
||||||
|
};
|
||||||
|
let peer_is_elevated = peer_is_elevated_result
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|r| r.as_ref().ok().copied());
|
||||||
|
if server_session_id.is_none()
|
||||||
|
&& !peer_is_system.unwrap_or(false)
|
||||||
|
&& !peer_is_elevated.unwrap_or(false)
|
||||||
|
{
|
||||||
|
// When the server session id cannot be determined, the session-id allow-path is
|
||||||
|
// disabled and only privileged peers can be authorized.
|
||||||
|
log::debug!(
|
||||||
|
"IPC authorization: server session id unavailable; rejecting non-privileged peer, peer_pid={:?}, peer_session_id={:?}",
|
||||||
|
peer_pid,
|
||||||
|
peer_session_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Main IPC trusts same-session peers, LocalSystem, and elevated administrators.
|
||||||
|
// Service-scoped IPC channels keep their own stricter authorization paths.
|
||||||
|
let authorized = session_authorized || peer_is_elevated.unwrap_or(false);
|
||||||
if !authorized {
|
if !authorized {
|
||||||
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) {
|
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) {
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -808,6 +839,13 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
|
|||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_elevated_result.as_ref()) {
|
||||||
|
log::debug!(
|
||||||
|
"Failed to determine whether peer process is elevated, pid={}, err={}",
|
||||||
|
pid,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
authorized,
|
authorized,
|
||||||
@@ -815,6 +853,7 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
|
|||||||
peer_session_id,
|
peer_session_id,
|
||||||
server_session_id,
|
server_session_id,
|
||||||
peer_is_system,
|
peer_is_system,
|
||||||
|
peer_is_elevated,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user