mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-08 16:24:57 +03:00
Compare commits
129 Commits
terminal-u
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb9f63e1d | ||
|
|
005a8b4a04 | ||
|
|
e2149974cc | ||
|
|
6c578292e8 | ||
|
|
28930c0463 | ||
|
|
37141afece | ||
|
|
493b14ba78 | ||
|
|
cf1de4de62 | ||
|
|
9fdb8410d3 | ||
|
|
a2b79462ab | ||
|
|
dce221be5a | ||
|
|
9d1ab3fba3 | ||
|
|
b3bd18845d | ||
|
|
435f6ec61d | ||
|
|
0497814004 | ||
|
|
4b1ef9e20d | ||
|
|
10d5250d23 | ||
|
|
2ee580d49d | ||
|
|
4a54029cac | ||
|
|
001848bf2f | ||
|
|
989bf80fe8 | ||
|
|
78b5f47668 | ||
|
|
97e9e44faa | ||
|
|
ff226f6d80 | ||
|
|
0cbdb6ffb3 | ||
|
|
b8117c5c34 | ||
|
|
a69614d464 | ||
|
|
58ee593e26 | ||
|
|
09bc9056c9 | ||
|
|
0c6df924d1 | ||
|
|
456817b4f4 | ||
|
|
16570ee34f | ||
|
|
2b40c61d8e | ||
|
|
dcc64cdeae | ||
|
|
2747d3d8b4 | ||
|
|
3c574a4182 | ||
|
|
311d4708e5 | ||
|
|
5cf4323d07 | ||
|
|
3976701ac6 | ||
|
|
9ded8d6ab2 | ||
|
|
cd2fff0655 | ||
|
|
10f61ffdc2 | ||
|
|
d72952bf93 | ||
|
|
a7c55db9ac | ||
|
|
bff47e2b81 | ||
|
|
3d478c4935 | ||
|
|
a658e987b7 | ||
|
|
7c8b0adc1e | ||
|
|
a732ebc3e1 | ||
|
|
30c0867e40 | ||
|
|
8f50ea64dc | ||
|
|
0797ebb695 | ||
|
|
c9391fb894 | ||
|
|
8a955888bf | ||
|
|
36e812e550 | ||
|
|
8baa995c7a | ||
|
|
f4a0535289 | ||
|
|
6665242edf | ||
|
|
3cdf1cce54 | ||
|
|
88ae00ba73 | ||
|
|
7c26575dbd | ||
|
|
93d064a9b0 | ||
|
|
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 | ||
|
|
472c4fc03a | ||
|
|
9f8f726f12 | ||
|
|
701a9c6cdc | ||
|
|
0d40cf2101 | ||
|
|
dd265dadd7 | ||
|
|
fe5a8cb2ad | ||
|
|
b6caa1a7b2 | ||
|
|
55c9707639 | ||
|
|
d8808baa83 | ||
|
|
1978020d27 | ||
|
|
0e4b91b8d7 | ||
|
|
9c831dc59b | ||
|
|
b757e97c11 | ||
|
|
9df486a689 | ||
|
|
72d27c3c47 | ||
|
|
6c20fc936d |
@@ -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.9"
|
||||||
NDK_VERSION: "r28c"
|
NDK_VERSION: "r28c"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -53,14 +58,24 @@ jobs:
|
|||||||
|
|
||||||
build-RustDeskTempTopMostWindow:
|
build-RustDeskTempTopMostWindow:
|
||||||
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
|
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
|
||||||
with:
|
|
||||||
upload-artifact: ${{ inputs.upload-artifact }}
|
|
||||||
target: windows-2022
|
|
||||||
configuration: Release
|
|
||||||
platform: x64
|
|
||||||
target_version: Windows10
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
target: windows-2022,
|
||||||
|
platform: x64,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: windows-11-arm,
|
||||||
|
platform: ARM64,
|
||||||
|
}
|
||||||
|
with:
|
||||||
|
upload-artifact: ${{ inputs.upload-artifact }}
|
||||||
|
target: ${{ matrix.job.target }}
|
||||||
|
configuration: Release
|
||||||
|
platform: ${{ matrix.job.platform }}
|
||||||
|
target_version: Windows10
|
||||||
|
|
||||||
build-for-windows-flutter:
|
build-for-windows-flutter:
|
||||||
name: ${{ matrix.job.target }}
|
name: ${{ matrix.job.target }}
|
||||||
@@ -76,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.9"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
@@ -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
|
||||||
|
|||||||
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`.
|
||||||
|
|||||||
68
Cargo.lock
generated
68
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",
|
||||||
@@ -5996,8 +5996,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parity-tokio-ipc"
|
name = "parity-tokio-ipc"
|
||||||
version = "0.7.3-5"
|
version = "0.7.3-6"
|
||||||
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291"
|
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -6588,7 +6588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8"
|
checksum = "556af5f5c953a2ee13f45753e581a38f9778e6551bc3ccc56d90b14628fe59d8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.10",
|
"cfg-if 0.1.10",
|
||||||
"rpassword 2.1.0",
|
"rpassword",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"termios 0.3.3",
|
"termios 0.3.3",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
@@ -6673,7 +6673,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6920,7 +6920,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rdev"
|
name = "rdev"
|
||||||
version = "0.5.0-2"
|
version = "0.5.0-2"
|
||||||
source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8"
|
source = "git+https://github.com/rustdesk-org/rdev#871bf1c856d6a30af2f56ab8848396a025140855"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa 0.24.1",
|
"cocoa 0.24.1",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
@@ -7152,17 +7152,6 @@ dependencies = [
|
|||||||
"winapi 0.2.8",
|
"winapi 0.2.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rpassword"
|
|
||||||
version = "7.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rtoolbox",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtcp"
|
name = "rtcp"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -7174,16 +7163,6 @@ dependencies = [
|
|||||||
"webrtc-util",
|
"webrtc-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtoolbox"
|
|
||||||
version = "0.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtp"
|
name = "rtp"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -7270,7 +7249,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.6"
|
version = "1.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7283,7 +7262,6 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cidr-utils",
|
"cidr-utils",
|
||||||
"clap 4.5.53",
|
|
||||||
"clipboard",
|
"clipboard",
|
||||||
"clipboard-master",
|
"clipboard-master",
|
||||||
"cocoa 0.24.1",
|
"cocoa 0.24.1",
|
||||||
@@ -7341,7 +7319,6 @@ dependencies = [
|
|||||||
"repng",
|
"repng",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ringbuf",
|
"ringbuf",
|
||||||
"rpassword 7.3.1",
|
|
||||||
"rubato",
|
"rubato",
|
||||||
"runas",
|
"runas",
|
||||||
"rust-pulsectl",
|
"rust-pulsectl",
|
||||||
@@ -7385,7 +7362,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.6"
|
version = "1.4.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -7457,7 +7434,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.11.0",
|
"linux-raw-sys 0.11.0",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7514,7 +7491,7 @@ dependencies = [
|
|||||||
"security-framework 3.5.1",
|
"security-framework 3.5.1",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9733,9 +9710,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-protocols-wlr"
|
name = "wayland-protocols-wlr"
|
||||||
version = "0.3.3"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
|
checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
@@ -10838,16 +10815,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wl-clipboard-rs"
|
name = "wl-clipboard-rs"
|
||||||
version = "0.9.0"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f"
|
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"os_pipe",
|
"os_pipe",
|
||||||
"rustix 0.38.34",
|
"rustix 1.1.2",
|
||||||
"tempfile",
|
"thiserror 2.0.17",
|
||||||
"thiserror 1.0.61",
|
|
||||||
"tree_magic_mini",
|
"tree_magic_mini",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.6"
|
version = "1.4.9"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -22,7 +22,6 @@ path = "src/service.rs"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
inline = []
|
inline = []
|
||||||
cli = []
|
|
||||||
use_samplerate = ["samplerate"]
|
use_samplerate = ["samplerate"]
|
||||||
use_rubato = ["rubato"]
|
use_rubato = ["rubato"]
|
||||||
use_dasp = ["dasp"]
|
use_dasp = ["dasp"]
|
||||||
@@ -62,8 +61,6 @@ dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpol
|
|||||||
rubato = { version = "0.12", optional = true }
|
rubato = { version = "0.12", optional = true }
|
||||||
samplerate = { version = "0.2", optional = true }
|
samplerate = { version = "0.2", optional = true }
|
||||||
uuid = { version = "1.3", features = ["v4"] }
|
uuid = { version = "1.3", features = ["v4"] }
|
||||||
clap = "4.2"
|
|
||||||
rpassword = "7.2"
|
|
||||||
num_cpus = "1.15"
|
num_cpus = "1.15"
|
||||||
bytes = { version = "1.4", features = ["serde"] }
|
bytes = { version = "1.4", features = ["serde"] }
|
||||||
default-net = "0.14"
|
default-net = "0.14"
|
||||||
@@ -213,7 +210,7 @@ exclude = ["vdi/host", "examples/custom_plugin"]
|
|||||||
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
libxdo-sys = { path = "libs/libxdo-sys-stub" }
|
||||||
|
|
||||||
[package.metadata.winres]
|
[package.metadata.winres]
|
||||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||||
ProductName = "RustDesk"
|
ProductName = "RustDesk"
|
||||||
FileDescription = "RustDesk Remote Desktop"
|
FileDescription = "RustDesk Remote Desktop"
|
||||||
OriginalFilename = "rustdesk.exe"
|
OriginalFilename = "rustdesk.exe"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.6
|
version: 1.4.9
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.6
|
version: 1.4.9
|
||||||
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
|
||||||
|
|||||||
@@ -716,6 +716,17 @@ closeConnection({String? id}) {
|
|||||||
stateGlobal.isInMainPage = true;
|
stateGlobal.isInMainPage = true;
|
||||||
} else {
|
} else {
|
||||||
final controller = Get.find<DesktopTabController>();
|
final controller = Get.find<DesktopTabController>();
|
||||||
|
if (controller.tabType == DesktopTabType.terminal &&
|
||||||
|
controller.onCloseWindow != null) {
|
||||||
|
// Terminal windows are scoped to one peer. The optional id passed to
|
||||||
|
// closeConnection() is that peer id, not a terminal tab key
|
||||||
|
// (${peerId}_${terminalId}). Closing from terminal dialogs should close
|
||||||
|
// the peer's whole terminal window, including all terminal tabs.
|
||||||
|
unawaited(controller.onCloseWindow!().catchError((e, _) {
|
||||||
|
debugPrint('[closeConnection] Failed to close terminal window: $e');
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
controller.closeBy(id);
|
controller.closeBy(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1174,6 +1185,48 @@ void msgBox(SessionID sessionId, String type, String title, String text,
|
|||||||
VoidCallback? onSubmit,
|
VoidCallback? onSubmit,
|
||||||
int? submitTimeout}) {
|
int? submitTimeout}) {
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
|
if (type.contains('insecure-connection')) {
|
||||||
|
Future<void> closeSession() async {
|
||||||
|
await bind.sessionSetCommon(
|
||||||
|
sessionId: sessionId,
|
||||||
|
key: 'continue-insecure-connection',
|
||||||
|
value: 'N',
|
||||||
|
);
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
closeConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void continueSession() {
|
||||||
|
unawaited(
|
||||||
|
bind.sessionSetCommon(
|
||||||
|
sessionId: sessionId,
|
||||||
|
key: 'continue-insecure-connection',
|
||||||
|
value: 'Y',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
dialogManager.dismissAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogManager.show(
|
||||||
|
(setState, close, context) => CustomAlertDialog(
|
||||||
|
title: null,
|
||||||
|
content: SelectionArea(child: msgboxContent(type, title, text)),
|
||||||
|
actions: [
|
||||||
|
dialogButton(
|
||||||
|
'Continue',
|
||||||
|
onPressed: continueSession,
|
||||||
|
isOutline: true,
|
||||||
|
),
|
||||||
|
dialogButton('Disconnect', onPressed: closeSession),
|
||||||
|
],
|
||||||
|
onSubmit: closeSession,
|
||||||
|
onCancel: closeSession,
|
||||||
|
),
|
||||||
|
tag: '$sessionId-$type-$title-$text-$link',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<Widget> buttons = [];
|
List<Widget> buttons = [];
|
||||||
bool hasOk = false;
|
bool hasOk = false;
|
||||||
submit() {
|
submit() {
|
||||||
@@ -3339,7 +3392,12 @@ Future<List<Rect>> getScreenRectList() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
|
||||||
{bool updateCursorPos = true}) {
|
{bool updateCursorPos = true, bool recordSelection = true}) {
|
||||||
|
if (recordSelection) {
|
||||||
|
ffi.ffiModel.lastUserDisplay = i;
|
||||||
|
ffi.ffiModel.cancelPendingRestoreTimer();
|
||||||
|
ffi.ffiModel.pendingMonitorRestore = null;
|
||||||
|
}
|
||||||
final displays = i == kAllDisplayValue
|
final displays = i == kAllDisplayValue
|
||||||
? List.generate(pi.displays.length, (index) => index)
|
? List.generate(pi.displays.length, (index) => index)
|
||||||
: [i];
|
: [i];
|
||||||
@@ -3702,14 +3760,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();
|
||||||
@@ -3721,9 +3819,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,
|
||||||
@@ -4179,8 +4282,7 @@ Widget? buildAvatarWidget({
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) =>
|
errorBuilder: (_, __, ___) => fallback ?? SizedBox.shrink(),
|
||||||
fallback ?? SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +13,84 @@ 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>{};
|
||||||
|
|
||||||
|
Future<bool> openWaylandKeyboardIssueUrl() {
|
||||||
|
return launchUrl(
|
||||||
|
Uri.parse(kWaylandKeyboardIssueUrl),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
|
||||||
|
return _waylandKeyboardPromptSuppressedConnectionIds.contains(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setWaylandKeyboardPromptSuppressedForConnection(
|
||||||
|
String connectionId, bool suppressed) {
|
||||||
|
if (suppressed) {
|
||||||
|
_waylandKeyboardPromptSuppressedConnectionIds.add(connectionId);
|
||||||
|
} else {
|
||||||
|
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
|
||||||
|
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldShowWaylandKeyboardPrompt({
|
||||||
|
required String connectionId,
|
||||||
|
required bool isWaylandPeer,
|
||||||
|
required bool allowWaylandKeyboardRemembered,
|
||||||
|
}) {
|
||||||
|
return isWaylandPeer &&
|
||||||
|
!allowWaylandKeyboardRemembered &&
|
||||||
|
!isWaylandKeyboardPromptSuppressedForConnection(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget waylandKeyboardScopeChip(BuildContext context, String text) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
border: Border.all(color: colorScheme.primary.withOpacity(0.35)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@@ -87,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
|
||||||
@@ -142,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
|
||||||
@@ -314,7 +606,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
// to-do:
|
// to-do:
|
||||||
// 1. Web desktop
|
// 1. Web desktop
|
||||||
// 2. Mobile, copy the image to the clipboard
|
// 2. Mobile, copy the image to the clipboard
|
||||||
if (isDesktop) {
|
if ((isDefaultConn || ffi.connType == ConnType.viewCamera) && isDesktop) {
|
||||||
final isScreenshotSupported = bind.sessionGetCommonSync(
|
final isScreenshotSupported = bind.sessionGetCommonSync(
|
||||||
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
sessionId: sessionId, key: 'is_screenshot_supported', param: '');
|
||||||
if ('true' == isScreenshotSupported) {
|
if ('true' == isScreenshotSupported) {
|
||||||
@@ -684,8 +976,10 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Lock after session end'))));
|
child: Text(translate('Lock after session end'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
if (pi.isSupportMultiDisplay &&
|
if (pi.isSupportMultiDisplay &&
|
||||||
PrivacyModeState.find(id).isEmpty &&
|
(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 =
|
||||||
@@ -759,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
|
||||||
@@ -768,23 +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 (ffiModel.pi.currentDisplay != 0 &&
|
if (!checkDisplayAllowedForPrivacyMode(targetImplKey, value)) {
|
||||||
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';
|
||||||
@@ -802,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) {
|
||||||
@@ -816,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
|
||||||
@@ -837,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();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class TerminalPage extends StatefulWidget {
|
|||||||
final bool? isSharedPassword;
|
final bool? isSharedPassword;
|
||||||
final String? connToken;
|
final String? connToken;
|
||||||
final int terminalId;
|
final int terminalId;
|
||||||
|
|
||||||
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
/// Tab key for focus management, passed from parent to avoid duplicate construction
|
||||||
final String tabKey;
|
final String tabKey;
|
||||||
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
|
||||||
@@ -43,6 +44,9 @@ class TerminalPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _TerminalPageState extends State<TerminalPage>
|
class _TerminalPageState extends State<TerminalPage>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
static const EdgeInsets _defaultTerminalPadding =
|
||||||
|
EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
||||||
|
|
||||||
late FFI _ffi;
|
late FFI _ffi;
|
||||||
late TerminalModel _terminalModel;
|
late TerminalModel _terminalModel;
|
||||||
double? _cellHeight;
|
double? _cellHeight;
|
||||||
@@ -91,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// Register this terminal model with FFI for event routing
|
// Register this terminal model with FFI for event routing
|
||||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||||
|
|
||||||
|
// Auto-close tab when shell exits
|
||||||
|
_terminalModel.onClosed = () {
|
||||||
|
if (mounted) {
|
||||||
|
widget.tabController.closeBy(widget.tabKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize terminal connection
|
// Initialize terminal connection
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.tabController.onSelected?.call(widget.id);
|
widget.tabController.onSelected?.call(widget.id);
|
||||||
@@ -155,13 +166,27 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// extra space left after dividing the available height by the height of a single
|
// extra space left after dividing the available height by the height of a single
|
||||||
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
|
||||||
EdgeInsets _calculatePadding(double heightPx) {
|
EdgeInsets _calculatePadding(double heightPx) {
|
||||||
if (_cellHeight == null) {
|
final cellHeight = _cellHeight;
|
||||||
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
|
if (!heightPx.isFinite ||
|
||||||
|
heightPx <= 0 ||
|
||||||
|
cellHeight == null ||
|
||||||
|
!cellHeight.isFinite ||
|
||||||
|
cellHeight <= 0) {
|
||||||
|
return _defaultTerminalPadding;
|
||||||
|
}
|
||||||
|
final rows = (heightPx / cellHeight).floor();
|
||||||
|
if (rows <= 0) {
|
||||||
|
return _defaultTerminalPadding;
|
||||||
|
}
|
||||||
|
final extraSpace = heightPx - rows * cellHeight;
|
||||||
|
if (!extraSpace.isFinite || extraSpace < 0) {
|
||||||
|
return _defaultTerminalPadding;
|
||||||
}
|
}
|
||||||
final rows = (heightPx / _cellHeight!).floor();
|
|
||||||
final extraSpace = heightPx - rows * _cellHeight!;
|
|
||||||
final topBottom = extraSpace / 2.0;
|
final topBottom = extraSpace / 2.0;
|
||||||
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
|
return EdgeInsets.symmetric(
|
||||||
|
horizontal: _defaultTerminalPadding.horizontal / 2,
|
||||||
|
vertical: topBottom,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
.setTitle(getWindowNameWithId(id));
|
.setTitle(getWindowNameWithId(id));
|
||||||
};
|
};
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
|
tabController.onCloseWindow = _closeWindowFromConnection;
|
||||||
final terminalId = params['terminalId'] ?? _nextTerminalId++;
|
final terminalId = params['terminalId'] ?? _nextTerminalId++;
|
||||||
tabController.add(_createTerminalTab(
|
tabController.add(_createTerminalTab(
|
||||||
peerId: params['id'],
|
peerId: params['id'],
|
||||||
@@ -144,6 +145,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
_windowClosing = true;
|
_windowClosing = true;
|
||||||
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
|
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
|
||||||
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
|
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
|
||||||
|
// Keep the cleanup target lookup below synchronous before its first await:
|
||||||
|
// it relies on the current frame still retaining each TerminalPage's FFI/model.
|
||||||
tabController.clear();
|
tabController.clear();
|
||||||
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
|
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
|
||||||
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
|
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
|
||||||
@@ -368,8 +371,34 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
final persistentSessions =
|
final persistentSessions =
|
||||||
args['persistent_sessions'] as List<dynamic>? ?? [];
|
args['persistent_sessions'] as List<dynamic>? ?? [];
|
||||||
final sortedSessions = persistentSessions.whereType<int>().toList()..sort();
|
final sortedSessions = persistentSessions.whereType<int>().toList()..sort();
|
||||||
|
var peerId = args['peer_id'] as String? ?? '';
|
||||||
|
if (peerId.isEmpty) {
|
||||||
|
if (tabController.state.value.tabs.isEmpty ||
|
||||||
|
tabController.state.value.selected >=
|
||||||
|
tabController.state.value.tabs.length) {
|
||||||
|
debugPrint('[TerminalTabPage] Skip restore: no selected tab');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final currentTab = tabController.state.value.selectedTabInfo;
|
||||||
|
final parsed = _parseTabKey(currentTab.key);
|
||||||
|
if (parsed == null) return;
|
||||||
|
peerId = parsed.$1;
|
||||||
|
}
|
||||||
|
final existingTerminalIds = tabController.state.value.tabs
|
||||||
|
.map((tab) => _parseTabKey(tab.key))
|
||||||
|
.where((parsed) => parsed != null && parsed.$1 == peerId)
|
||||||
|
.map((parsed) => parsed!.$2)
|
||||||
|
.toSet();
|
||||||
|
if (existingTerminalIds.isEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalTabPage] Skip restore: no seed tab for peer $peerId');
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (final terminalId in sortedSessions) {
|
for (final terminalId in sortedSessions) {
|
||||||
_addNewTerminalForCurrentPeer(terminalId: terminalId);
|
if (!existingTerminalIds.add(terminalId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_addNewTerminal(peerId, terminalId: terminalId);
|
||||||
// A delay is required to ensure the UI has sufficient time to update
|
// A delay is required to ensure the UI has sufficient time to update
|
||||||
// before adding the next terminal. Without this delay, `_TerminalPageState::dispose()`
|
// before adding the next terminal. Without this delay, `_TerminalPageState::dispose()`
|
||||||
// may be called prematurely while the tab widget is still in the tab controller.
|
// may be called prematurely while the tab widget is still in the tab controller.
|
||||||
@@ -546,6 +575,11 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _closeWindowFromConnection() async {
|
||||||
|
await _closeAllTabs();
|
||||||
|
await WindowController.fromWindowId(windowId()).close();
|
||||||
|
}
|
||||||
|
|
||||||
int windowId() {
|
int windowId() {
|
||||||
return widget.params["windowId"];
|
return widget.params["windowId"];
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,7 @@ class DesktopTabController {
|
|||||||
/// index, key
|
/// index, key
|
||||||
Function(int, String)? onRemoved;
|
Function(int, String)? onRemoved;
|
||||||
Function(String)? onSelected;
|
Function(String)? onSelected;
|
||||||
|
Future<void> Function()? onCloseWindow;
|
||||||
|
|
||||||
DesktopTabController(
|
DesktopTabController(
|
||||||
{required this.tabType, this.onRemoved, this.onSelected});
|
{required this.tabType, this.onRemoved, this.onSelected});
|
||||||
@@ -592,13 +593,13 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBar() {
|
Widget _buildBar() {
|
||||||
|
final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage();
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// custom double tap handler
|
// custom double tap handler
|
||||||
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
|
onTap: !isIncomingHomePage && showMaximize
|
||||||
showMaximize
|
|
||||||
? () {
|
? () {
|
||||||
final current = DateTime.now().millisecondsSinceEpoch;
|
final current = DateTime.now().millisecondsSinceEpoch;
|
||||||
final elapsed = current - _lastClickTime;
|
final elapsed = current - _lastClickTime;
|
||||||
@@ -609,7 +610,7 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
.then((value) => stateGlobal.setMaximized(value));
|
.then((value) => stateGlobal.setMaximized(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch.
|
||||||
onPanStart: (_) => startDragging(isMainWindow),
|
onPanStart: (_) => startDragging(isMainWindow),
|
||||||
onPanCancel: () {
|
onPanCancel: () {
|
||||||
// We want to disable dragging of the tab area in the tab bar.
|
// We want to disable dragging of the tab area in the tab bar.
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
// Register this terminal model with FFI for event routing
|
// Register this terminal model with FFI for event routing
|
||||||
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
|
||||||
|
|
||||||
|
// Auto-close connection when shell exits
|
||||||
|
_terminalModel.onClosed = () {
|
||||||
|
if (mounted) {
|
||||||
|
closeConnection(id: widget.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Web desktop users have full hardware keyboard access, so the on-screen
|
// Web desktop users have full hardware keyboard access, so the on-screen
|
||||||
// terminal extra keys bar is unnecessary and disabled.
|
// terminal extra keys bar is unnecessary and disabled.
|
||||||
_showTerminalExtraKeys = !isWebDesktop &&
|
_showTerminalExtraKeys = !isWebDesktop &&
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -391,14 +401,30 @@ class FileController {
|
|||||||
|
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
|
||||||
final dir = (await bind.sessionGetPeerOption(
|
final savedDir = (await bind.sessionGetPeerOption(
|
||||||
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
|
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
|
||||||
openDirectory(dir.isEmpty ? options.value.home : dir);
|
Future<bool> tryOpenReadyDirs() async {
|
||||||
|
final dirs = <String>{
|
||||||
|
if (directory.value.path.isNotEmpty) directory.value.path,
|
||||||
|
if (savedDir.isNotEmpty) savedDir,
|
||||||
|
options.value.home,
|
||||||
|
};
|
||||||
|
for (final dir in dirs) {
|
||||||
|
if (await _openDirectoryPath(dir, isBack: true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var opened = await tryOpenReadyDirs();
|
||||||
|
|
||||||
await Future.delayed(Duration(seconds: 1));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
|
||||||
if (directory.value.path.isEmpty) {
|
if (!opened) {
|
||||||
openDirectory(options.value.home);
|
// The peer may become ready during the reconnect delay, so retry the
|
||||||
|
// same candidates instead of only retrying the default home directory.
|
||||||
|
await tryOpenReadyDirs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,19 +455,23 @@ class FileController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<bool> refresh() async {
|
||||||
await openDirectory(directory.value.path);
|
// "." can be both a refresh command and a real remote directory path.
|
||||||
|
// Refresh must bypass openDirectory's command dispatch to avoid recursion.
|
||||||
|
return await _openDirectoryPath(directory.value.path, isBack: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openDirectory(String path, {bool isBack = false}) async {
|
Future<bool> openDirectory(String path, {bool isBack = false}) async {
|
||||||
if (path == ".") {
|
if (!isBack && path == ".") {
|
||||||
refresh();
|
return await refresh();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (path == "..") {
|
if (!isBack && path == "..") {
|
||||||
goToParentDirectory();
|
return await _goToParentDirectory(isBack: isBack);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return await _openDirectoryPath(path, isBack: isBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
|
||||||
if (!isBack) {
|
if (!isBack) {
|
||||||
pushHistory();
|
pushHistory();
|
||||||
}
|
}
|
||||||
@@ -458,8 +488,10 @@ class FileController {
|
|||||||
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
||||||
fd.format(isWindows, sort: sortBy.value);
|
fd.format(isWindows, sort: sortBy.value);
|
||||||
directory.value = fd;
|
directory.value = fd;
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Failed to openDirectory $path: $e");
|
debugPrint("Failed to openDirectory $path: $e");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,19 +519,22 @@ class FileController {
|
|||||||
goBack();
|
goBack();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openDirectory(path, isBack: true);
|
unawaited(_openDirectoryPath(path, isBack: true).then<void>((_) {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void goToParentDirectory() {
|
void goToParentDirectory() {
|
||||||
|
unawaited(_goToParentDirectory().then<void>((_) {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _goToParentDirectory({bool isBack = false}) async {
|
||||||
final isWindows = options.value.isWindows;
|
final isWindows = options.value.isWindows;
|
||||||
final dirPath = directory.value.path;
|
final dirPath = directory.value.path;
|
||||||
var parent = PathUtil.dirname(dirPath, isWindows);
|
var parent = PathUtil.dirname(dirPath, isWindows);
|
||||||
// specially for C:\, D:\, goto '/'
|
// specially for C:\, D:\, goto '/'
|
||||||
if (parent == dirPath && isWindows) {
|
if (parent == dirPath && isWindows) {
|
||||||
openDirectory('/');
|
return await _openDirectoryPath('/', isBack: isBack);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
openDirectory(parent);
|
return await _openDirectoryPath(parent, isBack: isBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO deprecated this
|
// TODO deprecated this
|
||||||
@@ -545,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,
|
||||||
@@ -892,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();
|
||||||
@@ -904,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();
|
||||||
@@ -975,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) {
|
||||||
@@ -1001,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 {
|
||||||
@@ -1010,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;
|
||||||
@@ -1035,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) {
|
||||||
@@ -1071,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']);
|
||||||
@@ -1120,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,
|
||||||
@@ -1168,6 +1315,9 @@ class JobController {
|
|||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
jobTable.clear();
|
jobTable.clear();
|
||||||
|
_transferConflictJobToBatch.clear();
|
||||||
|
_transferConflictRememberBatchId = null;
|
||||||
|
_transferConflictRememberOverrideConfirm = null;
|
||||||
jobResultListener.clear();
|
jobResultListener.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1510,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");
|
||||||
}
|
}
|
||||||
@@ -1819,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 = {};
|
||||||
@@ -110,6 +112,9 @@ class CachedPeerData {
|
|||||||
class FfiModel with ChangeNotifier {
|
class FfiModel with ChangeNotifier {
|
||||||
CachedPeerData cachedPeerData = CachedPeerData();
|
CachedPeerData cachedPeerData = CachedPeerData();
|
||||||
PeerInfo _pi = PeerInfo();
|
PeerInfo _pi = PeerInfo();
|
||||||
|
int? lastUserDisplay;
|
||||||
|
int? pendingMonitorRestore;
|
||||||
|
Timer? _pendingRestoreTimer;
|
||||||
Rect? _rect;
|
Rect? _rect;
|
||||||
|
|
||||||
var _inputBlocked = false;
|
var _inputBlocked = false;
|
||||||
@@ -119,6 +124,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
bool _touchMode = false;
|
bool _touchMode = false;
|
||||||
late VirtualMouseMode virtualMouseMode;
|
late VirtualMouseMode virtualMouseMode;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
Timer? _restartReconnectDelayTimer;
|
||||||
var _reconnects = 1;
|
var _reconnects = 1;
|
||||||
DateTime? _offlineReconnectStartTime;
|
DateTime? _offlineReconnectStartTime;
|
||||||
bool _viewOnly = false;
|
bool _viewOnly = false;
|
||||||
@@ -245,11 +251,14 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
_pi = PeerInfo();
|
_pi = PeerInfo();
|
||||||
|
lastUserDisplay = null;
|
||||||
|
_cancelPendingMonitorRestore();
|
||||||
_secure = null;
|
_secure = null;
|
||||||
_direct = null;
|
_direct = null;
|
||||||
_inputBlocked = false;
|
_inputBlocked = false;
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
waitForImageTimer?.cancel();
|
waitForImageTimer?.cancel();
|
||||||
timerScreenshot?.cancel();
|
timerScreenshot?.cancel();
|
||||||
@@ -341,6 +350,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
} else if (name == 'connection_ready') {
|
} else if (name == 'connection_ready') {
|
||||||
setConnectionType(peerId, evt['secure'] == 'true',
|
setConnectionType(peerId, evt['secure'] == 'true',
|
||||||
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
evt['direct'] == 'true', evt['stream_type'] ?? '');
|
||||||
|
resetRestartReconnectState();
|
||||||
} else if (name == 'switch_display') {
|
} else if (name == 'switch_display') {
|
||||||
// switch display is kept for backward compatibility
|
// switch display is kept for backward compatibility
|
||||||
handleSwitchDisplay(evt, sessionId, peerId);
|
handleSwitchDisplay(evt, sessionId, peerId);
|
||||||
@@ -922,8 +932,29 @@ class FfiModel with ChangeNotifier {
|
|||||||
enterUserLoginAndPasswordDialog(
|
enterUserLoginAndPasswordDialog(
|
||||||
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
sessionId, dialogManager, 'terminal-admin-login-tip', false);
|
||||||
} else if (type == 'restarting') {
|
} else if (type == 'restarting') {
|
||||||
showMsgBox(sessionId, type, title, text, link, false, dialogManager,
|
// Treat restart messages as reconnect control events. Rust still sends
|
||||||
hasCancel: false);
|
// title/text for legacy UI and translation reuse; Flutter keeps the last
|
||||||
|
// frame briefly, then shows the Connecting overlay.
|
||||||
|
if (_restartReconnectDelayTimer == null) {
|
||||||
|
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||||
|
_cancelPendingMonitorRestore();
|
||||||
|
bind.sessionReconnect(sessionId: sessionId, forceRelay: false);
|
||||||
|
clearPermissions();
|
||||||
|
// Retry once more after the silent window so restart reconnect attempts
|
||||||
|
// are spaced by the empirical short cadence instead of only updating UI.
|
||||||
|
_restartReconnectDelayTimer =
|
||||||
|
Timer(Duration(seconds: _restartReconnectSilentDelaySecs), () {
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
if (parent.target?.closed == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (type == 'restarting-show') {
|
||||||
|
_restartReconnectDelayTimer?.cancel();
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
reconnect(dialogManager, sessionId, false);
|
||||||
} else if (type == 'wait-remote-accept-nook') {
|
} else if (type == 'wait-remote-accept-nook') {
|
||||||
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
showWaitAcceptDialog(sessionId, type, title, text, dialogManager);
|
||||||
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
} else if (type == 'on-uac' || type == 'on-foreground-elevated') {
|
||||||
@@ -949,6 +980,11 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetRestartReconnectState() {
|
||||||
|
_restartReconnectDelayTimer?.cancel();
|
||||||
|
_restartReconnectDelayTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Auto-retry check for "Remote desktop is offline" error.
|
/// Auto-retry check for "Remote desktop is offline" error.
|
||||||
/// returns true to auto-retry, false otherwise.
|
/// returns true to auto-retry, false otherwise.
|
||||||
bool shouldAutoRetryOnOffline(
|
bool shouldAutoRetryOnOffline(
|
||||||
@@ -1054,10 +1090,22 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _cancelPendingMonitorRestore() {
|
||||||
|
_pendingRestoreTimer?.cancel();
|
||||||
|
_pendingRestoreTimer = null;
|
||||||
|
pendingMonitorRestore = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelPendingRestoreTimer() {
|
||||||
|
_pendingRestoreTimer?.cancel();
|
||||||
|
_pendingRestoreTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
||||||
bool forceRelay) {
|
bool forceRelay) {
|
||||||
// Disable relative mouse mode before reconnecting to ensure cursor is released.
|
// Disable relative mouse mode before reconnecting to ensure cursor is released.
|
||||||
parent.target?.inputModel.setRelativeMouseMode(false);
|
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||||
|
_cancelPendingMonitorRestore();
|
||||||
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
@@ -1371,9 +1419,29 @@ class FfiModel with ChangeNotifier {
|
|||||||
// now replaced to _updateCurDisplay
|
// now replaced to _updateCurDisplay
|
||||||
updateCurDisplay(sessionId);
|
updateCurDisplay(sessionId);
|
||||||
}
|
}
|
||||||
|
// After reconnecting, restore the last selected monitor once the canvas is ready.
|
||||||
|
// Switching earlier can offset the view if the monitor sizes differ.
|
||||||
|
final last = lastUserDisplay;
|
||||||
|
pendingMonitorRestore = (!isCache &&
|
||||||
|
last != null &&
|
||||||
|
last != currentDisplay &&
|
||||||
|
bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||||
|
sessionId: sessionId) !=
|
||||||
|
'Y' &&
|
||||||
|
((last == kAllDisplayValue && _pi.displays.isNotEmpty) ||
|
||||||
|
(last >= 0 && last < _pi.displays.length)))
|
||||||
|
? last
|
||||||
|
: null;
|
||||||
|
// Fallback if the first image event never reaches this tab (multi-UI).
|
||||||
|
_pendingRestoreTimer?.cancel();
|
||||||
|
if (pendingMonitorRestore != null) {
|
||||||
|
_pendingRestoreTimer = Timer(const Duration(milliseconds: 1500),
|
||||||
|
() => parent.target?._applyPendingMonitorRestore());
|
||||||
|
}
|
||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
_reconnects = 1;
|
_reconnects = 1;
|
||||||
_offlineReconnectStartTime = null;
|
_offlineReconnectStartTime = null;
|
||||||
|
resetRestartReconnectState();
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
@@ -3666,6 +3734,7 @@ class FFI {
|
|||||||
|
|
||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
void mobileReset() {
|
void mobileReset() {
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
ffiModel.waitForFirstImage.value = true;
|
ffiModel.waitForFirstImage.value = true;
|
||||||
ffiModel.isRefreshing = false;
|
ffiModel.isRefreshing = false;
|
||||||
ffiModel.waitForImageDialogShow.value = true;
|
ffiModel.waitForImageDialogShow.value = true;
|
||||||
@@ -3879,16 +3948,35 @@ class FFI {
|
|||||||
}
|
}
|
||||||
if (ffiModel.waitForFirstImage.value == true) {
|
if (ffiModel.waitForFirstImage.value == true) {
|
||||||
ffiModel.waitForFirstImage.value = false;
|
ffiModel.waitForFirstImage.value = false;
|
||||||
|
ffiModel.cancelPendingRestoreTimer();
|
||||||
|
ffiModel.resetRestartReconnectState();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
await canvasModel.updateViewStyle();
|
try {
|
||||||
await canvasModel.updateScrollStyle();
|
await canvasModel.updateViewStyle();
|
||||||
await canvasModel.initializeEdgeScrollEdgeThickness();
|
await canvasModel.updateScrollStyle();
|
||||||
for (final cb in imageModel.callbacksOnFirstImage) {
|
await canvasModel.initializeEdgeScrollEdgeThickness();
|
||||||
cb(id);
|
for (final cb in imageModel.callbacksOnFirstImage) {
|
||||||
|
cb(id);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_applyPendingMonitorRestore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyPendingMonitorRestore() {
|
||||||
|
final restore = ffiModel.pendingMonitorRestore;
|
||||||
|
ffiModel._cancelPendingMonitorRestore();
|
||||||
|
if (restore == null || closed) return;
|
||||||
|
// The display list may have changed since the restore was queued.
|
||||||
|
final displays = ffiModel.pi.displays;
|
||||||
|
if ((restore == kAllDisplayValue && displays.isNotEmpty) ||
|
||||||
|
(restore >= 0 && restore < displays.length)) {
|
||||||
|
openMonitorInTheSameTab(restore, this, ffiModel.pi,
|
||||||
|
recordSelection: false, updateCursorPos: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Login with [password], choose if the client should [remember] it.
|
/// Login with [password], choose if the client should [remember] it.
|
||||||
void login(String osUsername, String osPassword, SessionID sessionId,
|
void login(String osUsername, String osPassword, SessionID sessionId,
|
||||||
String password, bool remember) {
|
String password, bool remember) {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
@@ -27,25 +26,34 @@ class TerminalModel with ChangeNotifier {
|
|||||||
// Buffer for output data received before terminal view has valid dimensions.
|
// Buffer for output data received before terminal view has valid dimensions.
|
||||||
// This prevents NaN errors when writing to terminal before layout is complete.
|
// This prevents NaN errors when writing to terminal before layout is complete.
|
||||||
final _pendingOutputChunks = <String>[];
|
final _pendingOutputChunks = <String>[];
|
||||||
|
final _pendingOutputSuppressFlags = <bool>[];
|
||||||
int _pendingOutputSize = 0;
|
int _pendingOutputSize = 0;
|
||||||
static const int _kMaxOutputBufferChars = 8 * 1024;
|
static const int _kMaxOutputBufferChars = 8 * 1024;
|
||||||
// View ready state: true when terminal has valid dimensions, safe to write
|
// View ready state: true when terminal has valid dimensions, safe to write
|
||||||
bool _terminalViewReady = false;
|
bool _terminalViewReady = false;
|
||||||
|
bool _markViewReadyScheduled = false;
|
||||||
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
|
bool _suppressTerminalOutput = false;
|
||||||
|
bool _suppressNextTerminalDataOutput = false;
|
||||||
|
|
||||||
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
void Function(int w, int h, int pw, int ph)? onResizeExternal;
|
||||||
|
|
||||||
|
/// Called when the terminal session ends (shell exits).
|
||||||
|
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
|
||||||
|
VoidCallback? onClosed;
|
||||||
|
|
||||||
Future<void> _handleInput(String data) async {
|
Future<void> _handleInput(String data) async {
|
||||||
// If we press the `Enter` button on Android,
|
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
|
||||||
// `data` can be '\r' or '\n' when using different keyboards.
|
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
|
||||||
// Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline.
|
// - Peer Windows: '\r' works, '\n' is just a newline.
|
||||||
// Android -> Linux. Both '\r' and '\n' work as expected (execute a command).
|
// - Peer Linux: canonical-mode shells accept both, but raw-mode apps
|
||||||
// So when we receive '\n', we may need to convert it to '\r' to ensure compatibility.
|
// (readline, prompt_toolkit, vim, TUI frameworks) expect '\r'.
|
||||||
// Desktop -> Desktop works fine.
|
// - Peer macOS: same as Linux, raw-mode apps expect '\r'
|
||||||
// Check if we are on mobile or web(mobile), and convert '\n' to '\r'.
|
// (https://github.com/rustdesk/rustdesk/issues/14907).
|
||||||
|
// So on mobile / web-mobile, always normalize a lone '\n' to '\r'.
|
||||||
|
// We deliberately do not touch multi-character payloads (e.g. pasted text)
|
||||||
|
// so embedded newlines in pasted content are preserved.
|
||||||
final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop));
|
final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop));
|
||||||
if (isMobileOrWebMobile && isPeerWindows && data == '\n') {
|
if (isMobileOrWebMobile && data == '\n') {
|
||||||
data = '\r';
|
data = '\r';
|
||||||
}
|
}
|
||||||
if (_terminalOpened) {
|
if (_terminalOpened) {
|
||||||
@@ -70,7 +78,10 @@ class TerminalModel with ChangeNotifier {
|
|||||||
terminalController = TerminalController();
|
terminalController = TerminalController();
|
||||||
|
|
||||||
// Setup terminal callbacks
|
// Setup terminal callbacks
|
||||||
terminal.onOutput = _handleInput;
|
terminal.onOutput = (data) {
|
||||||
|
if (_suppressTerminalOutput) return;
|
||||||
|
_handleInput(data);
|
||||||
|
};
|
||||||
|
|
||||||
terminal.onResize = (w, h, pw, ph) async {
|
terminal.onResize = (w, h, pw, ph) async {
|
||||||
// Validate all dimensions before using them
|
// Validate all dimensions before using them
|
||||||
@@ -84,7 +95,7 @@ class TerminalModel with ChangeNotifier {
|
|||||||
// Mark terminal view as ready and flush any buffered output on first valid resize.
|
// Mark terminal view as ready and flush any buffered output on first valid resize.
|
||||||
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
|
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
|
||||||
if (!_terminalViewReady) {
|
if (!_terminalViewReady) {
|
||||||
_markViewReady();
|
_scheduleMarkViewReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_terminalOpened) {
|
if (_terminalOpened) {
|
||||||
@@ -110,14 +121,16 @@ class TerminalModel with ChangeNotifier {
|
|||||||
void onReady() {
|
void onReady() {
|
||||||
parent.dialogManager.dismissAll();
|
parent.dialogManager.dismissAll();
|
||||||
|
|
||||||
// Fire and forget - don't block onReady
|
// Fire and forget - don't block onReady. If the transport reconnects while
|
||||||
openTerminal().catchError((e) {
|
// this model is still open, re-send OpenTerminal so the remote service marks
|
||||||
|
// the persistent session active again and resumes output streaming.
|
||||||
|
openTerminal(force: _terminalOpened).catchError((e) {
|
||||||
debugPrint('[TerminalModel] Error opening terminal: $e');
|
debugPrint('[TerminalModel] Error opening terminal: $e');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openTerminal() async {
|
Future<void> openTerminal({bool force = false}) async {
|
||||||
if (_terminalOpened) return;
|
if (_terminalOpened && !force) return;
|
||||||
// Request the remote side to open a terminal with default shell
|
// Request the remote side to open a terminal with default shell
|
||||||
// The remote side will decide which shell to use based on its OS
|
// The remote side will decide which shell to use based on its OS
|
||||||
|
|
||||||
@@ -237,6 +250,33 @@ class TerminalModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int getExitCodeFromEvt(Map<String, dynamic> evt) {
|
||||||
|
if (evt.containsKey('exit_code')) {
|
||||||
|
final v = evt['exit_code'];
|
||||||
|
if (v is int) {
|
||||||
|
// Desktop and mobile send exit_code as an int
|
||||||
|
return v;
|
||||||
|
} else if (v is String) {
|
||||||
|
// Web sends exit_code as a string
|
||||||
|
final parsed = int.tryParse(v);
|
||||||
|
if (parsed != null) {
|
||||||
|
return parsed;
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalModel] Failed to parse exit_code as integer: $v. Expected a numeric string.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'[TerminalModel] Unexpected exit_code type: ${v.runtimeType}, value: $v. Expected int or String.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint('[TerminalModel] Event does not contain exit_code');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void handleTerminalResponse(Map<String, dynamic> evt) {
|
void handleTerminalResponse(Map<String, dynamic> evt) {
|
||||||
final String? type = evt['type'];
|
final String? type = evt['type'];
|
||||||
final int evtTerminalId = getTerminalIdFromEvt(evt);
|
final int evtTerminalId = getTerminalIdFromEvt(evt);
|
||||||
@@ -275,9 +315,12 @@ class TerminalModel with ChangeNotifier {
|
|||||||
if (success) {
|
if (success) {
|
||||||
_terminalOpened = true;
|
_terminalOpened = true;
|
||||||
|
|
||||||
// On reconnect ("Reconnected to existing terminal"), server may replay recent output.
|
// On reconnect, the server may replay recent output. That replay can include
|
||||||
// If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
|
// terminal queries like DSR/DA; xterm answers them through onOutput as
|
||||||
// We intentionally accept this tradeoff for now to keep logic simple.
|
// "^[[1;1R^[[2;2R^[[>0;0;0c", which must not be sent back to the peer.
|
||||||
|
final replayTerminalOutput = evt['replay_terminal_output'];
|
||||||
|
_suppressNextTerminalDataOutput = replayTerminalOutput == true ||
|
||||||
|
message == 'Reconnected to existing terminal with pending output';
|
||||||
|
|
||||||
// Fallback: if terminal view is not yet ready but already has valid
|
// Fallback: if terminal view is not yet ready but already has valid
|
||||||
// dimensions (e.g. layout completed before open response arrived),
|
// dimensions (e.g. layout completed before open response arrived),
|
||||||
@@ -285,7 +328,7 @@ class TerminalModel with ChangeNotifier {
|
|||||||
if (!_terminalViewReady &&
|
if (!_terminalViewReady &&
|
||||||
terminal.viewWidth > 0 &&
|
terminal.viewWidth > 0 &&
|
||||||
terminal.viewHeight > 0) {
|
terminal.viewHeight > 0) {
|
||||||
_markViewReady();
|
_scheduleMarkViewReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process any buffered input
|
// Process any buffered input
|
||||||
@@ -297,12 +340,16 @@ class TerminalModel with ChangeNotifier {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final persistentSessions =
|
final persistentSessions =
|
||||||
evt['persistent_sessions'] as List<dynamic>? ?? [];
|
(evt['persistent_sessions'] as List<dynamic>? ?? [])
|
||||||
|
.whereType<int>()
|
||||||
|
.where((id) => !parent.terminalModels.containsKey(id))
|
||||||
|
.toList();
|
||||||
if (kWindowId != null && persistentSessions.isNotEmpty) {
|
if (kWindowId != null && persistentSessions.isNotEmpty) {
|
||||||
DesktopMultiWindow.invokeMethod(
|
DesktopMultiWindow.invokeMethod(
|
||||||
kWindowId!,
|
kWindowId!,
|
||||||
kWindowEventRestoreTerminalSessions,
|
kWindowEventRestoreTerminalSessions,
|
||||||
jsonEncode({
|
jsonEncode({
|
||||||
|
'peer_id': id,
|
||||||
'persistent_sessions': persistentSessions,
|
'persistent_sessions': persistentSessions,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -332,6 +379,8 @@ class TerminalModel with ChangeNotifier {
|
|||||||
final data = evt['data'];
|
final data = evt['data'];
|
||||||
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
final suppressTerminalOutput = _suppressNextTerminalDataOutput;
|
||||||
|
_suppressNextTerminalDataOutput = false;
|
||||||
try {
|
try {
|
||||||
String text = '';
|
String text = '';
|
||||||
if (data is String) {
|
if (data is String) {
|
||||||
@@ -351,7 +400,7 @@ class TerminalModel with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeToTerminal(text);
|
_writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
debugPrint('[TerminalModel] Failed to process terminal data: $e');
|
||||||
}
|
}
|
||||||
@@ -361,7 +410,10 @@ class TerminalModel with ChangeNotifier {
|
|||||||
/// Write text to terminal, buffering if the view is not yet ready.
|
/// Write text to terminal, buffering if the view is not yet ready.
|
||||||
/// All terminal output should go through this method to avoid NaN errors
|
/// All terminal output should go through this method to avoid NaN errors
|
||||||
/// from writing before the terminal view has valid layout dimensions.
|
/// from writing before the terminal view has valid layout dimensions.
|
||||||
void _writeToTerminal(String text) {
|
void _writeToTerminal(
|
||||||
|
String text, {
|
||||||
|
bool suppressTerminalOutput = false,
|
||||||
|
}) {
|
||||||
if (!_terminalViewReady) {
|
if (!_terminalViewReady) {
|
||||||
// If a single chunk exceeds the cap, keep only its tail.
|
// If a single chunk exceeds the cap, keep only its tail.
|
||||||
// Note: truncation may split a multi-byte ANSI escape sequence,
|
// Note: truncation may split a multi-byte ANSI escape sequence,
|
||||||
@@ -373,34 +425,73 @@ class TerminalModel with ChangeNotifier {
|
|||||||
_pendingOutputChunks
|
_pendingOutputChunks
|
||||||
..clear()
|
..clear()
|
||||||
..add(truncated);
|
..add(truncated);
|
||||||
|
_pendingOutputSuppressFlags
|
||||||
|
..clear()
|
||||||
|
..add(suppressTerminalOutput);
|
||||||
_pendingOutputSize = truncated.length;
|
_pendingOutputSize = truncated.length;
|
||||||
} else {
|
} else {
|
||||||
_pendingOutputChunks.add(text);
|
_pendingOutputChunks.add(text);
|
||||||
|
_pendingOutputSuppressFlags.add(suppressTerminalOutput);
|
||||||
_pendingOutputSize += text.length;
|
_pendingOutputSize += text.length;
|
||||||
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
|
||||||
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
while (_pendingOutputSize > _kMaxOutputBufferChars &&
|
||||||
_pendingOutputChunks.length > 1) {
|
_pendingOutputChunks.length > 1) {
|
||||||
final removed = _pendingOutputChunks.removeAt(0);
|
final removed = _pendingOutputChunks.removeAt(0);
|
||||||
|
_pendingOutputSuppressFlags.removeAt(0);
|
||||||
_pendingOutputSize -= removed.length;
|
_pendingOutputSize -= removed.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminal.write(text);
|
_writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _flushOutputBuffer() {
|
void _flushOutputBuffer() {
|
||||||
if (_pendingOutputChunks.isEmpty) return;
|
if (_pendingOutputChunks.isEmpty) return;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
|
||||||
for (final chunk in _pendingOutputChunks) {
|
for (var i = 0; i < _pendingOutputChunks.length; i++) {
|
||||||
terminal.write(chunk);
|
_writeTerminalChunk(
|
||||||
|
_pendingOutputChunks[i],
|
||||||
|
suppressTerminalOutput: _pendingOutputSuppressFlags[i],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_pendingOutputChunks.clear();
|
_pendingOutputChunks.clear();
|
||||||
|
_pendingOutputSuppressFlags.clear();
|
||||||
_pendingOutputSize = 0;
|
_pendingOutputSize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _writeTerminalChunk(
|
||||||
|
String text, {
|
||||||
|
required bool suppressTerminalOutput,
|
||||||
|
}) {
|
||||||
|
if (!suppressTerminalOutput) {
|
||||||
|
terminal.write(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final previous = _suppressTerminalOutput;
|
||||||
|
_suppressTerminalOutput = true;
|
||||||
|
try {
|
||||||
|
terminal.write(text);
|
||||||
|
} finally {
|
||||||
|
_suppressTerminalOutput = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark terminal view as ready and flush buffered output.
|
/// Mark terminal view as ready and flush buffered output.
|
||||||
|
void _scheduleMarkViewReady() {
|
||||||
|
if (_disposed || _terminalViewReady || _markViewReadyScheduled) return;
|
||||||
|
_markViewReadyScheduled = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_markViewReadyScheduled = false;
|
||||||
|
if (_disposed || _terminalViewReady) return;
|
||||||
|
if (terminal.viewWidth > 0 && terminal.viewHeight > 0) {
|
||||||
|
_markViewReady();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.ensureVisualUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
void _markViewReady() {
|
void _markViewReady() {
|
||||||
if (_terminalViewReady) return;
|
if (_terminalViewReady) return;
|
||||||
_terminalViewReady = true;
|
_terminalViewReady = true;
|
||||||
@@ -408,10 +499,12 @@ class TerminalModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
void _handleTerminalClosed(Map<String, dynamic> evt) {
|
||||||
final int exitCode = evt['exit_code'] ?? 0;
|
final int exitCode = getExitCodeFromEvt(evt);
|
||||||
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
|
||||||
_terminalOpened = false;
|
_terminalOpened = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
// Auto-close the tab/page
|
||||||
|
onClosed?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTerminalError(Map<String, dynamic> evt) {
|
void _handleTerminalError(Map<String, dynamic> evt) {
|
||||||
@@ -426,7 +519,10 @@ class TerminalModel with ChangeNotifier {
|
|||||||
// Clear buffers to free memory
|
// Clear buffers to free memory
|
||||||
_inputBuffer.clear();
|
_inputBuffer.clear();
|
||||||
_pendingOutputChunks.clear();
|
_pendingOutputChunks.clear();
|
||||||
|
_pendingOutputSuppressFlags.clear();
|
||||||
_pendingOutputSize = 0;
|
_pendingOutputSize = 0;
|
||||||
|
_markViewReadyScheduled = false;
|
||||||
|
_suppressNextTerminalDataOutput = false;
|
||||||
// Terminal cleanup is handled server-side when service closes
|
// Terminal cleanup is handled server-side when service closes
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1914,6 +1914,15 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError("sessionHandleScreenshot");
|
throw UnimplementedError("sessionHandleScreenshot");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> sessionSetCommon(
|
||||||
|
{required UuidValue sessionId, required String key, required String value, dynamic hint}) {
|
||||||
|
js.context.callMethod('setByName', [
|
||||||
|
'common',
|
||||||
|
jsonEncode({'name': key, 'value': value})
|
||||||
|
]);
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
String? sessionGetCommonSync(
|
String? sessionGetCommonSync(
|
||||||
{required UuidValue sessionId,
|
{required UuidValue sessionId,
|
||||||
required String key,
|
required String key,
|
||||||
@@ -2034,7 +2043,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.9+67
|
||||||
|
|
||||||
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,55 @@
|
|||||||
|
|
||||||
#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
|
||||||
|
/* Registered clipboard formats use IDs 0xC000 through 0xFFFF.
|
||||||
|
* https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */
|
||||||
|
#define WF_CLIPRDR_MAX_FORMATS 0x4000u
|
||||||
|
/* Registered format names are string atoms; cap the converted WCHAR name.
|
||||||
|
* https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */
|
||||||
|
#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u
|
||||||
|
/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */
|
||||||
|
#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u)
|
||||||
|
|
||||||
|
/* Validates the remote descriptor array size after cItems has been read safely. */
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if (!value || !len)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
for (i = 0; i <= max_len; i++)
|
||||||
|
{
|
||||||
|
if (value[i] == '\0')
|
||||||
|
{
|
||||||
|
*len = i;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clipboard Formats
|
* Clipboard Formats
|
||||||
*/
|
*/
|
||||||
@@ -183,6 +232,7 @@ struct _CliprdrStream
|
|||||||
FILEDESCRIPTORW m_Dsc;
|
FILEDESCRIPTORW m_Dsc;
|
||||||
void *m_pData;
|
void *m_pData;
|
||||||
UINT32 m_connID;
|
UINT32 m_connID;
|
||||||
|
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
|
||||||
};
|
};
|
||||||
typedef struct _CliprdrStream CliprdrStream;
|
typedef struct _CliprdrStream CliprdrStream;
|
||||||
|
|
||||||
@@ -224,6 +274,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;
|
||||||
@@ -235,6 +286,9 @@ struct wf_clipboard
|
|||||||
char *req_fdata;
|
char *req_fdata;
|
||||||
HANDLE req_fevent;
|
HANDLE req_fevent;
|
||||||
BOOL req_f_received;
|
BOOL req_f_received;
|
||||||
|
UINT32 req_f_conn_id_expected; // connID of the outstanding request
|
||||||
|
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
|
||||||
|
LONG req_f_stream_id_seq; // source of unique per-stream ids
|
||||||
|
|
||||||
size_t nFiles;
|
size_t nFiles;
|
||||||
size_t file_array_size;
|
size_t file_array_size;
|
||||||
@@ -265,7 +319,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
|
|||||||
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
|
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
|
||||||
static UINT cliprdr_send_lock(wfClipboard *clipboard);
|
static UINT cliprdr_send_lock(wfClipboard *clipboard);
|
||||||
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
|
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
|
||||||
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid,
|
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
|
||||||
ULONG index, UINT32 flag, DWORD positionhigh,
|
ULONG index, UINT32 flag, DWORD positionhigh,
|
||||||
DWORD positionlow, ULONG request);
|
DWORD positionlow, ULONG request);
|
||||||
|
|
||||||
@@ -348,7 +402,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
|
|||||||
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
|
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
|
||||||
ULONG *pcbRead)
|
ULONG *pcbRead)
|
||||||
{
|
{
|
||||||
int ret;
|
UINT ret;
|
||||||
CliprdrStream *instance = (CliprdrStream *)This;
|
CliprdrStream *instance = (CliprdrStream *)This;
|
||||||
wfClipboard *clipboard;
|
wfClipboard *clipboard;
|
||||||
|
|
||||||
@@ -361,12 +415,23 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
|
|||||||
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
|
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
|
||||||
return S_FALSE;
|
return S_FALSE;
|
||||||
|
|
||||||
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex,
|
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
|
||||||
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
|
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
|
||||||
instance->m_lOffset.LowPart, cb);
|
instance->m_lOffset.LowPart, cb);
|
||||||
|
|
||||||
if (ret < 0)
|
if (ret != CHANNEL_RC_OK)
|
||||||
|
{
|
||||||
|
free(clipboard->req_fdata);
|
||||||
|
clipboard->req_fdata = NULL;
|
||||||
return E_FAIL;
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clipboard->req_fsize > cb)
|
||||||
|
{
|
||||||
|
free(clipboard->req_fdata);
|
||||||
|
clipboard->req_fdata = NULL;
|
||||||
|
return STG_E_READFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
if (clipboard->req_fdata)
|
if (clipboard->req_fdata)
|
||||||
{
|
{
|
||||||
@@ -578,6 +643,7 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
|||||||
instance->m_pData = pData;
|
instance->m_pData = pData;
|
||||||
instance->m_lOffset.QuadPart = 0;
|
instance->m_lOffset.QuadPart = 0;
|
||||||
instance->m_connID = connID;
|
instance->m_connID = connID;
|
||||||
|
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
|
||||||
|
|
||||||
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
|
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
|
||||||
{
|
{
|
||||||
@@ -588,16 +654,28 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
|||||||
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
|
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
|
||||||
{
|
{
|
||||||
/* get content size of this stream */
|
/* get content size of this stream */
|
||||||
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance,
|
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
|
||||||
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
|
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
|
||||||
8) == CHANNEL_RC_OK)
|
8) == CHANNEL_RC_OK)
|
||||||
{
|
{
|
||||||
success = TRUE;
|
success = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clipboard->req_fdata != NULL)
|
if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
|
||||||
|
{
|
||||||
|
LONGLONG sz = 0;
|
||||||
|
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
|
||||||
|
if (sz < 0)
|
||||||
|
success = FALSE;
|
||||||
|
else
|
||||||
|
instance->m_lSize.QuadPart = sz;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
success = FALSE;
|
||||||
|
}
|
||||||
|
if (clipboard->req_fdata)
|
||||||
{
|
{
|
||||||
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
|
|
||||||
free(clipboard->req_fdata);
|
free(clipboard->req_fdata);
|
||||||
clipboard->req_fdata = NULL;
|
clipboard->req_fdata = NULL;
|
||||||
}
|
}
|
||||||
@@ -629,6 +707,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 +869,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 +889,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))
|
||||||
{
|
{
|
||||||
@@ -1328,25 +1461,35 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format)
|
|||||||
return local_format;
|
return local_format;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void map_ensure_capacity(wfClipboard *clipboard)
|
static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity)
|
||||||
{
|
{
|
||||||
|
size_t old_size;
|
||||||
|
formatMapping *new_map;
|
||||||
|
|
||||||
if (!clipboard)
|
if (!clipboard)
|
||||||
return;
|
return FALSE;
|
||||||
|
|
||||||
if (clipboard->map_size >= clipboard->map_capacity)
|
if (!clipboard->format_mappings)
|
||||||
{
|
return FALSE;
|
||||||
size_t new_size;
|
|
||||||
formatMapping *new_map;
|
|
||||||
new_size = clipboard->map_capacity * 2;
|
|
||||||
new_map =
|
|
||||||
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size);
|
|
||||||
|
|
||||||
if (!new_map)
|
if (capacity <= clipboard->map_capacity)
|
||||||
return;
|
return TRUE;
|
||||||
|
|
||||||
clipboard->format_mappings = new_map;
|
if (capacity > WF_CLIPRDR_MAX_FORMATS ||
|
||||||
clipboard->map_capacity = new_size;
|
capacity > ((size_t)-1) / sizeof(formatMapping))
|
||||||
}
|
return FALSE;
|
||||||
|
|
||||||
|
old_size = clipboard->map_capacity;
|
||||||
|
new_map =
|
||||||
|
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity);
|
||||||
|
|
||||||
|
if (!new_map)
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size));
|
||||||
|
clipboard->format_mappings = new_map;
|
||||||
|
clipboard->map_capacity = capacity;
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static BOOL clear_format_map(wfClipboard *clipboard)
|
static BOOL clear_format_map(wfClipboard *clipboard)
|
||||||
@@ -1373,6 +1516,13 @@ static BOOL clear_format_map(wfClipboard *clipboard)
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard)
|
||||||
|
{
|
||||||
|
clear_format_map(clipboard);
|
||||||
|
clipboard->copied = FALSE;
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
|
||||||
{
|
{
|
||||||
CLIPRDR_TEMP_DIRECTORY tempDirectory;
|
CLIPRDR_TEMP_DIRECTORY tempDirectory;
|
||||||
@@ -1651,12 +1801,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
|
|||||||
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
|
||||||
}
|
}
|
||||||
|
|
||||||
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
|
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
|
||||||
UINT32 flag, DWORD positionhigh, DWORD positionlow,
|
UINT32 flag, DWORD positionhigh, DWORD positionlow,
|
||||||
ULONG nreq)
|
ULONG nreq)
|
||||||
{
|
{
|
||||||
UINT rc;
|
UINT rc;
|
||||||
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest;
|
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
|
||||||
|
|
||||||
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
|
||||||
return ERROR_INTERNAL_ERROR;
|
return ERROR_INTERNAL_ERROR;
|
||||||
@@ -1667,12 +1817,11 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
clipboard->req_f_received = FALSE;
|
clipboard->req_f_received = FALSE;
|
||||||
|
clipboard->req_f_conn_id_expected = connID;
|
||||||
|
clipboard->req_f_stream_id_expected = streamId;
|
||||||
|
|
||||||
fileContentsRequest.connID = connID;
|
fileContentsRequest.connID = connID;
|
||||||
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
|
fileContentsRequest.streamId = streamId;
|
||||||
// But it is OK, because it is only used to check if the stream is the same in
|
|
||||||
// `wf_cliprdr_server_file_contents_request()` function.
|
|
||||||
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
|
|
||||||
fileContentsRequest.listIndex = index;
|
fileContentsRequest.listIndex = index;
|
||||||
fileContentsRequest.dwFlags = flag;
|
fileContentsRequest.dwFlags = flag;
|
||||||
fileContentsRequest.nPositionLow = positionlow;
|
fileContentsRequest.nPositionLow = positionlow;
|
||||||
@@ -2161,16 +2310,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] =
|
||||||
@@ -2365,6 +2514,16 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
|
|
||||||
if (!clear_format_map(clipboard))
|
if (!clear_format_map(clipboard))
|
||||||
return ERROR_INTERNAL_ERROR;
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
clipboard->copied = FALSE;
|
||||||
|
|
||||||
|
if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS)
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
|
||||||
|
if (formatList->numFormats > 0 && !formatList->formats)
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
|
||||||
|
if (!map_ensure_capacity(clipboard, formatList->numFormats))
|
||||||
|
return ERROR_INTERNAL_ERROR;
|
||||||
|
|
||||||
clipboard->copied = TRUE;
|
clipboard->copied = TRUE;
|
||||||
|
|
||||||
@@ -2372,19 +2531,58 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
{
|
{
|
||||||
format = &(formatList->formats[i]);
|
format = &(formatList->formats[i]);
|
||||||
mapping = &(clipboard->format_mappings[i]);
|
mapping = &(clipboard->format_mappings[i]);
|
||||||
|
/* Do not validate the peer-provided formatId as a Windows registered format.
|
||||||
|
* It is only a remote protocol ID used when requesting data from the peer.
|
||||||
|
* For named formats, RegisterClipboardFormatW creates the local Windows
|
||||||
|
* clipboard ID below, and that local ID is checked before publishing. */
|
||||||
mapping->remote_format_id = format->formatId;
|
mapping->remote_format_id = format->formatId;
|
||||||
|
|
||||||
if (format->formatName)
|
if (format->formatName)
|
||||||
{
|
{
|
||||||
int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName,
|
size_t name_len;
|
||||||
strlen(format->formatName), NULL, 0);
|
int size;
|
||||||
mapping->name = calloc(size + 1, sizeof(WCHAR));
|
|
||||||
|
|
||||||
if (mapping->name)
|
if (!wf_cliprdr_bounded_strlen(format->formatName,
|
||||||
|
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
|
||||||
{
|
{
|
||||||
MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName),
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
mapping->name, size);
|
}
|
||||||
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
|
||||||
|
if (name_len == 0)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||||
|
NULL, 0);
|
||||||
|
if (size <= 0)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping->name = calloc((size_t)size + 1, sizeof(WCHAR));
|
||||||
|
if (!mapping->name)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
|
||||||
|
mapping->name, size) != size)
|
||||||
|
{
|
||||||
|
free(mapping->name);
|
||||||
|
mapping->name = NULL;
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
|
||||||
|
if (mapping->local_format_id == 0)
|
||||||
|
{
|
||||||
|
return wf_cliprdr_server_format_list_fail(clipboard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2394,7 +2592,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
clipboard->map_size++;
|
clipboard->map_size++;
|
||||||
map_ensure_capacity(clipboard);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file_transferring(clipboard))
|
if (file_transferring(clipboard))
|
||||||
@@ -2778,6 +2975,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 +3009,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);
|
||||||
@@ -2843,6 +3042,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
|||||||
BOOL bIsStreamFile = TRUE;
|
BOOL bIsStreamFile = TRUE;
|
||||||
static LPSTREAM pStreamStc = NULL;
|
static LPSTREAM pStreamStc = NULL;
|
||||||
static UINT32 uStreamIdStc = 0;
|
static UINT32 uStreamIdStc = 0;
|
||||||
|
static UINT32 uConnIdStc = 0;
|
||||||
wfClipboard *clipboard;
|
wfClipboard *clipboard;
|
||||||
UINT rc = ERROR_INTERNAL_ERROR;
|
UINT rc = ERROR_INTERNAL_ERROR;
|
||||||
UINT sRc;
|
UINT sRc;
|
||||||
@@ -2916,7 +3116,8 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
|||||||
vFormatEtc.lindex = fileContentsRequest->listIndex;
|
vFormatEtc.lindex = fileContentsRequest->listIndex;
|
||||||
vFormatEtc.ptd = NULL;
|
vFormatEtc.ptd = NULL;
|
||||||
|
|
||||||
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc)
|
if ((uStreamIdStc != fileContentsRequest->streamId) ||
|
||||||
|
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
|
||||||
{
|
{
|
||||||
LPENUMFORMATETC pEnumFormatEtc;
|
LPENUMFORMATETC pEnumFormatEtc;
|
||||||
ULONG CeltFetched;
|
ULONG CeltFetched;
|
||||||
@@ -2947,6 +3148,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
|||||||
{
|
{
|
||||||
pStreamStc = vStgMedium.pstm;
|
pStreamStc = vStgMedium.pstm;
|
||||||
uStreamIdStc = fileContentsRequest->streamId;
|
uStreamIdStc = fileContentsRequest->streamId;
|
||||||
|
uConnIdStc = fileContentsRequest->connID;
|
||||||
bIsStreamFile = TRUE;
|
bIsStreamFile = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3110,6 +3312,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
|||||||
rc = ERROR_INTERNAL_ERROR;
|
rc = ERROR_INTERNAL_ERROR;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
|
||||||
|
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
|
||||||
|
return CHANNEL_RC_OK;
|
||||||
clipboard->req_fsize = 0;
|
clipboard->req_fsize = 0;
|
||||||
clipboard->req_fdata = NULL;
|
clipboard->req_fdata = NULL;
|
||||||
|
|
||||||
@@ -3120,6 +3325,13 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
clipboard->req_fsize = fileContentsResponse->cbRequested;
|
clipboard->req_fsize = fileContentsResponse->cbRequested;
|
||||||
|
/*
|
||||||
|
* Keep the zero-size allocation: supported Windows builds use the Microsoft
|
||||||
|
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
|
||||||
|
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
|
||||||
|
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
|
||||||
|
* nonzero length cannot have a NULL data pointer on the normal call path.
|
||||||
|
*/
|
||||||
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
|
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
|
||||||
if (!clipboard->req_fdata)
|
if (!clipboard->req_fdata)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ pub enum MouseButton {
|
|||||||
|
|
||||||
/// Scroll up button
|
/// Scroll up button
|
||||||
ScrollUp,
|
ScrollUp,
|
||||||
/// Left right button
|
/// Scroll down button
|
||||||
ScrollDown,
|
ScrollDown,
|
||||||
/// Left right button
|
/// Scroll left button
|
||||||
ScrollLeft,
|
ScrollLeft,
|
||||||
/// Left right button
|
/// Scroll right button
|
||||||
ScrollRight,
|
ScrollRight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ impl KeyboardControllable for Enigo {
|
|||||||
// Windows uses uft-16 encoding. We need to check
|
// Windows uses uft-16 encoding. We need to check
|
||||||
// for variable length characters. As such some
|
// for variable length characters. As such some
|
||||||
// characters can be 32 bit long and those are
|
// characters can be 32 bit long and those are
|
||||||
// encoded in such called hight and low surrogates
|
// encoded in so-called high and low surrogates
|
||||||
// each 16 bit wide that needs to be send after
|
// each 16 bit wide that needs to be send after
|
||||||
// another to the SendInput function without
|
// another to the SendInput function without
|
||||||
// being interrupted by "keyup"
|
// being interrupted by "keyup"
|
||||||
|
|||||||
Submodule libs/hbb_common updated: 3e31a94939...7e1c392c62
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.6"
|
version = "1.4.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ windows = { version = "0.61", features = [
|
|||||||
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
|
||||||
|
|
||||||
[package.metadata.winres]
|
[package.metadata.winres]
|
||||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
|
||||||
ProductName = "RustDesk"
|
ProductName = "RustDesk"
|
||||||
OriginalFilename = "rustdesk.exe"
|
OriginalFilename = "rustdesk.exe"
|
||||||
FileDescription = "RustDesk Remote Desktop"
|
FileDescription = "RustDesk Remote Desktop"
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ mod webrtc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tile_log2(threads: u32) -> std::os::raw::c_uint {
|
||||||
|
(threads as f64).log2().ceil() as _
|
||||||
|
}
|
||||||
|
|
||||||
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
|
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
|
||||||
use aom_superblock_size::*;
|
use aom_superblock_size::*;
|
||||||
let resolution = width * height;
|
let resolution = width * height;
|
||||||
@@ -160,8 +164,7 @@ mod webrtc {
|
|||||||
} else {
|
} else {
|
||||||
AV1E_SET_TILE_COLUMNS
|
AV1E_SET_TILE_COLUMNS
|
||||||
};
|
};
|
||||||
// Failed on android
|
call_ctl!(ctx, tile_set, tile_log2(cfg.g_threads));
|
||||||
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
|
|
||||||
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
|
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
|
||||||
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
|
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
|
||||||
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
|
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
|
||||||
@@ -197,6 +200,23 @@ mod webrtc {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::os::raw::c_uint;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tile_log2_uses_c_uint_and_rounds_up() {
|
||||||
|
let one_thread: c_uint = tile_log2(1);
|
||||||
|
let three_threads: c_uint = tile_log2(3);
|
||||||
|
let max_threads: c_uint = tile_log2(64);
|
||||||
|
|
||||||
|
assert_eq!(one_thread, 0);
|
||||||
|
assert_eq!(three_threads, 2);
|
||||||
|
assert_eq!(max_threads, 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncoderApi for AomEncoder {
|
impl EncoderApi for AomEncoder {
|
||||||
|
|||||||
@@ -52,6 +52,33 @@ lazy_static::lazy_static! {
|
|||||||
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
|
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_windows(cls: &str, name: &str) -> Result<Vec<HWND>> {
|
||||||
|
let name_c = CString::new(name)?;
|
||||||
|
let cls_c = if cls.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(CString::new(cls)?)
|
||||||
|
};
|
||||||
|
let mut hwnds = Vec::new();
|
||||||
|
unsafe {
|
||||||
|
let mut after = NULL as _;
|
||||||
|
loop {
|
||||||
|
let hwnd = FindWindowExA(
|
||||||
|
NULL as _,
|
||||||
|
after,
|
||||||
|
cls_c.as_ref().map_or(NULL as _, |c| c.as_ptr()),
|
||||||
|
name_c.as_ptr(),
|
||||||
|
);
|
||||||
|
if hwnd.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hwnds.push(hwnd);
|
||||||
|
after = hwnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(hwnds)
|
||||||
|
}
|
||||||
|
|
||||||
pub type REFWICPixelFormatGUID = *const GUID;
|
pub type REFWICPixelFormatGUID = *const GUID;
|
||||||
pub type WICPixelFormatGUID = GUID;
|
pub type WICPixelFormatGUID = GUID;
|
||||||
|
|
||||||
@@ -247,6 +274,8 @@ pub struct CapturerMag {
|
|||||||
rect: RECT,
|
rect: RECT,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
|
excluded_window_target: Option<(String, String)>,
|
||||||
|
excluded_windows: Vec<HWND>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for CapturerMag {
|
impl Drop for CapturerMag {
|
||||||
@@ -261,6 +290,10 @@ impl CapturerMag {
|
|||||||
MagInterface::new().is_ok()
|
MagInterface::new().is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This captures through the legacy Windows Magnification API. Do not infer
|
||||||
|
// multi-monitor capture support from privacy overlay coverage: WebRTC also
|
||||||
|
// disables its magnifier capturer when SM_CMONITORS != 1.
|
||||||
|
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
|
||||||
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
|
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||||
@@ -305,6 +338,8 @@ impl CapturerMag {
|
|||||||
},
|
},
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
excluded_window_target: None,
|
||||||
|
excluded_windows: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -436,19 +471,41 @@ impl CapturerMag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
||||||
let name_c = CString::new(name)?;
|
let mut hwnds = find_windows(cls, name)?;
|
||||||
|
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.9
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#! /usr/bin/env bash
|
#! /usr/bin/env bash
|
||||||
sed -i "s/$1/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
|
sed -i "s/\b$1\b/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
|
||||||
cargo run # to bump version in cargo lock
|
cargo run # to bump version in cargo lock
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ LExit:
|
|||||||
return WcaFinalize(er);
|
return WcaFinalize(er);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to safely delete a file or directory using handle-based deletion.
|
// Helper function to safely delete a file using handle-based deletion.
|
||||||
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
|
// Directories are refused after opening the handle.
|
||||||
BOOL SafeDeleteItem(LPCWSTR fullPath)
|
BOOL SafeDeleteItem(LPCWSTR fullPath)
|
||||||
{
|
{
|
||||||
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
|
// Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT
|
||||||
// to prevent following symlinks.
|
// to prevent following symlinks.
|
||||||
// Use shared access to allow deletion even when other processes have the file open.
|
// Use shared access to allow deletion even when other processes have the file open.
|
||||||
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
|
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
|
||||||
HANDLE hFile = CreateFileW(
|
HANDLE hFile = CreateFileW(
|
||||||
fullPath,
|
fullPath,
|
||||||
DELETE,
|
DELETE | FILE_READ_ATTRIBUTES,
|
||||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
|
||||||
NULL,
|
NULL,
|
||||||
OPEN_EXISTING,
|
OPEN_EXISTING,
|
||||||
@@ -55,6 +55,21 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BY_HANDLE_FILE_INFORMATION fileInfo;
|
||||||
|
if (FALSE == GetFileInformationByHandle(hFile, &fileInfo))
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError());
|
||||||
|
CloseHandle(hFile);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath);
|
||||||
|
CloseHandle(hFile);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
// Use SetFileInformationByHandle to mark for deletion.
|
// Use SetFileInformationByHandle to mark for deletion.
|
||||||
// The file will be deleted when the handle is closed.
|
// The file will be deleted when the handle is closed.
|
||||||
FILE_DISPOSITION_INFO dispInfo;
|
FILE_DISPOSITION_INFO dispInfo;
|
||||||
@@ -77,98 +92,74 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to recursively delete a directory's contents with detailed logging.
|
BOOL PathEndsWithSlash(LPCWSTR path)
|
||||||
void RecursiveDelete(LPCWSTR path)
|
|
||||||
{
|
{
|
||||||
// Ensure the path is not empty or null.
|
size_t length = 0;
|
||||||
if (path == NULL || path[0] == L'\0')
|
HRESULT hr = StringCchLengthW(path, MAX_PATH, &length);
|
||||||
|
if (FAILED(hr) || length == 0)
|
||||||
|
{
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
WCHAR last = path[length - 1];
|
||||||
|
return last == L'\\' || last == L'/';
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes)
|
||||||
|
{
|
||||||
|
if (!(attributes & FILE_ATTRIBUTE_READONLY))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extra safety: never operate directly on a root path.
|
DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
|
||||||
if (PathIsRootW(path))
|
if (writableAttributes == 0)
|
||||||
{
|
{
|
||||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
|
writableAttributes = FILE_ATTRIBUTE_NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SetFileAttributesW(fullPath, writableAttributes))
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError());
|
||||||
// No need to handle extended-length paths (\\?\) in this context.
|
}
|
||||||
WCHAR searchPath[MAX_PATH];
|
|
||||||
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
|
BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
|
||||||
if (FAILED(hr)) {
|
{
|
||||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
|
WCHAR fullPath[MAX_PATH];
|
||||||
return;
|
LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\";
|
||||||
|
HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName);
|
||||||
|
if (FAILED(hr))
|
||||||
|
{
|
||||||
|
WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName);
|
||||||
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
WIN32_FIND_DATAW findData;
|
DWORD attributes = GetFileAttributesW(fullPath);
|
||||||
HANDLE hFind = FindFirstFileW(searchPath, &findData);
|
if (attributes == INVALID_FILE_ATTRIBUTES)
|
||||||
|
|
||||||
if (hFind == INVALID_HANDLE_VALUE)
|
|
||||||
{
|
{
|
||||||
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
|
DWORD error = GetLastError();
|
||||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
|
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
|
||||||
return;
|
{
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
|
||||||
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
if (attributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||||
{
|
{
|
||||||
// Skip '.' and '..' directories.
|
WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath);
|
||||||
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
|
return FALSE;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
|
||||||
// No need to handle extended-length paths (\\?\) in this context.
|
|
||||||
WCHAR fullPath[MAX_PATH];
|
|
||||||
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before acting, ensure the read-only attribute is not set.
|
|
||||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
|
|
||||||
{
|
|
||||||
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
|
|
||||||
{
|
|
||||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
|
||||||
{
|
|
||||||
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
|
|
||||||
// Do not follow reparse points, only remove the link itself.
|
|
||||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
|
|
||||||
{
|
|
||||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
|
|
||||||
SafeDeleteItem(fullPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Recursively delete directory contents first
|
|
||||||
RecursiveDelete(fullPath);
|
|
||||||
// Then delete the directory itself
|
|
||||||
SafeDeleteItem(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Delete file using safe handle-based deletion
|
|
||||||
SafeDeleteItem(fullPath);
|
|
||||||
}
|
|
||||||
} while (FindNextFileW(hFind, &findData) != 0);
|
|
||||||
|
|
||||||
DWORD lastError = GetLastError();
|
|
||||||
if (lastError != ERROR_NO_MORE_FILES)
|
|
||||||
{
|
|
||||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FindClose(hFind);
|
ClearReadOnlyAttribute(fullPath, attributes);
|
||||||
|
WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
|
||||||
|
return SafeDeleteItem(fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// See `Package.wxs` for the sequence of this custom action.
|
// See `Package.wxs` for the sequence of this custom action.
|
||||||
@@ -178,13 +169,13 @@ void RecursiveDelete(LPCWSTR path)
|
|||||||
// 2. RemoveExistingProducts
|
// 2. RemoveExistingProducts
|
||||||
// ├─ TerminateProcesses
|
// ├─ TerminateProcesses
|
||||||
// ├─ TryStopDeleteService
|
// ├─ TryStopDeleteService
|
||||||
// ├─ RemoveInstallFolder - <-- Here
|
// ├─ RemoveRuntimeGeneratedFiles - <-- Here
|
||||||
// └─ RemoveFiles
|
// └─ RemoveFiles
|
||||||
// 3. InstallValidate
|
// 3. InstallValidate
|
||||||
// 4. InstallFiles
|
// 4. InstallFiles
|
||||||
// 5. InstallExecute
|
// 5. InstallExecute
|
||||||
// 6. InstallFinalize
|
// 6. InstallFinalize
|
||||||
UINT __stdcall RemoveInstallFolder(
|
UINT __stdcall RemoveRuntimeGeneratedFiles(
|
||||||
__in MSIHANDLE hInstall)
|
__in MSIHANDLE hInstall)
|
||||||
{
|
{
|
||||||
HRESULT hr = S_OK;
|
HRESULT hr = S_OK;
|
||||||
@@ -194,7 +185,7 @@ UINT __stdcall RemoveInstallFolder(
|
|||||||
LPWSTR pwz = NULL;
|
LPWSTR pwz = NULL;
|
||||||
LPWSTR pwzData = NULL;
|
LPWSTR pwzData = NULL;
|
||||||
|
|
||||||
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
|
hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
|
||||||
ExitOnFailure(hr, "Failed to initialize");
|
ExitOnFailure(hr, "Failed to initialize");
|
||||||
|
|
||||||
hr = WcaGetProperty(L"CustomActionData", &pwzData);
|
hr = WcaGetProperty(L"CustomActionData", &pwzData);
|
||||||
@@ -202,24 +193,20 @@ UINT __stdcall RemoveInstallFolder(
|
|||||||
|
|
||||||
pwz = pwzData;
|
pwz = pwzData;
|
||||||
hr = WcaReadStringFromCaData(&pwz, &installFolder);
|
hr = WcaReadStringFromCaData(&pwz, &installFolder);
|
||||||
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
|
ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);
|
||||||
|
|
||||||
if (installFolder == NULL || installFolder[0] == L'\0') {
|
if (installFolder == NULL || installFolder[0] == L'\0') {
|
||||||
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
|
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
|
||||||
goto LExit;
|
goto LExit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PathIsRootW(installFolder)) {
|
if (PathIsRootW(installFolder)) {
|
||||||
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
|
WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
|
||||||
goto LExit;
|
goto LExit;
|
||||||
}
|
}
|
||||||
|
|
||||||
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
|
WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
|
||||||
|
DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");
|
||||||
RecursiveDelete(installFolder);
|
|
||||||
|
|
||||||
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
|
|
||||||
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
|
|
||||||
|
|
||||||
LExit:
|
LExit:
|
||||||
ReleaseStr(pwzData);
|
ReleaseStr(pwzData);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ LIBRARY "CustomActions"
|
|||||||
|
|
||||||
EXPORTS
|
EXPORTS
|
||||||
CustomActionHello
|
CustomActionHello
|
||||||
RemoveInstallFolder
|
RemoveRuntimeGeneratedFiles
|
||||||
TerminateProcesses
|
TerminateProcesses
|
||||||
AddFirewallRules
|
AddFirewallRules
|
||||||
SetPropertyIsServiceRunning
|
SetPropertyIsServiceRunning
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -16,8 +16,15 @@
|
|||||||
<!-- If a command line value was stored, restore it after the registry search has been performed -->
|
<!-- If a command line value was stored, restore it after the registry search has been performed -->
|
||||||
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />
|
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />
|
||||||
|
|
||||||
<!-- If a command line value or registry value was set, update the main properties with the value -->
|
<!-- Normalize INSTALLFOLDER from the command line or registry before assigning INSTALLFOLDER_INNER. -->
|
||||||
<SetProperty Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER" />
|
<!-- Case 1: already ends with \$(var.Product)\, keep it unchanged. -->
|
||||||
|
<SetProperty Action="SetInstallFolderInnerFromProductDir" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~>> "\$(var.Product)\"" />
|
||||||
|
<!-- Case 2: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
|
||||||
|
<SetProperty Action="SetInstallFolderInnerFromProductDirNoSlash" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~>> "\$(var.Product)"" />
|
||||||
|
<!-- Case 3: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
|
||||||
|
<SetProperty Action="SetInstallFolderInnerAppendProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~>> "\" AND NOT (INSTALLFOLDER ~>> "\$(var.Product)\" OR INSTALLFOLDER ~>> "\$(var.Product)")" />
|
||||||
|
<!-- Case 4: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
|
||||||
|
<SetProperty Action="SetInstallFolderInnerAppendSlashProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND NOT INSTALLFOLDER ~>> "\" AND NOT (INSTALLFOLDER ~>> "\$(var.Product)\" OR INSTALLFOLDER ~>> "\$(var.Product)")" />
|
||||||
|
|
||||||
<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
|
<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
|
||||||
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->
|
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</Component>
|
</Component>
|
||||||
</DirectoryRef>
|
</DirectoryRef>
|
||||||
|
|
||||||
<CustomAction Id="RemoveInstallFolder.SetParam" Return="check" Property="RemoveInstallFolder" Value="[INSTALLFOLDER_INNER]" />
|
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" Value="[INSTALLFOLDER_INNER]" />
|
||||||
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
|
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
|
||||||
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
|
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
|
||||||
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);"[INSTALLFOLDER_INNER]$(var.Product).exe" --service" />
|
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);"[INSTALLFOLDER_INNER]$(var.Product).exe" --service" />
|
||||||
@@ -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.-->
|
||||||
@@ -77,21 +77,21 @@
|
|||||||
|
|
||||||
<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE="outgoing")"/>
|
<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE="outgoing")"/>
|
||||||
|
|
||||||
<Custom Action="RemoveInstallFolder" Before="RemoveFiles"/>
|
<Custom Action="RemoveRuntimeGeneratedFiles" Before="RemoveFiles" Condition="Installed AND (REMOVE="ALL" OR UPGRADINGPRODUCTCODE)"/>
|
||||||
<Custom Action="RemoveInstallFolder.SetParam" Before="RemoveInstallFolder"/>
|
<Custom Action="RemoveRuntimeGeneratedFiles.SetParam" Before="RemoveRuntimeGeneratedFiles" Condition="Installed AND (REMOVE="ALL" OR UPGRADINGPRODUCTCODE)"/>
|
||||||
<Custom Action="TryStopDeleteService" Before="RemoveInstallFolder.SetParam" />
|
<Custom Action="TryStopDeleteService" Before="RemoveRuntimeGeneratedFiles.SetParam" />
|
||||||
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
|
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
|
||||||
|
|
||||||
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
|
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
|
||||||
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
|
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
|
||||||
|
|
||||||
<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT >= 603" />
|
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT >= 603" />
|
||||||
|
|
||||||
<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
|
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
|
||||||
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
|
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
|
||||||
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>
|
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
|
||||||
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
|
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
|
||||||
<Custom Action="RemoveAmyuniIdd" Before="RemoveInstallFolder"/>
|
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
|
||||||
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
|
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
|
||||||
</InstallExecuteSequence>
|
</InstallExecuteSequence>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />
|
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />
|
||||||
|
|
||||||
<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||||
<CustomAction Id="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||||
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||||
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||||
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ Patch dialog sequence:
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
||||||
|
<?include ../Includes.wxi?>
|
||||||
<?foreach WIXUIARCH in X86;X64;A64 ?>
|
<?foreach WIXUIARCH in X86;X64;A64 ?>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<UI Id="UI_MyInstallDialog_$(WIXUIARCH)">
|
<UI Id="UI_MyInstallDialog_$(WIXUIARCH)">
|
||||||
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
|
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
|
||||||
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="5" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||||
</UI>
|
</UI>
|
||||||
|
|
||||||
<UIRef Id="UI_MyInstallDialog" />
|
<UIRef Id="UI_MyInstallDialog" />
|
||||||
@@ -64,9 +65,16 @@ Patch dialog sequence:
|
|||||||
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg" Condition="LicenseAccepted = "1"" />
|
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg" Condition="LicenseAccepted = "1"" />
|
||||||
|
|
||||||
<Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
|
<Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
|
||||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
|
<!-- Normalize INSTALLFOLDER_INNER before SetTargetPath and WixUIValidatePath run. -->
|
||||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
<!-- UI case 1: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
|
||||||
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"" />
|
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\" Order="1" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~>> "\$(var.Product)"" />
|
||||||
|
<!-- UI case 2: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
|
||||||
|
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]$(var.Product)\" Order="2" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~>> "\" AND NOT (INSTALLFOLDER_INNER ~>> "\$(var.Product)\" OR INSTALLFOLDER_INNER ~>> "\$(var.Product)")" />
|
||||||
|
<!-- UI case 3: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
|
||||||
|
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\$(var.Product)\" Order="3" Condition="INSTALLFOLDER_INNER AND NOT INSTALLFOLDER_INNER ~>> "\" AND NOT (INSTALLFOLDER_INNER ~>> "\$(var.Product)\" OR INSTALLFOLDER_INNER ~>> "\$(var.Product)")" />
|
||||||
|
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="4" />
|
||||||
|
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="6" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
||||||
|
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="7" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"" />
|
||||||
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
|
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
|
||||||
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
|
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
|
||||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1" Condition="NOT Installed" />
|
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1" Condition="NOT Installed" />
|
||||||
|
|||||||
@@ -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.9
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.6
|
Version: 1.4.9
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.6
|
Version: 1.4.9
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ pre_start()
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# When loging out from the interactive shell, the execution sequence is:
|
# When logging out from the interactive shell, the execution sequence is:
|
||||||
#
|
#
|
||||||
# IF ~/.bash_logout exists THEN
|
# IF ~/.bash_logout exists THEN
|
||||||
# execute ~/.bash_logout
|
# execute ~/.bash_logout
|
||||||
|
|||||||
@@ -130,14 +130,18 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
|
|||||||
--cc=cl \
|
--cc=cl \
|
||||||
--enable-gpl \
|
--enable-gpl \
|
||||||
--enable-d3d11va \
|
--enable-d3d11va \
|
||||||
--enable-cuda \
|
|
||||||
--enable-ffnvcodec \
|
|
||||||
--enable-hwaccel=h264_nvdec \
|
|
||||||
--enable-hwaccel=hevc_nvdec \
|
|
||||||
--enable-hwaccel=h264_d3d11va \
|
--enable-hwaccel=h264_d3d11va \
|
||||||
--enable-hwaccel=hevc_d3d11va \
|
--enable-hwaccel=hevc_d3d11va \
|
||||||
--enable-hwaccel=h264_d3d11va2 \
|
--enable-hwaccel=h264_d3d11va2 \
|
||||||
--enable-hwaccel=hevc_d3d11va2 \
|
--enable-hwaccel=hevc_d3d11va2 \
|
||||||
|
")
|
||||||
|
|
||||||
|
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86" OR VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
|
||||||
|
string(APPEND OPTIONS "\
|
||||||
|
--enable-cuda \
|
||||||
|
--enable-ffnvcodec \
|
||||||
|
--enable-hwaccel=h264_nvdec \
|
||||||
|
--enable-hwaccel=hevc_nvdec \
|
||||||
--enable-amf \
|
--enable-amf \
|
||||||
--enable-encoder=h264_amf \
|
--enable-encoder=h264_amf \
|
||||||
--enable-encoder=hevc_amf \
|
--enable-encoder=hevc_amf \
|
||||||
@@ -147,6 +151,7 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
|
|||||||
--enable-encoder=h264_qsv \
|
--enable-encoder=h264_qsv \
|
||||||
--enable-encoder=hevc_qsv \
|
--enable-encoder=hevc_qsv \
|
||||||
")
|
")
|
||||||
|
endif()
|
||||||
|
|
||||||
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
|
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
|
||||||
set(LIB_MACHINE_ARG /machine:x86)
|
set(LIB_MACHINE_ARG /machine:x86)
|
||||||
@@ -154,6 +159,9 @@ elseif(VCPKG_TARGET_IS_WINDOWS)
|
|||||||
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
|
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
|
||||||
set(LIB_MACHINE_ARG /machine:x64)
|
set(LIB_MACHINE_ARG /machine:x64)
|
||||||
string(APPEND OPTIONS " --arch=x86_64")
|
string(APPEND OPTIONS " --arch=x86_64")
|
||||||
|
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64")
|
||||||
|
set(LIB_MACHINE_ARG /machine:arm64)
|
||||||
|
string(APPEND OPTIONS " --arch=aarch64 --enable-cross-compile")
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported target architecture")
|
message(FATAL_ERROR "Unsupported target architecture")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
199
src/cli.rs
199
src/cli.rs
@@ -1,199 +0,0 @@
|
|||||||
use crate::client::*;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use hbb_common::{
|
|
||||||
config::PeerConfig,
|
|
||||||
config::READ_TIMEOUT,
|
|
||||||
futures::{SinkExt, StreamExt},
|
|
||||||
log,
|
|
||||||
message_proto::*,
|
|
||||||
protobuf::Message as _,
|
|
||||||
rendezvous_proto::ConnType,
|
|
||||||
tokio::{self, sync::mpsc},
|
|
||||||
Stream,
|
|
||||||
};
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Session {
|
|
||||||
id: String,
|
|
||||||
lc: Arc<RwLock<LoginConfigHandler>>,
|
|
||||||
sender: mpsc::UnboundedSender<Data>,
|
|
||||||
password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Session {
|
|
||||||
pub fn new(id: &str, sender: mpsc::UnboundedSender<Data>) -> Self {
|
|
||||||
let mut password = "".to_owned();
|
|
||||||
if PeerConfig::load(id).password.is_empty() {
|
|
||||||
match rpassword::prompt_password("Enter password: ") {
|
|
||||||
Ok(p) => password = p,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to read password: {:?}", e);
|
|
||||||
password = "".to_owned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let session = Self {
|
|
||||||
id: id.to_owned(),
|
|
||||||
sender,
|
|
||||||
password,
|
|
||||||
lc: Default::default(),
|
|
||||||
};
|
|
||||||
session.lc.write().unwrap().initialize(
|
|
||||||
id.to_owned(),
|
|
||||||
ConnType::PORT_FORWARD,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Interface for Session {
|
|
||||||
fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
|
||||||
return self.lc.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) {
|
|
||||||
match msgtype {
|
|
||||||
"input-password" => {
|
|
||||||
self.sender
|
|
||||||
.send(Data::Login((self.password.clone(), true)))
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
"re-input-password" => {
|
|
||||||
log::error!("{}: {}", title, text);
|
|
||||||
match rpassword::prompt_password("Enter password: ") {
|
|
||||||
Ok(password) => {
|
|
||||||
let login_data = Data::Login((password, true));
|
|
||||||
self.sender.send(login_data).ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("reinput password failed, {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msg if msg.contains("error") => {
|
|
||||||
log::error!("{}: {}: {}", msgtype, title, text);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::info!("{}: {}: {}", msgtype, title, text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_login_error(&self, err: &str) -> bool {
|
|
||||||
handle_login_error(self.lc.clone(), err, self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_peer_info(&self, pi: PeerInfo) {
|
|
||||||
self.lc.write().unwrap().handle_peer_info(&pi);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {
|
|
||||||
log::info!(
|
|
||||||
"password={}",
|
|
||||||
hbb_common::password_security::temporary_password()
|
|
||||||
);
|
|
||||||
handle_hash(self.lc.clone(), &pass, hash, self, peer).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_login_from_ui(
|
|
||||||
&self,
|
|
||||||
os_username: String,
|
|
||||||
os_password: String,
|
|
||||||
password: String,
|
|
||||||
remember: bool,
|
|
||||||
peer: &mut Stream,
|
|
||||||
) {
|
|
||||||
handle_login_from_ui(
|
|
||||||
self.lc.clone(),
|
|
||||||
os_username,
|
|
||||||
os_password,
|
|
||||||
password,
|
|
||||||
remember,
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream) {
|
|
||||||
handle_test_delay(t, peer).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send(&self, data: Data) {
|
|
||||||
self.sender.send(data).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
pub async fn connect_test(id: &str, key: String, token: String) {
|
|
||||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
|
||||||
let handler = Session::new(&id, sender);
|
|
||||||
match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await {
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("Failed to connect {}: {}", &id, err);
|
|
||||||
}
|
|
||||||
Ok((mut stream, direct)) => {
|
|
||||||
log::info!("direct: {}", direct);
|
|
||||||
// rpassword::prompt_password("Input anything to exit").ok();
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res {
|
|
||||||
Err(_) => {
|
|
||||||
log::error!("Timeout");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(Some(Ok(bytes))) => {
|
|
||||||
if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
|
|
||||||
match msg_in.union {
|
|
||||||
Some(message::Union::Hash(hash)) => {
|
|
||||||
log::info!("Got hash");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
pub async fn start_one_port_forward(
|
|
||||||
id: String,
|
|
||||||
port: i32,
|
|
||||||
remote_host: String,
|
|
||||||
remote_port: i32,
|
|
||||||
key: String,
|
|
||||||
token: String,
|
|
||||||
) {
|
|
||||||
crate::common::test_rendezvous_server();
|
|
||||||
crate::common::test_nat_type();
|
|
||||||
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
|
||||||
let handler = Session::new(&id, sender);
|
|
||||||
if let Err(err) = crate::port_forward::listen(
|
|
||||||
handler.id.clone(),
|
|
||||||
handler.password.clone(),
|
|
||||||
port,
|
|
||||||
handler.clone(),
|
|
||||||
receiver,
|
|
||||||
&key,
|
|
||||||
&token,
|
|
||||||
handler.lc.clone(),
|
|
||||||
remote_host,
|
|
||||||
remote_port,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!("Failed to listen on {}: {}", port, err);
|
|
||||||
}
|
|
||||||
log::info!("port forward (:{}) exit", port);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user