mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-21 09:08:35 +08:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e191d11f74 | ||
|
|
fc396d2166 | ||
|
|
2575e14811 | ||
|
|
0b9a6a280e | ||
|
|
343f12b380 | ||
|
|
ce1e4863cb | ||
|
|
f631c1c28d | ||
|
|
8b9a7a3506 | ||
|
|
055b351164 | ||
|
|
2e89a33210 | ||
|
|
965cc6af26 | ||
|
|
16e191f913 | ||
|
|
1d1e79c802 | ||
|
|
9ffe516f54 | ||
|
|
ac20d2fb56 | ||
|
|
2217152216 | ||
|
|
ee288280b3 | ||
|
|
cccdb2f289 | ||
|
|
1ddab27c0e | ||
|
|
451b6dc651 | ||
|
|
86b327ee41 | ||
|
|
6e305d4865 | ||
|
|
77af6c4ce1 | ||
|
|
fa49c72835 | ||
|
|
c150143d86 | ||
|
|
023d46b48c | ||
|
|
356adbcd8c | ||
|
|
33b47dd6e3 | ||
|
|
a548e9c94d | ||
|
|
cefda0dec1 | ||
|
|
8f545491a2 | ||
|
|
263bbfc66f | ||
|
|
a039741e5a | ||
|
|
2a0e8c109b | ||
|
|
9614bf266a | ||
|
|
aa63ebc7e5 | ||
|
|
fbba8f0b34 | ||
|
|
a27fa43081 | ||
|
|
7a5941de98 | ||
|
|
db3ca6a373 | ||
|
|
ce5f0d513f | ||
|
|
05b0f95b79 | ||
|
|
8b24b195a2 | ||
|
|
5fc8e8c428 | ||
|
|
25f917a7b4 | ||
|
|
55005f8129 | ||
|
|
f08cb0412d | ||
|
|
fc2e27bcf0 | ||
|
|
7aa4592669 | ||
|
|
d656ae2956 | ||
|
|
e4f00361f6 | ||
|
|
1b49d49df2 | ||
|
|
80f759c1ed | ||
|
|
da80f3352a | ||
|
|
ec3ba5be8e | ||
|
|
d04756ad70 | ||
|
|
0eba939cd6 | ||
|
|
1f02bc9d3e | ||
|
|
5fa8c25e65 | ||
|
|
c44803f5b0 | ||
|
|
4b066b1fba | ||
|
|
dd004f1a2d | ||
|
|
222dbf12cd | ||
|
|
b5d54debce | ||
|
|
08cdf7134d | ||
|
|
be5037bd03 | ||
|
|
f9915df926 | ||
|
|
f96c759cf5 | ||
|
|
8f329ebc1a | ||
|
|
4a3c11e711 | ||
|
|
0dbd3094ec | ||
|
|
40999c3211 | ||
|
|
7c2d62237f | ||
|
|
ef90ab2bd4 | ||
|
|
4f3b821883 | ||
|
|
98b00cdb3d | ||
|
|
8e4127b6a0 | ||
|
|
b1f54acf90 | ||
|
|
39a430f96f | ||
|
|
a9f2e14091 | ||
|
|
77baba3122 | ||
|
|
1c62a28ef3 | ||
|
|
9ed2499666 | ||
|
|
06bc554216 | ||
|
|
090f5b65ac | ||
|
|
49dabd3533 | ||
|
|
7289dbc80f | ||
|
|
72f5184ee0 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
target-branch: "master"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: "Git submodule"
|
||||
labels:
|
||||
- "dependencies"
|
||||
4
.github/workflows/bridge.yml
vendored
4
.github/workflows/bridge.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
CARGO_EXPAND_VERSION: "1.0.95"
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||
@@ -25,6 +26,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
@@ -73,6 +76,7 @@ jobs:
|
||||
- name: Install flutter rust bridge deps
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked
|
||||
cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked
|
||||
pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd
|
||||
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -45,6 +45,8 @@ jobs:
|
||||
# steps:
|
||||
# - name: Checkout source code
|
||||
# uses: actions/checkout@v3
|
||||
# with:
|
||||
# submodules: recursive
|
||||
|
||||
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
|
||||
# uses: actions-rs/toolchain@v1
|
||||
@@ -92,6 +94,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install prerequisites
|
||||
shell: bash
|
||||
|
||||
88
.github/workflows/flutter-build.yml
vendored
88
.github/workflows/flutter-build.yml
vendored
@@ -31,17 +31,13 @@ env:
|
||||
FLUTTER_ELINUX_VERSION: "3.16.9"
|
||||
TAG_NAME: "${{ inputs.upload-tag }}"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.11.16
|
||||
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
|
||||
VERSION: "1.3.6"
|
||||
# vcpkg version: 2025.01.13
|
||||
VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836"
|
||||
VERSION: "1.3.8"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||
# To make a custom build with your own servers set the below secret values
|
||||
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
||||
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
||||
API_SERVER: "${{ secrets.API_SERVER }}"
|
||||
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||
|
||||
@@ -87,6 +83,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
@@ -276,6 +274,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install LLVM and Clang
|
||||
uses: rustdesk-org/install-llvm-action-32bit@master
|
||||
@@ -404,6 +404,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
@@ -413,7 +415,7 @@ jobs:
|
||||
|
||||
- name: Build rustdesk
|
||||
run: |
|
||||
./build.py --flutter --hwcodec
|
||||
./build.py --flutter --hwcodec --unix-file-copy-paste
|
||||
|
||||
- name: create unsigned dmg
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
@@ -489,6 +491,9 @@ jobs:
|
||||
brew install nasm yasm
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -546,6 +551,12 @@ jobs:
|
||||
run: |
|
||||
rustup target add ${{ matrix.job.target }}
|
||||
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
|
||||
|
||||
- name: Upload liblibrustdesk.a Artifacts
|
||||
uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: liblibrustdesk.a
|
||||
path: target/aarch64-apple-ios/release/liblibrustdesk.a
|
||||
|
||||
- name: Build rustdesk
|
||||
shell: bash
|
||||
@@ -588,6 +599,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed"
|
||||
|
||||
@@ -660,6 +673,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Import the codesign cert
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
@@ -781,7 +796,7 @@ jobs:
|
||||
sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
|
||||
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
|
||||
fi
|
||||
./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }}
|
||||
./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }}
|
||||
|
||||
- name: create unsigned dmg
|
||||
if: env.UPLOAD_ARTIFACT == 'true'
|
||||
@@ -952,6 +967,9 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -1021,16 +1039,6 @@ jobs:
|
||||
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
|
||||
key: ${{ matrix.job.target }}
|
||||
|
||||
- name: fix android for flutter 3.13
|
||||
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
run: |
|
||||
cd flutter
|
||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
|
||||
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
|
||||
flutter pub get
|
||||
cd lib
|
||||
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
||||
|
||||
- name: Build rustdesk lib
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
@@ -1228,6 +1236,9 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -1270,16 +1281,6 @@ jobs:
|
||||
name: librustdesk.so.i686-linux-android
|
||||
path: ./flutter/android/app/src/main/jniLibs/x86
|
||||
|
||||
- name: fix android for flutter 3.13
|
||||
if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }}
|
||||
run: |
|
||||
cd flutter
|
||||
sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml
|
||||
sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml
|
||||
flutter pub get
|
||||
cd lib
|
||||
find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g'
|
||||
|
||||
- name: Build rustdesk
|
||||
shell: bash
|
||||
env:
|
||||
@@ -1373,7 +1374,7 @@ jobs:
|
||||
arch: aarch64,
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
distro: ubuntu18.04,
|
||||
on: [self-hosted, Linux, ARM64],
|
||||
on: ubuntu-22.04-arm,
|
||||
deb_arch: arm64,
|
||||
vcpkg-triplet: arm64-linux,
|
||||
}
|
||||
@@ -1386,16 +1387,20 @@ jobs:
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Maximize build space
|
||||
if: ${{ matrix.job.arch == 'x86_64' }}
|
||||
run: |
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y nasm qemu-user-static
|
||||
sudo apt-get install -y nasm
|
||||
if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then
|
||||
sudo apt-get install -y qemu-user-static
|
||||
fi
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set Swap Space
|
||||
if: ${{ matrix.job.arch == 'x86_64' }}
|
||||
@@ -1549,7 +1554,7 @@ jobs:
|
||||
export JOBS=""
|
||||
fi
|
||||
echo $JOBS
|
||||
cargo build --lib $JOBS --features hwcodec,flutter --release
|
||||
cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
|
||||
rm -rf target/release/deps target/release/build
|
||||
rm -rf ~/.cargo
|
||||
|
||||
@@ -1686,7 +1691,6 @@ jobs:
|
||||
|
||||
build-rustdesk-linux-sciter:
|
||||
if: ${{ inputs.upload-artifact }}
|
||||
needs: build-rustdesk-linux # not for dep, just make it run later for parallelism
|
||||
runs-on: ${{ matrix.job.on }}
|
||||
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
|
||||
strategy:
|
||||
@@ -1702,17 +1706,17 @@ jobs:
|
||||
deb_arch: amd64,
|
||||
sciter_arch: x64,
|
||||
vcpkg-triplet: x64-linux,
|
||||
extra_features: ",hwcodec",
|
||||
extra_features: ",hwcodec,unix-file-copy-paste",
|
||||
}
|
||||
- {
|
||||
arch: armv7,
|
||||
target: armv7-unknown-linux-gnueabihf,
|
||||
on: [self-hosted, Linux, ARM64],
|
||||
on: ubuntu-22.04-arm,
|
||||
distro: ubuntu18.04-rustdesk,
|
||||
deb_arch: armhf,
|
||||
sciter_arch: arm32,
|
||||
vcpkg-triplet: arm-linux,
|
||||
extra_features: "",
|
||||
extra_features: ",unix-file-copy-paste",
|
||||
}
|
||||
steps:
|
||||
- name: Export GitHub Actions cache environment variables
|
||||
@@ -1724,6 +1728,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
@@ -1914,6 +1920,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download Binary
|
||||
uses: actions/download-artifact@master
|
||||
@@ -1979,13 +1987,15 @@ jobs:
|
||||
target: aarch64-unknown-linux-gnu,
|
||||
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
|
||||
distro: ubuntu22.04,
|
||||
on: [self-hosted, Linux, ARM64],
|
||||
on: ubuntu-22.04-arm,
|
||||
arch: aarch64,
|
||||
suffix: "",
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download Binary
|
||||
uses: actions/download-artifact@master
|
||||
@@ -2043,6 +2053,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Prepare env
|
||||
run: |
|
||||
|
||||
6
.github/workflows/playground.yml
vendored
6
.github/workflows/playground.yml
vendored
@@ -18,7 +18,7 @@ env:
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
# vcpkg version: 2024.11.16
|
||||
VCPKG_COMMIT_ID: "b2cb0da531c2f1f740045bfe7c4dac59f0b2b69c"
|
||||
VERSION: "1.3.6"
|
||||
VERSION: "1.3.8"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -90,7 +90,8 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ matrix.job.ref }}
|
||||
|
||||
submodules: recursive
|
||||
|
||||
- name: Import the codesign cert
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
@@ -250,6 +251,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ matrix.job.ref }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "libs/hbb_common"]
|
||||
path = libs/hbb_common
|
||||
url = https://github.com/rustdesk/hbb_common
|
||||
110
Cargo.lock
generated
110
Cargo.lock
generated
@@ -224,7 +224,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.4.0"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60"
|
||||
source = "git+https://github.com/rustdesk-org/arboard#4e16bad260ea05dd7dcdb68cc7549dad3920b940"
|
||||
dependencies = [
|
||||
"clipboard-win",
|
||||
"core-graphics 0.23.2",
|
||||
@@ -234,6 +234,7 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde 1.0.203",
|
||||
"serde_derive",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -723,9 +724,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.16.1"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
|
||||
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -735,9 +736,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
dependencies = [
|
||||
"serde 1.0.203",
|
||||
]
|
||||
@@ -1581,6 +1582,16 @@ dependencies = [
|
||||
"windows 0.32.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "default_net"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/rustdesk-org/default_net#78f8f70cd85151a3a2c4a3230d80d5272703c02e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"regex",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@@ -1697,7 +1708,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2221,17 +2232,17 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "fuser"
|
||||
version = "0.13.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b"
|
||||
checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix 0.29.0",
|
||||
"page_size",
|
||||
"pkg-config",
|
||||
"smallvec",
|
||||
"zerocopy 0.6.6",
|
||||
"zerocopy 0.8.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2251,9 +2262,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -2261,9 +2272,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
@@ -2278,9 +2289,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -2312,9 +2323,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
@@ -2323,21 +2334,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -2901,6 +2912,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"confy",
|
||||
"default_net",
|
||||
"directories-next",
|
||||
"dirs-next",
|
||||
"dlopen",
|
||||
@@ -2925,6 +2937,7 @@ dependencies = [
|
||||
"serde 1.0.203",
|
||||
"serde_derive",
|
||||
"serde_json 1.0.118",
|
||||
"sha2",
|
||||
"socket2 0.3.19",
|
||||
"sodiumoxide",
|
||||
"sysinfo",
|
||||
@@ -3065,7 +3078,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
[[package]]
|
||||
name = "hwcodec"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#3e7c0dc755f8a77bbed3b2a9921553a511fd7bb5"
|
||||
source = "git+https://github.com/rustdesk-org/hwcodec#0ea7e709d3c48bb6446e33a9cc8fd0e0da5709b9"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -4163,7 +4176,7 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate 2.0.2",
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
"syn 2.0.68",
|
||||
@@ -4382,9 +4395,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.64"
|
||||
version = "0.10.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if 1.0.0",
|
||||
@@ -4414,9 +4427,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.102"
|
||||
version = "0.9.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
||||
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -4496,9 +4509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
@@ -5494,7 +5507,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.3.6"
|
||||
version = "1.3.8"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -5594,7 +5607,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.6"
|
||||
version = "1.3.8"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
@@ -8062,16 +8075,6 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.34"
|
||||
@@ -8082,10 +8085,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.6.6"
|
||||
name = "zerocopy"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
|
||||
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.8.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
@@ -8094,9 +8106,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.34"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.36",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.3.6"
|
||||
version = "1.3.8"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
@@ -16,6 +16,10 @@ crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
name = "naming"
|
||||
path = "src/naming.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "service"
|
||||
path = "src/service.rs"
|
||||
|
||||
[features]
|
||||
inline = []
|
||||
cli = []
|
||||
@@ -177,7 +181,7 @@ members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "lib
|
||||
exclude = ["vdi/host", "examples/custom_plugin"]
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved."
|
||||
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
|
||||
ProductName = "RustDesk"
|
||||
FileDescription = "RustDesk Remote Desktop"
|
||||
OriginalFilename = "rustdesk.exe"
|
||||
|
||||
11
README.md
11
README.md
@@ -1,11 +1,10 @@
|
||||
<p align="center">
|
||||
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#public-servers">Servers</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Structure</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a>]<br>
|
||||
<b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
|
||||
</p>
|
||||
|
||||
@@ -128,6 +127,7 @@ Begin by cloning the repository and building the Docker container:
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
git submodule update --init --recursive
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
@@ -165,6 +165,10 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
|
||||
|
||||
> [!Caution]
|
||||
> **Misuse Disclaimer:** <br>
|
||||
> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
@@ -175,6 +179,3 @@ Please ensure that you are running these commands from the root of the RustDesk
|
||||
|
||||

|
||||
|
||||
## [Public Servers](#public-servers)
|
||||
|
||||
RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.6
|
||||
version: 1.3.8
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.3.6
|
||||
version: 1.3.8
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
23
build.py
23
build.py
@@ -9,6 +9,7 @@ import shutil
|
||||
import hashlib
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
windows = platform.platform().startswith('Windows')
|
||||
osx = platform.platform().startswith(
|
||||
@@ -354,7 +355,7 @@ def build_flutter_deb(version, features):
|
||||
system2('mkdir -p tmpdeb/DEBIAN')
|
||||
generate_control_file(version)
|
||||
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
|
||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||
md5_file_folder("tmpdeb/")
|
||||
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
|
||||
|
||||
system2('/bin/rm -rf tmpdeb/')
|
||||
@@ -391,7 +392,7 @@ def build_deb_from_folder(version, binary_folder):
|
||||
system2('mkdir -p tmpdeb/DEBIAN')
|
||||
generate_control_file(version)
|
||||
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
|
||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||
md5_file_folder("tmpdeb/")
|
||||
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
|
||||
|
||||
system2('/bin/rm -rf tmpdeb/')
|
||||
@@ -404,12 +405,13 @@ def build_flutter_dmg(version, features):
|
||||
if not skip_cargo:
|
||||
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
|
||||
system2(
|
||||
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
|
||||
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
|
||||
# copy dylib
|
||||
system2(
|
||||
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
|
||||
os.chdir('flutter')
|
||||
system2('flutter build macos --release')
|
||||
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
|
||||
'''
|
||||
system2(
|
||||
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
|
||||
@@ -624,18 +626,21 @@ def main():
|
||||
system2('mkdir -p tmpdeb/usr/share/rustdesk')
|
||||
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
|
||||
system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/')
|
||||
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
|
||||
md5_file('etc/rustdesk/startwm.sh')
|
||||
md5_file('etc/X11/rustdesk/xorg.conf')
|
||||
md5_file('etc/pam.d/rustdesk')
|
||||
md5_file('usr/share/rustdesk/libsciter-gtk.so')
|
||||
md5_file_folder("tmpdeb/")
|
||||
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
|
||||
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
|
||||
|
||||
|
||||
def md5_file(fn):
|
||||
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
|
||||
system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
|
||||
system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
|
||||
|
||||
def md5_file_folder(base_dir):
|
||||
base_path = Path(base_dir)
|
||||
for file in base_path.rglob('*'):
|
||||
if file.is_file() and 'DEBIAN' not in file.parts:
|
||||
relative_path = file.relative_to(base_path)
|
||||
md5_file(str(relative_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
125
docs/CODE_OF_CONDUCT-NO.md
Normal file
125
docs/CODE_OF_CONDUCT-NO.md
Normal file
@@ -0,0 +1,125 @@
|
||||
|
||||
# Atferdskodeks for bidragsyterpaktern
|
||||
|
||||
## Hva Vi Står For
|
||||
|
||||
Vi som medlemer, bidragere, og ledere står for å skape ett hat-fritt felleskap,
|
||||
uansett alder, kroppstørrelse, synlig eller usynlige funksjonsnedsettninger,
|
||||
etnesitet, kjønns karaktertrekk, kjønnsidentitet, kunnskapsnivå, utdanning,
|
||||
sosial-økonomisk status, nasjonalitet, utsende, rase, religion, eller seksual
|
||||
identitet og orientasjon.
|
||||
|
||||
Vi står for åpen, velkommende, mangfold, inklusiv og sunn oppførsel i vårt felleskap.
|
||||
|
||||
## Våre Standarer
|
||||
|
||||
Eksempler på oppførsel som hjelper ett positivt felleskap inkluderer:
|
||||
|
||||
* Vise empati og vennlighet mot andre mennesker
|
||||
* Være respektfull ovenfor ulike meninger, synspunkter og erfaringer
|
||||
* Gi og ta konstruktiv kritikk i beste mening
|
||||
* Akseptere ansvar og unskylde seg for de som er utsatt av våre feil,
|
||||
og lære av disse
|
||||
* Fokusere på det som er best ikke bare for individer, men for felleskapet
|
||||
|
||||
Eksempler på uakseptabel oppførsel inkluderer:
|
||||
|
||||
* Bruk av seksualisert språk eller bilder, og seksual oppmerksomhet.
|
||||
* Troll-ene, fornermende og nedsettende kommentarer, og personlig eller politiske angrep
|
||||
* Offentlig eller privat trakassering
|
||||
* Publisering av andres private informasjon, sånn som bosteds- og epost-addresser,
|
||||
uten deres godskjenning.
|
||||
* Andre rettningslinjer som kan bli sett på som upassende i en profesjonell setting.
|
||||
|
||||
## Håndhevingsansvar
|
||||
|
||||
Felleskapets ledere har ansvar for å klarifisere og håndheve våre standarer av
|
||||
akseptert oppførsel og vill ta rimelige og rettferdige handliger som respons på
|
||||
oppførsel de anser som upassende, truende, fornermende eller skadelig.
|
||||
|
||||
Felleskapets ledere har retten og ansvaret til å fjerne, redigere, eller avslå
|
||||
kommentarer, commits, kode, wiki endringer, issues, og andre birag som ikke
|
||||
samsvarer med disse etiske rettningslinjene, og vill kommunisere grunner for
|
||||
moderatorenes valg når passende.
|
||||
|
||||
## Omfang
|
||||
|
||||
Disse etiske rettningslinjene gjelder innenfor alle platformene til felleskapet, og
|
||||
de gjelder også når ett individ representerer felleskapet på offentlige medier.
|
||||
Eksempler på representasjon av vårt felleskap inkluderer bruke av offisielle e-mail
|
||||
addresser, publisering gjennom en offisiell sosial media bruker, eller oppførsel som en
|
||||
utpekt representant på digitale og fysiske arrangsjemanger.
|
||||
|
||||
## Håndheving
|
||||
|
||||
Hendelser av misbruk, trakasserende eller på noen måte uakseptert oppførsel kann
|
||||
bli raportert til felleskapets ledere med ansvar for håndheving på
|
||||
[info@rustdesk.com](mailto:info@rustdesk.com).
|
||||
All tilbakemelding vill bli sett gjennom og investigert rettferdig så fort som mulig.
|
||||
|
||||
Alle felleskapets ledere er obligert til å respektere privatlivet og sikkerhetet ovenfor
|
||||
den som raporterer en hendelse.
|
||||
|
||||
## Håndhevings Guide
|
||||
|
||||
Felleskapets ledere vill følge disse Rettningslinjene for sammfunspåvirkning med
|
||||
tanke på konsekvenser for en handling de anser i brudd med disse etiske rettningslinjene:
|
||||
|
||||
### 1. Korreksjon
|
||||
|
||||
**Sammfunspåvirkning**: Bruk av upassende språk eller annen oppførsel ansett som
|
||||
uprofesjonelt eller uvelkommen i dette felleskapet.
|
||||
|
||||
**Konsekvens**: En privat, skrevet advarsel fra en leder av felleskapet, som
|
||||
klarifiserer grunnlaget til hvorfor denne oppførselen var upassende. En offentlig
|
||||
unskyldning kan bli forespurt.
|
||||
|
||||
### 2. Advarsel
|
||||
|
||||
**Sammfunspåvirkning**: Ett brudd på en singulær hendelse eller en serie handlinger.
|
||||
|
||||
**Konsekvens**: En advarsel med konsekvenser for kontinuerende oppførsel. Ingen
|
||||
interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
|
||||
de som håndhever disse etiske rettningslinjene, er tillat for en spesifisert tidsperiode.
|
||||
Dette inkluderer å unngå interaksjoner i felleskapets platformer, samt eksterne
|
||||
kanaler, som f.eks sosial media. Brudd av disse vilkårene kan føre til midlertidig
|
||||
eller permanent bannlysning.
|
||||
|
||||
### 3. Midlertidig Bannlysning
|
||||
|
||||
**Sammfunspåvirkning**: Ett særiøst brudd på felleskapets standarer, inkludert
|
||||
vedvarende upassende oppførsel.
|
||||
|
||||
**Konsekvens**: En midlertidig bannlysning fra noen som helst interaksjon eller
|
||||
offentlig kommunikasjon med felleskapet for en spesifisert tidsperiode. Ingen
|
||||
interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med
|
||||
de som håndhever disse etiske rettningslinjene, er tillat for denne perioden.
|
||||
Brudd på disse vilkårene kan føre til permanent bannlysning.
|
||||
|
||||
### 4. Permanent Bannlysning
|
||||
|
||||
**Sammfunspåvirkning**: Demonstasjon av mønster i brudd på felleskapets standarer,
|
||||
inklusivt vedvarende upassende oppførsel, trakassering av ett individ, eller
|
||||
aggresjon mot eller nedsettelse av grupper individer.
|
||||
|
||||
**Konsekvens**: En permanent bannlysning fra alle offentlige interaksjoner i
|
||||
felleskapet
|
||||
|
||||
## Attribusjon
|
||||
|
||||
Disse etiske rettningslinjene er adaptert fra [Contributor Covenant][homepage],
|
||||
versjon 2.0, tilgjengelig ved
|
||||
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||
|
||||
Sammfunspåvirknings guid inspirert av
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For svar til vanlige spørsmål angående disse etiske rettningslinjene, se FAQ på
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Oversettelse tilgjengelig
|
||||
ved [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
46
docs/CONTRIBUTING-NO.md
Normal file
46
docs/CONTRIBUTING-NO.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Bidrag til RustDesk
|
||||
|
||||
RustDesk er åpene for bidrag fra alle. Her er reglene for de som har lyst til å
|
||||
hjelpe oss:
|
||||
|
||||
## Bidrag
|
||||
|
||||
Bidrag til RustDesk eller deres avhengigheter burde være i form av GitHub pull requests.
|
||||
Hver pull request vill bli sett igjennom av en kjerne bidrager (noen med autoritet til
|
||||
å godkjenne endringene) og enten bli sendt til main treet eller respondert med
|
||||
tilbakemelding på endringer som er nødvendig. Alle bidrag burde følge dette formate
|
||||
også de fra kjerne bidragere.
|
||||
|
||||
Om du ønsker å jobbe på en issue må du huske å gjøre krav på den først. Dette
|
||||
kann gjøres ved å kommentere på den GitHub issue-en du ønsker å jobbe på.
|
||||
Dette er for å hindre duplikat innsats på samme problem.
|
||||
|
||||
## Pull Request Sjekkliste
|
||||
|
||||
- Lag en gren fra master grenen og, hvis det er nødvendig, rebase den til den nåværende
|
||||
master grenen før du sender inn din pull request. Hvis ikke dette gjøres på rent
|
||||
vis vill du bli spurt om å rebase dine endringer.
|
||||
|
||||
- Commits burde være så små som mulig, samtidig som de må være korrekt uavhenging av hverandre
|
||||
(hver commit burde kompilere og bestå tester).
|
||||
|
||||
- Commits burde være akkopaniert med en Developer Certificate of Origin
|
||||
(http://developercertificate.org), som indikerer att du (og din arbeidsgiver
|
||||
i det tilfellet) godkjenner å bli knyttet til vilkårene av [prosjekt lisensen](../LICENCE).
|
||||
Ved bruk av git er dette `-s` opsjonen til `git commit`.
|
||||
|
||||
- Hvis dine endringer ikke blir sett eller hvis du trenger en spesefik person til
|
||||
å se på dem kan du @-svare en med autoritet til å godkjenne dine endringer.
|
||||
Dette kann gjøres i en pull request, en kommentar eller via epost på [email](mailto:info@rustdesk.com).
|
||||
|
||||
- Legg til tester relevant til en fikset bug eller en ny tilgjengelighet.
|
||||
|
||||
For spesefike git instruksjoner, se [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
|
||||
|
||||
## Oppførsel
|
||||
|
||||
https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
|
||||
|
||||
## Kommunikasjon
|
||||
|
||||
RustDesk bidragere burker [Discord](https://discord.gg/nDceKgxnkV).
|
||||
14
docs/DEVCONTAINER-NO.md
Normal file
14
docs/DEVCONTAINER-NO.md
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
Etter start av devcontainer i docker konteineren, blir en linux binærfil i debug modus laget.
|
||||
|
||||
Nå tilbyr devcontainer linux og android builds i både debug og release modus.
|
||||
|
||||
Under er tabellen over kommandoer som kan kjøres fra rot-direktive for kreasjon av spesefike builds.
|
||||
|
||||
Kommando|Build Type|Modus
|
||||
-|-|-|
|
||||
`.devcontainer/build.sh --debug linux`|Linux|debug
|
||||
`.devcontainer/build.sh --release linux`|Linux|release
|
||||
`.devcontainer/build.sh --debug android`|android-arm64|debug
|
||||
`.devcontainer/build.sh --release android`|android-arm64|release
|
||||
|
||||
@@ -147,6 +147,10 @@ target/release/rustdesk
|
||||
|
||||
Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzen. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenken Sie auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf Ihrem eigentlichen System.
|
||||
|
||||
> [!Vorsicht]
|
||||
> **Haftungsausschluss bei Missbrauch::** <br>
|
||||
> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung.
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen
|
||||
|
||||
@@ -147,6 +147,10 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter
|
||||
|
||||
> [!Precaución]
|
||||
> **Descargo de responsabilidad por uso indebido:** <br>
|
||||
> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El uso indebido, como el acceso no autorizado, el control o la invasión de la privacidad, está estrictamente en contra de nuestras directrices. Los autores no son responsables de ningún uso indebido de la aplicación.
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||

|
||||
|
||||
@@ -137,6 +137,10 @@ Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)** : Communiquer avec [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attendre une connexion distante directe (TCP hole punching) ou relayée.
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme
|
||||
|
||||
> [!Attention]
|
||||
> **Avertissement contre l'utilisation abusive:** <br>
|
||||
> Les développeurs de RustDesk ne cautionnent ni ne soutiennent aucune utilisation non éthique ou illégale de ce logiciel. Toute utilisation abusive, telle que l'accès non autorisé, le contrôle ou l'invasion de la vie privée, est strictement contraire à nos directives. Les auteurs ne sont pas responsables de toute utilisation abusive de l'application.
|
||||
|
||||
## Images
|
||||
|
||||

|
||||
|
||||
@@ -164,6 +164,10 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter
|
||||
|
||||
> [!Attenzione]
|
||||
> **Dichiarazione di non responsabilità per uso improprio:** <br>
|
||||
> Gli sviluppatori di RustDesk non approvano né supportano alcun uso non etico o illegale di questo software. L'uso improprio, come l'accesso non autorizzato, il controllo o l'invasione della privacy, è strettamente contro le nostre linee guida. Gli autori non sono responsabili per qualsiasi uso improprio dell'applicazione.
|
||||
|
||||
## Schermate
|
||||
|
||||

|
||||
|
||||
@@ -168,6 +168,10 @@ target/release/rustdesk
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript
|
||||
|
||||
> [!注意]
|
||||
> **:不正使用に関する免責事項** <br>
|
||||
> RustDeskの開発者は、このソフトウェアの非倫理的または違法な使用を容認または支持しません。不正アクセス、不正な制御、またはプライバシーの侵害などの不正使用は、当社のガイドラインに厳密に違反します。開発者は、アプリケーションの不正使用に対して一切の責任を負いません。
|
||||
|
||||
## スクリーンショット
|
||||
|
||||

|
||||
|
||||
@@ -148,6 +148,10 @@ target/release/rustdesk
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트
|
||||
|
||||
> [!주의]
|
||||
> **오용에 대한 면책 조항:** <br>
|
||||
> RustDesk의 개발자들은 이 소프트웨어의 비윤리적이거나 불법적인 사용을 용인하거나 지원하지 않습니다. 무단 접근, 제어 또는 개인정보 침해와 같은 오용은 우리의 지침을 엄격히 위반하는 것입니다. 개발자들은 애플리케이션의 오용에 대해 책임을 지지 않습니다.
|
||||
|
||||
## 스냅샷
|
||||
|
||||

|
||||
|
||||
177
docs/README-NO.md
Normal file
177
docs/README-NO.md
Normal file
@@ -0,0 +1,177 @@
|
||||
<p align="center">
|
||||
<img src="res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||
<a href="#public-servers">Servere</a> •
|
||||
<a href="#raw-steps-to-build">Build</a> •
|
||||
<a href="#how-to-build-with-docker">Docker</a> •
|
||||
<a href="#file-structure">Struktur</a> •
|
||||
<a href="#snapshot">Snapshot</a><br>
|
||||
[<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>] | [<a href="docs/README-NO.md">Norsk</a><br>
|
||||
<b>Vi trenger din hjelp til å oversette denne README-en, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> og <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> tid ditt morsmål</b>
|
||||
</p>
|
||||
|
||||
Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||
|
||||
[](https://ko-fi.com/I2I04VU09)
|
||||
|
||||
Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo).
|
||||
|
||||

|
||||
|
||||
RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](docs/CONTRIBUTING-NO.md) for hjelp med oppstart.
|
||||
|
||||
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||
|
||||
[**BINARY NEDLASTING**](https://github.com/rustdesk/rustdesk/releases)
|
||||
|
||||
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Få det på 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="Få det på Flathub"
|
||||
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
|
||||
|
||||
## Avhengigheter
|
||||
|
||||
Desktop versjoner bruker Flutter eller Sciter (avviklet) for GUI, denne veiledningen er bare for Sciter, grunnet att det er letter og en mer venlig start. Skjekk ut vår [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for bygging av Flutter versjonen.
|
||||
|
||||
Venligst last ned Sciters dynamiske bibliotek selv.
|
||||
|
||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
||||
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||
|
||||
## Rå steg for bygging
|
||||
|
||||
- Klargjør ditt Rust development env og C++ build env
|
||||
|
||||
- Installer [vcpkg](https://github.com/microsoft/vcpkg), og koriger `VCPKG_ROOT` env vaiabelen
|
||||
|
||||
- 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
|
||||
|
||||
- Kjør `cargo run`
|
||||
|
||||
## [Bygg](https://rustdesk.com/docs/en/dev/build/)
|
||||
|
||||
## Hvordan Bygge til Linux
|
||||
|
||||
### Ubuntu 18 (Debian 10)
|
||||
|
||||
```sh
|
||||
sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
|
||||
libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
|
||||
libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev
|
||||
```
|
||||
|
||||
### openSUSE Tumbleweed
|
||||
|
||||
```sh
|
||||
sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel
|
||||
```
|
||||
|
||||
### Fedora 28 (CentOS 8)
|
||||
|
||||
```sh
|
||||
sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel
|
||||
```
|
||||
|
||||
### Arch (Manjaro)
|
||||
|
||||
```sh
|
||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||
```
|
||||
|
||||
### Installer vcpkg
|
||||
|
||||
```sh
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
git checkout 2023.04.15
|
||||
cd ..
|
||||
vcpkg/bootstrap-vcpkg.sh
|
||||
export VCPKG_ROOT=$HOME/vcpkg
|
||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||
```
|
||||
|
||||
### Fiks libvpx (For Fedora)
|
||||
|
||||
```sh
|
||||
cd vcpkg/buildtrees/libvpx/src
|
||||
cd *
|
||||
./configure
|
||||
sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
|
||||
sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
|
||||
make
|
||||
cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
||||
cd
|
||||
```
|
||||
|
||||
### Bygg
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
mkdir -p target/debug
|
||||
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
|
||||
mv libsciter-gtk.so target/debug
|
||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||
```
|
||||
|
||||
## Hvordan bygge med Docker
|
||||
|
||||
Start med å klone repositoret og bygg Docker konteineren:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/rustdesk/rustdesk
|
||||
cd rustdesk
|
||||
docker build -t "rustdesk-builder" .
|
||||
```
|
||||
|
||||
Deretter, hver gang du trenger å bygge applikasjonen, kjør følgene kommando:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Det kan ta lengere tid før avhengighetene blir bufret første gang du bygger, senere bygg er raskere. Hvis du trenger å spesifisere forkjellige argumenter til bygge kommandoen, kan du gjøre det på slutten av kommandoen ved `<OPTIONAL-ARGS>` feltet. For eksempel, hvis du ville bygge en optimalisert release versjon, ville du kjørt kommandoen over fulgt `--release`. Den kjørbare filen vill være tilgjengelig i mål direktive på ditt system, og kan bli kjørt med:
|
||||
|
||||
```sh
|
||||
target/debug/rustdesk
|
||||
```
|
||||
|
||||
Eller, hvis du kjører ett release program:
|
||||
|
||||
```sh
|
||||
target/release/rustdesk
|
||||
```
|
||||
|
||||
Venligst pass på att du kjører disse kommandoene fra roten av RustDesk repositoret, eller kan det hende att applikasjon ikke finner de riktige ressursene. Pass også på att andre cargo subkommandoer som for eksempel `install` eller `run` ikke støttes med denne metoden da de vill installere eller kjøre programmet i konteineren istedet for verten.
|
||||
|
||||
## Fil Struktur
|
||||
|
||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodek, configurasjon, tcp/udp innpakning, protobuf, fs funksjon for fil overføring, og noen andre verktøy funksjoner
|
||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: skjermfangst
|
||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform spesefik keyboard/mus kontroll
|
||||
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: fil kopi og innliming implementasjon for Windows, Linux, macOS.
|
||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: foreldret Sciter UI (avviklet)
|
||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/utklippstavle/input/video tjenester, og internett tilkobling
|
||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start en peer tilkobling
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient
|
||||
|
||||
## Skjermbilder
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
@@ -165,6 +165,3 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru
|
||||
|
||||

|
||||
|
||||
## [Serwery publiczne](#public-servers)
|
||||
|
||||
RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -137,6 +137,10 @@ Por favor verifique que está executando estes comandos da raiz do repositório
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed)
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma
|
||||
|
||||
> [!Cuidadob]
|
||||
> **Aviso de uso indevido:** <br>
|
||||
> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||
@@ -148,6 +148,10 @@ target/release/rustdesk
|
||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: свяжитесь с [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дождитесь удаленного прямого (обход TCP NAT) или ретранслируемого соединения
|
||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
|
||||
|
||||
> [!Осторожно]
|
||||
> **Отказ от ответственности за неправомерное использование:** <br>
|
||||
> Разработчики RustDesk не одобряют и не поддерживают какое-либо неэтичное или незаконное использование данного программного обеспечения. Неправомерное использование, такое как несанкционированный доступ, контроль или вторжение в частную жизнь, строго противоречит нашим правилам. Авторы не несут ответственности за любое неправомерное использование приложения.
|
||||
|
||||
## Скриншоты
|
||||
|
||||

|
||||
|
||||
@@ -166,6 +166,10 @@ Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan e
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
|
||||
|
||||
> [!Dikkat]
|
||||
> **Yanlış Kullanım Uyarısı:** <br>
|
||||
> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir.
|
||||
|
||||
## Ekran Görüntüleri
|
||||
|
||||

|
||||
|
||||
@@ -172,6 +172,3 @@ target/release/rustdesk
|
||||
|
||||

|
||||
|
||||
## [Публічні сервери](#публічні-сервери)
|
||||
|
||||
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -218,6 +218,10 @@ target/release/rustdesk
|
||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 适用于桌面和移动设备的 Flutter 代码
|
||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码
|
||||
|
||||
> [!警告]
|
||||
> **免责声明:** <br>
|
||||
> RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。
|
||||
|
||||
## 截图
|
||||
|
||||

|
||||
|
||||
9
docs/SECURITY-NO.md
Normal file
9
docs/SECURITY-NO.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Sikkerhets Rettningslinjer
|
||||
|
||||
## Reportering av en Sårbarhet
|
||||
|
||||
Vi verdsetter pris på sikkerhet for prosjektet høyt. Og oppmunterer alle brukere til å rapportere sårbarheter de oppdager til oss.
|
||||
Om du finner en sikkerhets sårbarhet i RustDesk prosjektet, venligst raportere det ansvarsfult ved å sende oss en email til info@rustdesk.com.
|
||||
|
||||
På dette tidspunktet har vi ingen bug dusør program. Vi er ett lite team som prøver å løse ett stort problem. Vi trenger att du raporterer alle sårbarhetene
|
||||
annsvarfult så vi kan fortsettte å bygge ett en sikker applikasjon for hele felleskapet.
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
name = "custom_plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "custom_plugin"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["flutter"]
|
||||
flutter = []
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
rustdesk = { path = "../../", version = "1.2.0", features = ["flutter"]}
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
#opt-level = 'z' # only have smaller size after strip
|
||||
rpath = true
|
||||
@@ -1,30 +0,0 @@
|
||||
use librustdesk::api::RustDeskApiTable;
|
||||
/// This file demonstrates how to write a custom plugin for RustDesk.
|
||||
use std::ffi::{c_char, c_int, CString};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref PLUGIN_NAME: CString = CString::new("A Template Rust Plugin").unwrap();
|
||||
pub static ref PLUGIN_ID: CString = CString::new("TemplatePlugin").unwrap();
|
||||
// Do your own logic based on the API provided by RustDesk.
|
||||
pub static ref API: RustDeskApiTable = RustDeskApiTable::default();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_name() -> *const c_char {
|
||||
return PLUGIN_NAME.as_ptr();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_id() -> *const c_char {
|
||||
return PLUGIN_ID.as_ptr();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_init() -> c_int {
|
||||
return 0 as _;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn plugin_dispose() -> c_int {
|
||||
return 0 as _;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.KeyEvent as KeyEventAndroid
|
||||
import android.view.ViewConfiguration
|
||||
import android.graphics.Rect
|
||||
import android.media.AudioManager
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
@@ -34,10 +35,15 @@ import hbb.MessageOuterClass.KeyEvent
|
||||
import hbb.MessageOuterClass.KeyboardMode
|
||||
import hbb.KeyEventConverter
|
||||
|
||||
const val LIFT_DOWN = 9
|
||||
const val LIFT_MOVE = 8
|
||||
const val LIFT_UP = 10
|
||||
// const val BUTTON_UP = 2
|
||||
// const val BUTTON_BACK = 0x08
|
||||
|
||||
const val LEFT_DOWN = 9
|
||||
const val LEFT_MOVE = 8
|
||||
const val LEFT_UP = 10
|
||||
const val RIGHT_UP = 18
|
||||
// (BUTTON_BACK << 3) | BUTTON_UP
|
||||
const val BACK_UP = 66
|
||||
const val WHEEL_BUTTON_DOWN = 33
|
||||
const val WHEEL_BUTTON_UP = 34
|
||||
const val WHEEL_DOWN = 523331
|
||||
@@ -64,12 +70,15 @@ class InputService : AccessibilityService() {
|
||||
|
||||
private val logTag = "input service"
|
||||
private var leftIsDown = false
|
||||
private var touchPath = Path()
|
||||
private val touchPath = Path()
|
||||
private var stroke: GestureDescription.StrokeDescription? = null
|
||||
private var lastTouchGestureStartTime = 0L
|
||||
private var mouseX = 0
|
||||
private var mouseY = 0
|
||||
private var timer = Timer()
|
||||
private var recentActionTask: TimerTask? = null
|
||||
// 100(tap timeout) + 400(long press timeout)
|
||||
private val longPressDuration = ViewConfiguration.getTapTimeout().toLong() + ViewConfiguration.getLongPressTimeout().toLong()
|
||||
|
||||
private val wheelActionsQueue = LinkedList<GestureDescription>()
|
||||
private var isWheelActionsPolling = false
|
||||
@@ -77,6 +86,9 @@ class InputService : AccessibilityService() {
|
||||
|
||||
private var fakeEditTextForTextStateCalculation: EditText? = null
|
||||
|
||||
private var lastX = 0
|
||||
private var lastY = 0
|
||||
|
||||
private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@@ -84,7 +96,7 @@ class InputService : AccessibilityService() {
|
||||
val x = max(0, _x)
|
||||
val y = max(0, _y)
|
||||
|
||||
if (mask == 0 || mask == LIFT_MOVE) {
|
||||
if (mask == 0 || mask == LEFT_MOVE) {
|
||||
val oldX = mouseX
|
||||
val oldY = mouseY
|
||||
mouseX = x * SCREEN_INFO.scale
|
||||
@@ -98,31 +110,30 @@ class InputService : AccessibilityService() {
|
||||
}
|
||||
}
|
||||
|
||||
// left button down ,was up
|
||||
if (mask == LIFT_DOWN) {
|
||||
// left button down, was up
|
||||
if (mask == LEFT_DOWN) {
|
||||
isWaitingLongPress = true
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
if (isWaitingLongPress) {
|
||||
isWaitingLongPress = false
|
||||
leftIsDown = false
|
||||
endGesture(mouseX, mouseY)
|
||||
continueGesture(mouseX, mouseY)
|
||||
}
|
||||
}
|
||||
}, LONG_TAP_DELAY * 4)
|
||||
}, longPressDuration)
|
||||
|
||||
leftIsDown = true
|
||||
startGesture(mouseX, mouseY)
|
||||
return
|
||||
}
|
||||
|
||||
// left down ,was down
|
||||
// left down, was down
|
||||
if (leftIsDown) {
|
||||
continueGesture(mouseX, mouseY)
|
||||
}
|
||||
|
||||
// left up ,was down
|
||||
if (mask == LIFT_UP) {
|
||||
// left up, was down
|
||||
if (mask == LEFT_UP) {
|
||||
if (leftIsDown) {
|
||||
leftIsDown = false
|
||||
isWaitingLongPress = false
|
||||
@@ -132,6 +143,11 @@ class InputService : AccessibilityService() {
|
||||
}
|
||||
|
||||
if (mask == RIGHT_UP) {
|
||||
longPress(mouseX, mouseY)
|
||||
return
|
||||
}
|
||||
|
||||
if (mask == BACK_UP) {
|
||||
performGlobalAction(GLOBAL_ACTION_BACK)
|
||||
return
|
||||
}
|
||||
@@ -241,36 +257,78 @@ class InputService : AccessibilityService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startGesture(x: Int, y: Int) {
|
||||
touchPath = Path()
|
||||
touchPath.moveTo(x.toFloat(), y.toFloat())
|
||||
lastTouchGestureStartTime = System.currentTimeMillis()
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun performClick(x: Int, y: Int, duration: Long) {
|
||||
val path = Path()
|
||||
path.moveTo(x.toFloat(), y.toFloat())
|
||||
try {
|
||||
val longPressStroke = GestureDescription.StrokeDescription(path, 0, duration)
|
||||
val builder = GestureDescription.Builder()
|
||||
builder.addStroke(longPressStroke)
|
||||
Log.d(logTag, "performClick x:$x y:$y time:$duration")
|
||||
dispatchGesture(builder.build(), null, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(logTag, "performClick, error:$e")
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueGesture(x: Int, y: Int) {
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun longPress(x: Int, y: Int) {
|
||||
performClick(x, y, longPressDuration)
|
||||
}
|
||||
|
||||
private fun startGesture(x: Int, y: Int) {
|
||||
touchPath.reset()
|
||||
touchPath.moveTo(x.toFloat(), y.toFloat())
|
||||
lastTouchGestureStartTime = System.currentTimeMillis()
|
||||
lastX = x
|
||||
lastY = y
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun doDispatchGesture(x: Int, y: Int, willContinue: Boolean) {
|
||||
touchPath.lineTo(x.toFloat(), y.toFloat())
|
||||
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
|
||||
if (duration <= 0) {
|
||||
duration = 1
|
||||
}
|
||||
try {
|
||||
if (stroke == null) {
|
||||
stroke = GestureDescription.StrokeDescription(
|
||||
touchPath,
|
||||
0,
|
||||
duration,
|
||||
willContinue
|
||||
)
|
||||
} else {
|
||||
stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue)
|
||||
}
|
||||
stroke?.let {
|
||||
val builder = GestureDescription.Builder()
|
||||
builder.addStroke(it)
|
||||
Log.d(logTag, "doDispatchGesture x:$x y:$y time:$duration")
|
||||
dispatchGesture(builder.build(), null, null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(logTag, "doDispatchGesture, willContinue:$willContinue, error:$e")
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun continueGesture(x: Int, y: Int) {
|
||||
doDispatchGesture(x, y, true)
|
||||
touchPath.reset()
|
||||
touchPath.moveTo(x.toFloat(), y.toFloat())
|
||||
lastTouchGestureStartTime = System.currentTimeMillis()
|
||||
lastX = x
|
||||
lastY = y
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private fun endGesture(x: Int, y: Int) {
|
||||
try {
|
||||
touchPath.lineTo(x.toFloat(), y.toFloat())
|
||||
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
|
||||
if (duration <= 0) {
|
||||
duration = 1
|
||||
}
|
||||
val stroke = GestureDescription.StrokeDescription(
|
||||
touchPath,
|
||||
0,
|
||||
duration
|
||||
)
|
||||
val builder = GestureDescription.Builder()
|
||||
builder.addStroke(stroke)
|
||||
Log.d(logTag, "end gesture x:$x y:$y time:$duration")
|
||||
dispatchGesture(builder.build(), null, null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(logTag, "endGesture error:$e")
|
||||
}
|
||||
doDispatchGesture(x, y, false)
|
||||
touchPath.reset()
|
||||
stroke = null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
|
||||
@@ -65,8 +65,8 @@ class MainService : Service() {
|
||||
@Keep
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) {
|
||||
// turn on screen with LIFT_DOWN when screen off
|
||||
if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) {
|
||||
// turn on screen with LEFT_DOWN when screen off
|
||||
if (!powerManager.isInteractive && (kind == 0 || mask == LEFT_DOWN)) {
|
||||
if (wakeLock.isHeld) {
|
||||
Log.d(logTag, "Turn on Screen, WakeLock release")
|
||||
wakeLock.release()
|
||||
|
||||
BIN
flutter/assets/device_group.ttf
Normal file
BIN
flutter/assets/device_group.ttf
Normal file
Binary file not shown.
@@ -150,6 +150,10 @@ prebuild)
|
||||
|
||||
# Flutter used to compile Flutter<->Rust bridge files
|
||||
|
||||
CARGO_EXPAND_VERSION="$(yq -r \
|
||||
.env.CARGO_EXPAND_VERSION \
|
||||
.github/workflows/bridge.yml)"
|
||||
|
||||
FLUTTER_BRIDGE_VERSION="$(yq -r \
|
||||
.env.FLUTTER_VERSION \
|
||||
.github/workflows/bridge.yml)"
|
||||
@@ -237,7 +241,10 @@ prebuild)
|
||||
|
||||
# Install rust bridge generator
|
||||
|
||||
cargo install cargo-expand
|
||||
cargo install \
|
||||
cargo-expand \
|
||||
--version "${CARGO_EXPAND_VERSION}" \
|
||||
--locked
|
||||
cargo install flutter_rust_bridge_codegen \
|
||||
--version "${FLUTTER_RUST_BRIDGE_VERSION}" \
|
||||
--features "uuid" \
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
cd build/web/
|
||||
python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);'
|
||||
mv jds/dist/index.js ./
|
||||
mv jds/dist/vendor.js ./
|
||||
/bin/rm -rf js
|
||||
python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)'
|
||||
python3 -c 'import hashlib;x=hashlib.sha1(open("./index.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/index.js", "index.js?v="+x);open("index.html","wt").write(y)'
|
||||
python3 -c 'import hashlib;x=hashlib.sha1(open("./vendor.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/vendor.js", "vendor.js?v="+x);open("index.html","wt").write(y)'
|
||||
tar czf x *
|
||||
scp x sg:/tmp/
|
||||
ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R"
|
||||
/bin/rm x
|
||||
cd -
|
||||
@@ -103,6 +103,8 @@ enum DesktopType {
|
||||
class IconFont {
|
||||
static const _family1 = 'Tabbar';
|
||||
static const _family2 = 'PeerSearchbar';
|
||||
static const _family3 = 'AddressBook';
|
||||
static const _family4 = 'DeviceGroup';
|
||||
IconFont._();
|
||||
|
||||
static const IconData max = IconData(0xe606, fontFamily: _family1);
|
||||
@@ -113,8 +115,11 @@ class IconFont {
|
||||
static const IconData menu = IconData(0xe628, fontFamily: _family1);
|
||||
static const IconData search = IconData(0xe6a4, fontFamily: _family2);
|
||||
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
|
||||
static const IconData addressBook =
|
||||
IconData(0xe602, fontFamily: "AddressBook");
|
||||
static const IconData addressBook = IconData(0xe602, fontFamily: _family3);
|
||||
static const IconData deviceGroupOutline =
|
||||
IconData(0xe623, fontFamily: _family4);
|
||||
static const IconData deviceGroupFill =
|
||||
IconData(0xe748, fontFamily: _family4);
|
||||
}
|
||||
|
||||
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
@@ -817,7 +822,11 @@ class OverlayDialogManager {
|
||||
|
||||
close([res]) {
|
||||
_dialogs.remove(dialogTag);
|
||||
dialog.complete(res);
|
||||
try {
|
||||
dialog.complete(res);
|
||||
} catch (e) {
|
||||
debugPrint("Dialog complete catch error: $e");
|
||||
}
|
||||
BackButtonInterceptor.removeByName(dialogTag);
|
||||
}
|
||||
|
||||
@@ -2566,6 +2575,8 @@ bool get kUseCompatibleUiMode =>
|
||||
isWindows &&
|
||||
const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
|
||||
|
||||
bool get isWin10 => windowsBuildNumber.windowsVersion == WindowsTarget.w10;
|
||||
|
||||
class ServerConfig {
|
||||
late String idServer;
|
||||
late String relayServer;
|
||||
@@ -3610,7 +3621,7 @@ void earlyAssert() {
|
||||
}
|
||||
|
||||
void checkUpdate() {
|
||||
if (isDesktop || isAndroid) {
|
||||
if (!isWeb) {
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.registerEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||
@@ -3638,3 +3649,74 @@ extension WorkaroundFreezeLinuxMint on Widget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use `extension` here, the border looks weird if using `extension` in my test.
|
||||
Widget workaroundWindowBorder(BuildContext context, Widget child) {
|
||||
if (!isWin10) {
|
||||
return child;
|
||||
}
|
||||
|
||||
final isLight = Theme.of(context).brightness == Brightness.light;
|
||||
final borderColor = isLight ? Colors.black87 : Colors.grey;
|
||||
final width = isLight ? 0.5 : 0.1;
|
||||
|
||||
getBorderWidget(Widget child) {
|
||||
return Obx(() =>
|
||||
(stateGlobal.isMaximized.isTrue || stateGlobal.fullscreen.isTrue)
|
||||
? Offstage()
|
||||
: child);
|
||||
}
|
||||
|
||||
final List<Widget> borders = [
|
||||
getBorderWidget(Container(
|
||||
color: borderColor,
|
||||
height: width + 0.1,
|
||||
))
|
||||
];
|
||||
if (kWindowType == WindowType.Main && !isLight) {
|
||||
borders.addAll([
|
||||
getBorderWidget(Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
color: borderColor,
|
||||
width: width,
|
||||
),
|
||||
)),
|
||||
getBorderWidget(Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
color: borderColor,
|
||||
width: width,
|
||||
),
|
||||
)),
|
||||
getBorderWidget(Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
color: borderColor,
|
||||
height: width,
|
||||
),
|
||||
)),
|
||||
]);
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
child,
|
||||
...borders,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void updateTextAndPreserveSelection(
|
||||
TextEditingController controller, String text) {
|
||||
// Only care about select all for now.
|
||||
final isSelected = controller.selection.isValid &&
|
||||
controller.selection.end > controller.selection.start;
|
||||
|
||||
// Set text will make the selection invalid.
|
||||
controller.text = text;
|
||||
|
||||
if (isSelected) {
|
||||
controller.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: controller.value.text.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ class PeerPayload {
|
||||
int? status;
|
||||
String user = '';
|
||||
String user_name = '';
|
||||
String? device_group_name;
|
||||
String note = '';
|
||||
|
||||
PeerPayload.fromJson(Map<String, dynamic> json)
|
||||
@@ -75,6 +76,7 @@ class PeerPayload {
|
||||
status = json['status'],
|
||||
user = json['user'] ?? '',
|
||||
user_name = json['user_name'] ?? '',
|
||||
device_group_name = json['device_group_name'] ?? '',
|
||||
note = json['note'] ?? '';
|
||||
|
||||
static Peer toPeer(PeerPayload p) {
|
||||
@@ -84,6 +86,7 @@ class PeerPayload {
|
||||
"username": p.info['username'] ?? '',
|
||||
"platform": _platform(p.info['os']),
|
||||
"hostname": p.info['device_name'],
|
||||
"device_group_name": p.device_group_name,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,3 +268,19 @@ class AbTag {
|
||||
: name = json['name'] ?? '',
|
||||
color = json['color'] ?? '';
|
||||
}
|
||||
|
||||
class DeviceGroupPayload {
|
||||
String name;
|
||||
|
||||
DeviceGroupPayload(this.name);
|
||||
|
||||
DeviceGroupPayload.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'] ?? '';
|
||||
|
||||
Map<String, dynamic> toGroupCacheJson() {
|
||||
final Map<String, dynamic> map = {
|
||||
'name': name,
|
||||
};
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
||||
import '../../../models/platform_model.dart';
|
||||
@@ -6,56 +5,104 @@ import 'package:flutter_hbb/models/peer_model.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||
|
||||
Future<List<Peer>> getAllPeers() async {
|
||||
Map<String, dynamic> recentPeers = jsonDecode(bind.mainLoadRecentPeersSync());
|
||||
Map<String, dynamic> lanPeers = jsonDecode(bind.mainLoadLanPeersSync());
|
||||
Map<String, dynamic> combinedPeers = {};
|
||||
class AllPeersLoader {
|
||||
List<Peer> peers = [];
|
||||
|
||||
void mergePeers(Map<String, dynamic> peers) {
|
||||
if (peers.containsKey("peers")) {
|
||||
dynamic peerData = peers["peers"];
|
||||
bool _isPeersLoading = false;
|
||||
bool _isPeersLoaded = false;
|
||||
|
||||
if (peerData is String) {
|
||||
try {
|
||||
peerData = jsonDecode(peerData);
|
||||
} catch (e) {
|
||||
print("Error decoding peers: $e");
|
||||
return;
|
||||
}
|
||||
}
|
||||
final String _listenerKey = 'AllPeersLoader';
|
||||
|
||||
if (peerData is List) {
|
||||
for (var peer in peerData) {
|
||||
if (peer is Map && peer.containsKey("id")) {
|
||||
String id = peer["id"];
|
||||
if (!combinedPeers.containsKey(id)) {
|
||||
combinedPeers[id] = peer;
|
||||
}
|
||||
}
|
||||
}
|
||||
late void Function(VoidCallback) setState;
|
||||
|
||||
bool get needLoad => !_isPeersLoaded && !_isPeersLoading;
|
||||
bool get isPeersLoaded => _isPeersLoaded;
|
||||
|
||||
AllPeersLoader();
|
||||
|
||||
void init(void Function(VoidCallback) setState) {
|
||||
this.setState = setState;
|
||||
gFFI.recentPeersModel.addListener(_mergeAllPeers);
|
||||
gFFI.lanPeersModel.addListener(_mergeAllPeers);
|
||||
gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
||||
gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
gFFI.recentPeersModel.removeListener(_mergeAllPeers);
|
||||
gFFI.lanPeersModel.removeListener(_mergeAllPeers);
|
||||
gFFI.abModel.removePeerUpdateListener(_listenerKey);
|
||||
gFFI.groupModel.removePeerUpdateListener(_listenerKey);
|
||||
}
|
||||
|
||||
Future<void> getAllPeers() async {
|
||||
if (!needLoad) {
|
||||
return;
|
||||
}
|
||||
_isPeersLoading = true;
|
||||
|
||||
if (gFFI.recentPeersModel.peers.isEmpty) {
|
||||
bind.mainLoadRecentPeers();
|
||||
}
|
||||
if (gFFI.lanPeersModel.peers.isEmpty) {
|
||||
bind.mainLoadLanPeers();
|
||||
}
|
||||
// No need to care about peers from abModel, and group model.
|
||||
// Because they will pull data in `refreshCurrentUser()` on startup.
|
||||
|
||||
final startTime = DateTime.now();
|
||||
_mergeAllPeers();
|
||||
final diffTime = DateTime.now().difference(startTime).inMilliseconds;
|
||||
if (diffTime < 100) {
|
||||
await Future.delayed(Duration(milliseconds: diffTime));
|
||||
}
|
||||
}
|
||||
|
||||
void _mergeAllPeers() {
|
||||
Map<String, dynamic> combinedPeers = {};
|
||||
for (var p in gFFI.abModel.allPeers()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mergePeers(recentPeers);
|
||||
mergePeers(lanPeers);
|
||||
for (var p in gFFI.abModel.allPeers()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) {
|
||||
if (!combinedPeers.containsKey(p.id)) {
|
||||
combinedPeers[p.id] = p.toJson();
|
||||
|
||||
List<Peer> parsedPeers = [];
|
||||
for (var peer in combinedPeers.values) {
|
||||
parsedPeers.add(Peer.fromJson(peer));
|
||||
}
|
||||
}
|
||||
|
||||
List<Peer> parsedPeers = [];
|
||||
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 (var peer in combinedPeers.values) {
|
||||
parsedPeers.add(Peer.fromJson(peer));
|
||||
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;
|
||||
_isPeersLoaded = true;
|
||||
});
|
||||
}
|
||||
return parsedPeers;
|
||||
}
|
||||
|
||||
class AutocompletePeerTile extends StatefulWidget {
|
||||
|
||||
@@ -71,7 +71,7 @@ void changeIdDialog() {
|
||||
final rules = [
|
||||
RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')),
|
||||
LengthRangeValidationRule(6, 16),
|
||||
RegexValidationRule('allowed characters', RegExp(r'^\w*$'))
|
||||
RegexValidationRule('allowed characters', RegExp(r'^[\w-]*$'))
|
||||
];
|
||||
|
||||
gFFI.dialogManager.show((setState, close, context) {
|
||||
|
||||
@@ -20,8 +20,11 @@ class MyGroup extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyGroupState extends State<MyGroup> {
|
||||
RxString get selectedUser => gFFI.groupModel.selectedUser;
|
||||
RxString get searchUserText => gFFI.groupModel.searchUserText;
|
||||
RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup;
|
||||
RxString get selectedAccessibleItemName =>
|
||||
gFFI.groupModel.selectedAccessibleItemName;
|
||||
RxString get searchAccessibleItemNameText =>
|
||||
gFFI.groupModel.searchAccessibleItemNameText;
|
||||
static TextEditingController searchUserController = TextEditingController();
|
||||
|
||||
@override
|
||||
@@ -72,7 +75,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: _buildUserContacts(),
|
||||
child: _buildLeftList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
@@ -105,7 +108,7 @@ class _MyGroupState extends State<MyGroup> {
|
||||
_buildLeftHeader(),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: _buildUserContacts(),
|
||||
child: _buildLeftList(),
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -130,7 +133,8 @@ class _MyGroupState extends State<MyGroup> {
|
||||
child: TextField(
|
||||
controller: searchUserController,
|
||||
onChanged: (value) {
|
||||
searchUserText.value = value;
|
||||
searchAccessibleItemNameText.value = value;
|
||||
selectedAccessibleItemName.value = '';
|
||||
},
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
style: TextStyle(fontSize: fontSize),
|
||||
@@ -150,20 +154,30 @@ class _MyGroupState extends State<MyGroup> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserContacts() {
|
||||
Widget _buildLeftList() {
|
||||
return Obx(() {
|
||||
final items = gFFI.groupModel.users.where((p0) {
|
||||
if (searchUserText.isNotEmpty) {
|
||||
final userItems = gFFI.groupModel.users.where((p0) {
|
||||
if (searchAccessibleItemNameText.isNotEmpty) {
|
||||
return p0.name
|
||||
.toLowerCase()
|
||||
.contains(searchUserText.value.toLowerCase());
|
||||
.contains(searchAccessibleItemNameText.value.toLowerCase());
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) {
|
||||
if (searchAccessibleItemNameText.isNotEmpty) {
|
||||
return p0.name
|
||||
.toLowerCase()
|
||||
.contains(searchAccessibleItemNameText.value.toLowerCase());
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
listView(bool isPortrait) => ListView.builder(
|
||||
shrinkWrap: isPortrait,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => _buildUserItem(items[index]));
|
||||
itemCount: deviceGroupItems.length + userItems.length,
|
||||
itemBuilder: (context, index) => index < deviceGroupItems.length
|
||||
? _buildDeviceGroupItem(deviceGroupItems[index])
|
||||
: _buildUserItem(userItems[index - deviceGroupItems.length]));
|
||||
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
|
||||
return Obx(() => stateGlobal.isPortrait.isFalse
|
||||
? listView(false)
|
||||
@@ -174,14 +188,16 @@ class _MyGroupState extends State<MyGroup> {
|
||||
Widget _buildUserItem(UserPayload user) {
|
||||
final username = user.name;
|
||||
return InkWell(onTap: () {
|
||||
if (selectedUser.value != username) {
|
||||
selectedUser.value = username;
|
||||
isSelectedDeviceGroup.value = false;
|
||||
if (selectedAccessibleItemName.value != username) {
|
||||
selectedAccessibleItemName.value = username;
|
||||
} else {
|
||||
selectedUser.value = '';
|
||||
selectedAccessibleItemName.value = '';
|
||||
}
|
||||
}, child: Obx(
|
||||
() {
|
||||
bool selected = selectedUser.value == username;
|
||||
bool selected = !isSelectedDeviceGroup.value &&
|
||||
selectedAccessibleItemName.value == username;
|
||||
final isMe = username == gFFI.userModel.userName.value;
|
||||
final colorMe = MyTheme.color(context).me!;
|
||||
return Container(
|
||||
@@ -238,4 +254,43 @@ class _MyGroupState extends State<MyGroup> {
|
||||
},
|
||||
)).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
|
||||
}
|
||||
|
||||
Widget _buildDeviceGroupItem(DeviceGroupPayload deviceGroup) {
|
||||
final name = deviceGroup.name;
|
||||
return InkWell(onTap: () {
|
||||
isSelectedDeviceGroup.value = true;
|
||||
if (selectedAccessibleItemName.value != name) {
|
||||
selectedAccessibleItemName.value = name;
|
||||
} else {
|
||||
selectedAccessibleItemName.value = '';
|
||||
}
|
||||
}, child: Obx(
|
||||
() {
|
||||
bool selected = isSelectedDeviceGroup.value &&
|
||||
selectedAccessibleItemName.value == name;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? MyTheme.color(context).highlight : null,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 0.7,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.1))),
|
||||
),
|
||||
child: Container(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Icon(IconFont.deviceGroupOutline,
|
||||
color: MyTheme.accent, size: 19),
|
||||
).marginOnly(right: 4),
|
||||
Expanded(child: Text(name)),
|
||||
],
|
||||
).paddingSymmetric(vertical: 4),
|
||||
),
|
||||
);
|
||||
},
|
||||
)).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,18 +716,18 @@ abstract class BasePeerCard extends StatelessWidget {
|
||||
switch (tab) {
|
||||
case PeerTabIndex.recent:
|
||||
await bind.mainRemovePeer(id: id);
|
||||
await bind.mainLoadRecentPeers();
|
||||
bind.mainLoadRecentPeers();
|
||||
break;
|
||||
case PeerTabIndex.fav:
|
||||
final favs = (await bind.mainGetFav()).toList();
|
||||
if (favs.remove(id)) {
|
||||
await bind.mainStoreFav(favs: favs);
|
||||
await bind.mainLoadFavPeers();
|
||||
bind.mainLoadFavPeers();
|
||||
}
|
||||
break;
|
||||
case PeerTabIndex.lan:
|
||||
await bind.mainRemoveDiscovered(id: id);
|
||||
await bind.mainLoadLanPeers();
|
||||
bind.mainLoadLanPeers();
|
||||
break;
|
||||
case PeerTabIndex.ab:
|
||||
await gFFI.abModel.deletePeers([id]);
|
||||
|
||||
@@ -33,8 +33,8 @@ class PeerTabPage extends StatefulWidget {
|
||||
|
||||
class _TabEntry {
|
||||
final Widget widget;
|
||||
final Function({dynamic hint}) load;
|
||||
_TabEntry(this.widget, this.load);
|
||||
final Function({dynamic hint})? load;
|
||||
_TabEntry(this.widget, [this.load]);
|
||||
}
|
||||
|
||||
EdgeInsets? _menuPadding() {
|
||||
@@ -44,21 +44,15 @@ EdgeInsets? _menuPadding() {
|
||||
class _PeerTabPageState extends State<PeerTabPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final List<_TabEntry> entries = [
|
||||
_TabEntry(
|
||||
RecentPeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
bind.mainLoadRecentPeers),
|
||||
_TabEntry(
|
||||
FavoritePeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
bind.mainLoadFavPeers),
|
||||
_TabEntry(
|
||||
DiscoveredPeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
),
|
||||
bind.mainDiscover),
|
||||
_TabEntry(RecentPeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
)),
|
||||
_TabEntry(FavoritePeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
)),
|
||||
_TabEntry(DiscoveredPeersView(
|
||||
menuPadding: _menuPadding(),
|
||||
)),
|
||||
_TabEntry(
|
||||
AddressBook(
|
||||
menuPadding: _menuPadding(),
|
||||
@@ -100,7 +94,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
gFFI.peerTabModel.setCurrentTabCachedPeers([]);
|
||||
}
|
||||
gFFI.peerTabModel.setCurrentTab(tabIndex);
|
||||
entries[tabIndex].load(hint: false);
|
||||
entries[tabIndex].load?.call(hint: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +219,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
child: RefreshWidget(
|
||||
onPressed: () {
|
||||
if (gFFI.peerTabModel.currentTab < entries.length) {
|
||||
entries[gFFI.peerTabModel.currentTab].load();
|
||||
entries[gFFI.peerTabModel.currentTab].load?.call();
|
||||
}
|
||||
},
|
||||
spinning: loading,
|
||||
@@ -404,7 +398,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
for (var p in peers) {
|
||||
await bind.mainRemovePeer(id: p.id);
|
||||
}
|
||||
await bind.mainLoadRecentPeers();
|
||||
bind.mainLoadRecentPeers();
|
||||
break;
|
||||
case 1:
|
||||
final favs = (await bind.mainGetFav()).toList();
|
||||
@@ -412,13 +406,13 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
favs.remove(p.id);
|
||||
}).toList();
|
||||
await bind.mainStoreFav(favs: favs);
|
||||
await bind.mainLoadFavPeers();
|
||||
bind.mainLoadFavPeers();
|
||||
break;
|
||||
case 2:
|
||||
for (var p in peers) {
|
||||
await bind.mainRemoveDiscovered(id: p.id);
|
||||
}
|
||||
await bind.mainLoadLanPeers();
|
||||
bind.mainLoadLanPeers();
|
||||
break;
|
||||
case 3:
|
||||
await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
|
||||
|
||||
@@ -25,13 +25,13 @@ class PeerSortType {
|
||||
static const String remoteId = 'Remote ID';
|
||||
static const String remoteHost = 'Remote Host';
|
||||
static const String username = 'Username';
|
||||
// static const String status = 'Status';
|
||||
static const String status = 'Status';
|
||||
|
||||
static List<String> values = [
|
||||
PeerSortType.remoteId,
|
||||
PeerSortType.remoteHost,
|
||||
PeerSortType.username,
|
||||
// PeerSortType.status
|
||||
PeerSortType.status
|
||||
];
|
||||
}
|
||||
|
||||
@@ -384,9 +384,9 @@ class _PeersViewState extends State<_PeersView>
|
||||
peers.sort((p1, p2) =>
|
||||
p1.username.toLowerCase().compareTo(p2.username.toLowerCase()));
|
||||
break;
|
||||
// case PeerSortType.status:
|
||||
// peers.sort((p1, p2) => p1.online ? -1 : 1);
|
||||
// break;
|
||||
case PeerSortType.status:
|
||||
peers.sort((p1, p2) => p1.online ? -1 : 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,14 +562,26 @@ class MyGroupPeerView extends BasePeersView {
|
||||
);
|
||||
|
||||
static bool filter(Peer peer) {
|
||||
if (gFFI.groupModel.searchUserText.isNotEmpty) {
|
||||
if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
|
||||
final model = gFFI.groupModel;
|
||||
if (model.searchAccessibleItemNameText.isNotEmpty) {
|
||||
final text = model.searchAccessibleItemNameText.value;
|
||||
final searchPeersOfUser = peer.loginName.contains(text) &&
|
||||
model.users.any((user) => user.name == peer.loginName);
|
||||
final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) &&
|
||||
model.deviceGroups.any((g) => g.name == peer.device_group_name);
|
||||
if (!searchPeersOfUser && !searchPeersOfDeviceGroup) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (gFFI.groupModel.selectedUser.isNotEmpty) {
|
||||
if (gFFI.groupModel.selectedUser.value != peer.loginName) {
|
||||
return false;
|
||||
if (model.selectedAccessibleItemName.isNotEmpty) {
|
||||
if (model.isSelectedDeviceGroup.value) {
|
||||
if (model.selectedAccessibleItemName.value != peer.device_group_name) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (model.selectedAccessibleItemName.value != peer.loginName) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -187,6 +187,11 @@ class _RawTouchGestureDetectorRegionState
|
||||
return;
|
||||
}
|
||||
_cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch;
|
||||
if (ffiModel.isPeerMobile) {
|
||||
await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,15 +209,31 @@ class _RawTouchGestureDetectorRegionState
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!ffi.ffiModel.isPeerMobile) {
|
||||
if (handleTouch) {
|
||||
final isMoved = await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
if (!isMoved) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
} else {
|
||||
// It's better to send a message to tell the controlled device that the long press event is triggered.
|
||||
// We're now using a `TimerTask` in `InputService.kt` to decide whether to trigger the long press event.
|
||||
// It's not accurate and it's better to use the same detection logic in the controlling side.
|
||||
}
|
||||
}
|
||||
|
||||
onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async {
|
||||
if (!ffiModel.isPeerMobile || lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
final isMoved = await ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
if (!isMoved) {
|
||||
if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!ffi.ffiModel.isPeerMobile) {
|
||||
await inputModel.tap(MouseButtons.right);
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +361,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
ffi.cursorModel.clearRemoteWindowCoords();
|
||||
}
|
||||
if (handleTouch) {
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
await inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +453,8 @@ class _RawTouchGestureDetectorRegionState
|
||||
instance
|
||||
..onLongPressDown = onLongPressDown
|
||||
..onLongPressUp = onLongPressUp
|
||||
..onLongPress = onLongPress;
|
||||
..onLongPress = onLongPress
|
||||
..onLongPressMoveUpdate = onLongPressMoveUpdate;
|
||||
}),
|
||||
// Customized
|
||||
HoldTapMoveGestureRecognizer:
|
||||
|
||||
@@ -248,7 +248,7 @@ const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
// Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead.
|
||||
const kWindowResizeEdgeSize = 5.0;
|
||||
const kWindowBorderWidth = 1.0;
|
||||
final kWindowBorderWidth = isWindows ? 0.0 : 1.0;
|
||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||
const kFrameBorderRadius = 12.0;
|
||||
const kFrameClipRRectBorderRadius = 12.0;
|
||||
|
||||
@@ -39,7 +39,7 @@ class _OnlineStatusWidgetState extends State<OnlineStatusWidget> {
|
||||
double? get height => bind.isIncomingOnly() ? null : em * 3;
|
||||
|
||||
void onUsePublicServerGuide() {
|
||||
const url = "https://rustdesk.com/pricing.html";
|
||||
const url = "https://rustdesk.com/pricing";
|
||||
canLaunchUrlString(url).then((can) {
|
||||
if (can) {
|
||||
launchUrlString(url);
|
||||
@@ -200,18 +200,21 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
final _idController = IDTextEditingController();
|
||||
|
||||
final RxBool _idInputFocused = false.obs;
|
||||
final FocusNode _idFocusNode = FocusNode();
|
||||
final TextEditingController _idEditingController = TextEditingController();
|
||||
|
||||
bool isWindowMinimized = false;
|
||||
List<Peer> peers = [];
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
final AllPeersLoader _allPeersLoader = AllPeersLoader();
|
||||
|
||||
// https://github.com/flutter/flutter/issues/157244
|
||||
Iterable<Peer> _autocompleteOpts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_allPeersLoader.init(setState);
|
||||
_idFocusNode.addListener(onFocusChanged);
|
||||
if (_idController.text.isEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
@@ -222,6 +225,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
}
|
||||
});
|
||||
}
|
||||
Get.put<TextEditingController>(_idEditingController);
|
||||
Get.put<IDTextEditingController>(_idController);
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
@@ -230,6 +234,10 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
void dispose() {
|
||||
_idController.dispose();
|
||||
windowManager.removeListener(this);
|
||||
_allPeersLoader.clear();
|
||||
_idFocusNode.removeListener(onFocusChanged);
|
||||
_idFocusNode.dispose();
|
||||
_idEditingController.dispose();
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
}
|
||||
@@ -273,6 +281,20 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
bind.mainOnMainWindowClose();
|
||||
}
|
||||
|
||||
void onFocusChanged() {
|
||||
_idInputFocused.value = _idFocusNode.hasFocus;
|
||||
if (_idFocusNode.hasFocus) {
|
||||
if (_allPeersLoader.needLoad) {
|
||||
_allPeersLoader.getAllPeers();
|
||||
}
|
||||
|
||||
final textLength = _idEditingController.value.text.length;
|
||||
// Select all to facilitate removing text, just following the behavior of address input of chrome.
|
||||
_idEditingController.selection =
|
||||
TextSelection(baseOffset: 0, extentOffset: textLength);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||
@@ -304,18 +326,6 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
connect(context, id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
Future<void> _fetchPeers() async {
|
||||
setState(() {
|
||||
isPeersLoading = true;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
peers = await getAllPeers();
|
||||
setState(() {
|
||||
isPeersLoading = false;
|
||||
isPeersLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
/// Search for a peer.
|
||||
Widget _buildRemoteIDTextField(BuildContext context) {
|
||||
@@ -332,11 +342,12 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Autocomplete<Peer>(
|
||||
child: RawAutocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
_autocompleteOpts = const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
} else if (_allPeersLoader.peers.isEmpty &&
|
||||
!_allPeersLoader.isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
username: '',
|
||||
@@ -350,6 +361,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
device_group_name: '',
|
||||
);
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
@@ -362,7 +374,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
);
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
_autocompleteOpts = peers
|
||||
_autocompleteOpts = _allPeersLoader.peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
@@ -376,25 +388,16 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
focusNode: _idFocusNode,
|
||||
textEditingController: _idEditingController,
|
||||
fieldViewBuilder: (
|
||||
BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
FocusNode fieldFocusNode,
|
||||
VoidCallback onFieldSubmitted,
|
||||
) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
Get.put<TextEditingController>(fieldTextEditingController);
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idInputFocused.value = fieldFocusNode.hasFocus;
|
||||
if (fieldFocusNode.hasFocus && !isPeersLoading) {
|
||||
_fetchPeers();
|
||||
}
|
||||
});
|
||||
final textLength =
|
||||
fieldTextEditingController.value.text.length;
|
||||
// select all to facilitate removing text, just following the behavior of address input of chrome
|
||||
fieldTextEditingController.selection =
|
||||
TextSelection(baseOffset: 0, extentOffset: textLength);
|
||||
updateTextAndPreserveSelection(
|
||||
fieldTextEditingController, _idController.text);
|
||||
return Obx(() => TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
@@ -467,7 +470,8 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: 319,
|
||||
),
|
||||
child: peers.isEmpty && isPeersLoading
|
||||
child: _allPeersLoader.peers.isEmpty &&
|
||||
!_allPeersLoader.isPeersLoaded
|
||||
? Container(
|
||||
height: 80,
|
||||
child: Center(
|
||||
|
||||
@@ -834,10 +834,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
_uniLinksSubscription?.cancel();
|
||||
Get.delete<RxBool>(tag: 'stop-service');
|
||||
_updateTimer?.cancel();
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||
}
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -607,7 +607,6 @@ class _GeneralState extends State<_General> {
|
||||
bool user_dir_exists = await Directory(user_dir).exists();
|
||||
bool root_dir_exists =
|
||||
showRootDir ? await Directory(root_dir).exists() : false;
|
||||
// canLaunchUrl blocked on windows portable, user SYSTEM
|
||||
return {
|
||||
'user_dir': user_dir,
|
||||
'root_dir': root_dir,
|
||||
|
||||
@@ -103,11 +103,13 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
));
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
: workaroundWindowBorder(
|
||||
context,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
|
||||
@@ -118,11 +118,13 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: child),
|
||||
)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
);
|
||||
: workaroundWindowBorder(
|
||||
context,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: MyTheme.color(context).border!)),
|
||||
child: child,
|
||||
));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
|
||||
@@ -212,14 +212,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
);
|
||||
final tabWidget = isLinux
|
||||
? buildVirtualWindowFrame(context, child)
|
||||
: Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: child,
|
||||
));
|
||||
: workaroundWindowBorder(
|
||||
context,
|
||||
Obx(() => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: MyTheme.color(context).border!,
|
||||
width: stateGlobal.windowBorderWidth.value),
|
||||
),
|
||||
child: child,
|
||||
)));
|
||||
return isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(() => SubWindowDragToResizeArea(
|
||||
|
||||
@@ -88,12 +88,14 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
);
|
||||
return isLinux
|
||||
? buildVirtualWindowFrame(context, body)
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: MyTheme.color(context).border!)),
|
||||
child: body,
|
||||
);
|
||||
: workaroundWindowBorder(
|
||||
context,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border:
|
||||
Border.all(color: MyTheme.color(context).border!)),
|
||||
child: body,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -133,7 +133,8 @@ void runMainApp(bool startService) async {
|
||||
runApp(App());
|
||||
|
||||
// Set window option.
|
||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions();
|
||||
WindowOptions windowOptions =
|
||||
getHiddenTitleBarWindowOptions(isMainWindow: true);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
// Restore the location of the main window before window hide or show.
|
||||
await restoreWindowPosition(WindowType.Main);
|
||||
@@ -354,7 +355,10 @@ void runInstallPage() async {
|
||||
}
|
||||
|
||||
WindowOptions getHiddenTitleBarWindowOptions(
|
||||
{Size? size, bool center = false, bool? alwaysOnTop}) {
|
||||
{bool isMainWindow = false,
|
||||
Size? size,
|
||||
bool center = false,
|
||||
bool? alwaysOnTop}) {
|
||||
var defaultTitleBarStyle = TitleBarStyle.hidden;
|
||||
// we do not hide titlebar on win7 because of the frame overflow.
|
||||
if (kUseCompatibleUiMode) {
|
||||
@@ -363,7 +367,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
|
||||
return WindowOptions(
|
||||
size: size,
|
||||
center: center,
|
||||
backgroundColor: Colors.transparent,
|
||||
backgroundColor: (isMacOS && isMainWindow) ? null : Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: defaultTitleBarStyle,
|
||||
alwaysOnTop: alwaysOnTop,
|
||||
@@ -485,9 +489,10 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
child = keyListenerBuilder(context, child);
|
||||
}
|
||||
if (isLinux) {
|
||||
child = buildVirtualWindowFrame(context, child);
|
||||
return buildVirtualWindowFrame(context, child);
|
||||
} else {
|
||||
return workaroundWindowBorder(context, child);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -41,10 +41,11 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
final _idController = IDTextEditingController();
|
||||
final RxBool _idEmpty = true.obs;
|
||||
|
||||
List<Peer> peers = [];
|
||||
final FocusNode _idFocusNode = FocusNode();
|
||||
final TextEditingController _idEditingController = TextEditingController();
|
||||
|
||||
final AllPeersLoader _allPeersLoader = AllPeersLoader();
|
||||
|
||||
bool isPeersLoading = false;
|
||||
bool isPeersLoaded = false;
|
||||
StreamSubscription? _uniLinksSubscription;
|
||||
|
||||
// https://github.com/flutter/flutter/issues/157244
|
||||
@@ -61,6 +62,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_allPeersLoader.init(setState);
|
||||
_idFocusNode.addListener(onFocusChanged);
|
||||
if (_idController.text.isEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final lastRemoteId = await bind.mainGetLastRemoteId();
|
||||
@@ -71,6 +74,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
});
|
||||
}
|
||||
Get.put<TextEditingController>(_idEditingController);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -80,7 +84,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (!bind.isCustomClient())
|
||||
if (!bind.isCustomClient() && !isIOS)
|
||||
Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)),
|
||||
_buildRemoteIDTextField(),
|
||||
])),
|
||||
@@ -99,6 +103,20 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
connect(context, id);
|
||||
}
|
||||
|
||||
void onFocusChanged() {
|
||||
_idEmpty.value = _idEditingController.text.isEmpty;
|
||||
if (_idFocusNode.hasFocus) {
|
||||
if (_allPeersLoader.needLoad) {
|
||||
_allPeersLoader.getAllPeers();
|
||||
}
|
||||
|
||||
final textLength = _idEditingController.value.text.length;
|
||||
// Select all to facilitate removing text, just following the behavior of address input of chrome.
|
||||
_idEditingController.selection =
|
||||
TextSelection(baseOffset: 0, extentOffset: textLength);
|
||||
}
|
||||
}
|
||||
|
||||
/// UI for software update.
|
||||
/// If _updateUrl] is not empty, shows a button to update the software.
|
||||
Widget _buildUpdateUI(String updateUrl) {
|
||||
@@ -107,7 +125,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
: InkWell(
|
||||
onTap: () async {
|
||||
final url = 'https://rustdesk.com/download';
|
||||
// https://pub.dev/packages/url_launcher#configuration
|
||||
// https://pub.dev/packages/url_launcher#configuration
|
||||
// https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs
|
||||
//
|
||||
// `await launchUrl(Uri.parse(url))` can also run if skip
|
||||
@@ -115,9 +133,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
// 2. `<action android:name="android.support.customtabs.action.CustomTabsService" />` in AndroidManifest.xml
|
||||
//
|
||||
// But it is better to add the check.
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
}
|
||||
await launchUrl(Uri.parse(url));
|
||||
},
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.center,
|
||||
@@ -129,18 +145,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
color: Colors.white, fontWeight: FontWeight.bold))));
|
||||
}
|
||||
|
||||
Future<void> _fetchPeers() async {
|
||||
setState(() {
|
||||
isPeersLoading = true;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
peers = await getAllPeers();
|
||||
setState(() {
|
||||
isPeersLoading = false;
|
||||
isPeersLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// UI for the remote ID TextField.
|
||||
/// Search for a peer and connect to it if the id exists.
|
||||
Widget _buildRemoteIDTextField() {
|
||||
@@ -158,11 +162,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Autocomplete<Peer>(
|
||||
child: RawAutocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
_autocompleteOpts = const Iterable<Peer>.empty();
|
||||
} else if (peers.isEmpty && !isPeersLoaded) {
|
||||
} else if (_allPeersLoader.peers.isEmpty &&
|
||||
!_allPeersLoader.isPeersLoaded) {
|
||||
Peer emptyPeer = Peer(
|
||||
id: '',
|
||||
username: '',
|
||||
@@ -176,6 +181,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
device_group_name: '',
|
||||
);
|
||||
_autocompleteOpts = [emptyPeer];
|
||||
} else {
|
||||
@@ -189,7 +195,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
String textToFind = textEditingValue.text.toLowerCase();
|
||||
|
||||
_autocompleteOpts = peers
|
||||
_autocompleteOpts = _allPeersLoader.peers
|
||||
.where((peer) =>
|
||||
peer.id.toLowerCase().contains(textToFind) ||
|
||||
peer.username
|
||||
@@ -203,25 +209,14 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
}
|
||||
return _autocompleteOpts;
|
||||
},
|
||||
focusNode: _idFocusNode,
|
||||
textEditingController: _idEditingController,
|
||||
fieldViewBuilder: (BuildContext context,
|
||||
TextEditingController fieldTextEditingController,
|
||||
FocusNode fieldFocusNode,
|
||||
VoidCallback onFieldSubmitted) {
|
||||
fieldTextEditingController.text = _idController.text;
|
||||
Get.put<TextEditingController>(
|
||||
fieldTextEditingController);
|
||||
fieldFocusNode.addListener(() async {
|
||||
_idEmpty.value =
|
||||
fieldTextEditingController.text.isEmpty;
|
||||
if (fieldFocusNode.hasFocus && !isPeersLoading) {
|
||||
_fetchPeers();
|
||||
}
|
||||
});
|
||||
final textLength =
|
||||
fieldTextEditingController.value.text.length;
|
||||
// select all to facilitate removing text, just following the behavior of address input of chrome
|
||||
fieldTextEditingController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: textLength);
|
||||
updateTextAndPreserveSelection(
|
||||
fieldTextEditingController, _idController.text);
|
||||
return AutoSizeTextField(
|
||||
controller: fieldTextEditingController,
|
||||
focusNode: fieldFocusNode,
|
||||
@@ -301,7 +296,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: 320,
|
||||
),
|
||||
child: peers.isEmpty && isPeersLoading
|
||||
child: _allPeersLoader
|
||||
.peers.isEmpty &&
|
||||
!_allPeersLoader.isPeersLoaded
|
||||
? Container(
|
||||
height: 80,
|
||||
child: Center(
|
||||
@@ -364,16 +361,16 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
void dispose() {
|
||||
_uniLinksSubscription?.cancel();
|
||||
_idController.dispose();
|
||||
_idFocusNode.removeListener(onFocusChanged);
|
||||
_allPeersLoader.clear();
|
||||
_idFocusNode.dispose();
|
||||
_idEditingController.dispose();
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()) {
|
||||
Get.delete<TextEditingController>();
|
||||
}
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.unregisterEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,9 +782,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
tiles: [
|
||||
SettingsTile(
|
||||
onPressed: (context) async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
}
|
||||
await launchUrl(Uri.parse(url));
|
||||
},
|
||||
title: Text(translate("Version: ") + version),
|
||||
value: Padding(
|
||||
@@ -928,9 +926,7 @@ void showAbout(OverlayDialogManager dialogManager) {
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
const url = 'https://rustdesk.com/';
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
}
|
||||
await launchUrl(Uri.parse(url));
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
|
||||
@@ -58,6 +58,9 @@ class AbModel {
|
||||
String? _personalAbGuid;
|
||||
RxBool legacyMode = false.obs;
|
||||
|
||||
// Only handles peers add/remove
|
||||
final Map<String, VoidCallback> _peerIdUpdateListeners = {};
|
||||
|
||||
final sortTags = shouldSortTags().obs;
|
||||
final filterByIntersection = filterAbTagByIntersection().obs;
|
||||
|
||||
@@ -188,6 +191,7 @@ class AbModel {
|
||||
debugPrint("pull current Ab error: $e");
|
||||
}
|
||||
}
|
||||
_callbackPeerUpdate();
|
||||
if (listInitialized && current.initialized) {
|
||||
_saveCache();
|
||||
}
|
||||
@@ -419,6 +423,7 @@ class AbModel {
|
||||
}
|
||||
});
|
||||
}
|
||||
_callbackPeerUpdate();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -620,6 +625,9 @@ class AbModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (abEntries.isNotEmpty) {
|
||||
_callbackPeerUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,6 +750,20 @@ class AbModel {
|
||||
}
|
||||
}
|
||||
|
||||
void _callbackPeerUpdate() {
|
||||
for (var listener in _peerIdUpdateListeners.values) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
void addPeerUpdateListener(String key, VoidCallback listener) {
|
||||
_peerIdUpdateListeners[key] = listener;
|
||||
}
|
||||
|
||||
void removePeerUpdateListener(String key) {
|
||||
_peerIdUpdateListeners.remove(key);
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,20 @@ import '../utils/http_service.dart' as http;
|
||||
class GroupModel {
|
||||
final RxBool groupLoading = false.obs;
|
||||
final RxString groupLoadError = "".obs;
|
||||
final RxList<DeviceGroupPayload> deviceGroups = RxList.empty(growable: true);
|
||||
final RxList<UserPayload> users = RxList.empty(growable: true);
|
||||
final RxList<Peer> peers = RxList.empty(growable: true);
|
||||
final RxString selectedUser = ''.obs;
|
||||
final RxString searchUserText = ''.obs;
|
||||
final RxBool isSelectedDeviceGroup = false.obs;
|
||||
final RxString selectedAccessibleItemName = ''.obs;
|
||||
final RxString searchAccessibleItemNameText = ''.obs;
|
||||
WeakReference<FFI> parent;
|
||||
var initialized = false;
|
||||
var _cacheLoadOnceFlag = false;
|
||||
var _statusCode = 200;
|
||||
|
||||
bool get emtpy => users.isEmpty && peers.isEmpty;
|
||||
final Map<String, VoidCallback> _peerIdUpdateListeners = {};
|
||||
|
||||
bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty;
|
||||
|
||||
late final Peers peersModel;
|
||||
|
||||
@@ -55,6 +59,12 @@ class GroupModel {
|
||||
}
|
||||
|
||||
Future<void> _pull() async {
|
||||
List<DeviceGroupPayload> tmpDeviceGroups = List.empty(growable: true);
|
||||
if (!await _getDeviceGroups(tmpDeviceGroups)) {
|
||||
// old hbbs doesn't support this api
|
||||
// return;
|
||||
}
|
||||
tmpDeviceGroups.sort((a, b) => a.name.compareTo(b.name));
|
||||
List<UserPayload> tmpUsers = List.empty(growable: true);
|
||||
if (!await _getUsers(tmpUsers)) {
|
||||
return;
|
||||
@@ -63,6 +73,7 @@ class GroupModel {
|
||||
if (!await _getPeers(tmpPeers)) {
|
||||
return;
|
||||
}
|
||||
deviceGroups.value = tmpDeviceGroups;
|
||||
// me first
|
||||
var index = tmpUsers
|
||||
.indexWhere((user) => user.name == gFFI.userModel.userName.value);
|
||||
@@ -71,8 +82,9 @@ class GroupModel {
|
||||
tmpUsers.insert(0, user);
|
||||
}
|
||||
users.value = tmpUsers;
|
||||
if (!users.any((u) => u.name == selectedUser.value)) {
|
||||
selectedUser.value = '';
|
||||
if (!users.any((u) => u.name == selectedAccessibleItemName.value) &&
|
||||
!deviceGroups.any((d) => d.name == selectedAccessibleItemName.value)) {
|
||||
selectedAccessibleItemName.value = '';
|
||||
}
|
||||
// recover online
|
||||
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
|
||||
@@ -82,6 +94,64 @@ class GroupModel {
|
||||
.map((e) => e.online = true)
|
||||
.toList();
|
||||
groupLoadError.value = '';
|
||||
_callbackPeerUpdate();
|
||||
}
|
||||
|
||||
Future<bool> _getDeviceGroups(
|
||||
List<DeviceGroupPayload> tmpDeviceGroups) async {
|
||||
final api = "${await bind.mainGetApiServer()}/api/device-group/accessible";
|
||||
try {
|
||||
var uri0 = Uri.parse(api);
|
||||
final pageSize = 100;
|
||||
var total = 0;
|
||||
int current = 0;
|
||||
do {
|
||||
current += 1;
|
||||
var uri = Uri(
|
||||
scheme: uri0.scheme,
|
||||
host: uri0.host,
|
||||
path: uri0.path,
|
||||
port: uri0.port,
|
||||
queryParameters: {
|
||||
'current': current.toString(),
|
||||
'pageSize': pageSize.toString(),
|
||||
});
|
||||
final resp = await http.get(uri, headers: getHttpHeaders());
|
||||
_statusCode = resp.statusCode;
|
||||
Map<String, dynamic> json =
|
||||
_jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
|
||||
if (json.containsKey('error')) {
|
||||
throw json['error'];
|
||||
}
|
||||
if (resp.statusCode != 200) {
|
||||
throw 'HTTP ${resp.statusCode}';
|
||||
}
|
||||
if (json.containsKey('total')) {
|
||||
if (total == 0) total = json['total'];
|
||||
if (json.containsKey('data')) {
|
||||
final data = json['data'];
|
||||
if (data is List) {
|
||||
for (final user in data) {
|
||||
final u = DeviceGroupPayload.fromJson(user);
|
||||
int index = tmpDeviceGroups.indexWhere((e) => e.name == u.name);
|
||||
if (index < 0) {
|
||||
tmpDeviceGroups.add(u);
|
||||
} else {
|
||||
tmpDeviceGroups[index] = u;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (current * pageSize < total);
|
||||
return true;
|
||||
} catch (err) {
|
||||
debugPrint('get accessible device groups: $err');
|
||||
// old hbbs doesn't support this api
|
||||
// groupLoadError.value =
|
||||
// '${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> _getUsers(List<UserPayload> tmpUsers) async {
|
||||
@@ -225,6 +295,7 @@ class GroupModel {
|
||||
try {
|
||||
final map = (<String, dynamic>{
|
||||
"access_token": bind.mainGetLocalOption(key: 'access_token'),
|
||||
"device_groups": deviceGroups.map((e) => e.toGroupCacheJson()).toList(),
|
||||
"users": users.map((e) => e.toGroupCacheJson()).toList(),
|
||||
'peers': peers.map((e) => e.toGroupCacheJson()).toList()
|
||||
});
|
||||
@@ -244,8 +315,14 @@ class GroupModel {
|
||||
if (groupLoading.value) return;
|
||||
final data = jsonDecode(cache);
|
||||
if (data == null || data['access_token'] != access_token) return;
|
||||
deviceGroups.clear();
|
||||
users.clear();
|
||||
peers.clear();
|
||||
if (data['device_groups'] is List) {
|
||||
for (var u in data['device_groups']) {
|
||||
deviceGroups.add(DeviceGroupPayload.fromJson(u));
|
||||
}
|
||||
}
|
||||
if (data['users'] is List) {
|
||||
for (var u in data['users']) {
|
||||
users.add(UserPayload.fromJson(u));
|
||||
@@ -255,6 +332,7 @@ class GroupModel {
|
||||
for (final peer in data['peers']) {
|
||||
peers.add(Peer.fromJson(peer));
|
||||
}
|
||||
_callbackPeerUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("load group cache: $e");
|
||||
@@ -263,9 +341,24 @@ class GroupModel {
|
||||
|
||||
reset() async {
|
||||
groupLoadError.value = '';
|
||||
deviceGroups.clear();
|
||||
users.clear();
|
||||
peers.clear();
|
||||
selectedUser.value = '';
|
||||
selectedAccessibleItemName.value = '';
|
||||
await bind.mainClearGroup();
|
||||
}
|
||||
|
||||
void _callbackPeerUpdate() {
|
||||
for (var listener in _peerIdUpdateListeners.values) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
void addPeerUpdateListener(String key, VoidCallback listener) {
|
||||
_peerIdUpdateListeners[key] = listener;
|
||||
}
|
||||
|
||||
void removePeerUpdateListener(String key) {
|
||||
_peerIdUpdateListeners.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import '../common.dart';
|
||||
import '../consts.dart';
|
||||
|
||||
/// Mouse button enum.
|
||||
enum MouseButtons { left, right, wheel }
|
||||
enum MouseButtons { left, right, wheel, back }
|
||||
|
||||
const _kMouseEventDown = 'mousedown';
|
||||
const _kMouseEventUp = 'mouseup';
|
||||
@@ -155,6 +155,8 @@ extension ToString on MouseButtons {
|
||||
return 'right';
|
||||
case MouseButtons.wheel:
|
||||
return 'wheel';
|
||||
case MouseButtons.back:
|
||||
return 'back';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1426,7 +1428,18 @@ class InputModel {
|
||||
}
|
||||
}
|
||||
|
||||
void onMobileBack() => tap(MouseButtons.right);
|
||||
void onMobileBack() {
|
||||
final minBackButtonVersion = "1.3.8";
|
||||
final peerVersion =
|
||||
parent.target?.ffiModel.pi.version ?? minBackButtonVersion;
|
||||
var btn = MouseButtons.back;
|
||||
// For compatibility with old versions
|
||||
if (versionCmp(peerVersion, minBackButtonVersion) < 0) {
|
||||
btn = MouseButtons.right;
|
||||
}
|
||||
tap(btn);
|
||||
}
|
||||
|
||||
void onMobileHome() => tap(MouseButtons.wheel);
|
||||
Future<void> onMobileApps() async {
|
||||
sendMouse('down', MouseButtons.wheel);
|
||||
|
||||
@@ -1500,13 +1500,15 @@ class CanvasModel with ChangeNotifier {
|
||||
return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0);
|
||||
}
|
||||
|
||||
updateSize() => _size = getSize();
|
||||
|
||||
updateViewStyle({refreshMousePos = true, notify = true}) async {
|
||||
final style = await bind.sessionGetViewStyle(sessionId: sessionId);
|
||||
if (style == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_size = getSize();
|
||||
updateSize();
|
||||
final displayWidth = getDisplayWidth();
|
||||
final displayHeight = getDisplayHeight();
|
||||
final viewStyle = ViewStyle(
|
||||
@@ -1543,7 +1545,7 @@ class CanvasModel with ChangeNotifier {
|
||||
_resetCanvasOffset(int displayWidth, int displayHeight) {
|
||||
_x = (size.width - displayWidth * _scale) / 2;
|
||||
_y = (size.height - displayHeight * _scale) / 2;
|
||||
if (isMobile && _lastViewStyle.style == kRemoteViewStyleOriginal) {
|
||||
if (isMobile) {
|
||||
_moveToCenterCursor();
|
||||
}
|
||||
}
|
||||
@@ -1736,7 +1738,8 @@ class CanvasModel with ChangeNotifier {
|
||||
_timerMobileFocusCanvasCursor?.cancel();
|
||||
_timerMobileFocusCanvasCursor =
|
||||
Timer(Duration(milliseconds: 100), () async {
|
||||
await updateViewStyle(refreshMousePos: false, notify: false);
|
||||
updateSize();
|
||||
_resetCanvasOffset(getDisplayWidth(), getDisplayHeight());
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
@@ -2427,6 +2430,8 @@ class CursorModel with ChangeNotifier {
|
||||
_x = -10000;
|
||||
_x = -10000;
|
||||
_image = null;
|
||||
_firstUpdateMouseTime = null;
|
||||
gotMouseControl = true;
|
||||
disposeImages();
|
||||
|
||||
_clearCache();
|
||||
|
||||
@@ -19,6 +19,7 @@ class Peer {
|
||||
String rdpUsername;
|
||||
bool online = false;
|
||||
String loginName; //login username
|
||||
String device_group_name;
|
||||
bool? sameServer;
|
||||
|
||||
String getId() {
|
||||
@@ -41,6 +42,7 @@ class Peer {
|
||||
rdpPort = json['rdpPort'] ?? '',
|
||||
rdpUsername = json['rdpUsername'] ?? '',
|
||||
loginName = json['loginName'] ?? '',
|
||||
device_group_name = json['device_group_name'] ?? '',
|
||||
sameServer = json['same_server'];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -57,6 +59,7 @@ class Peer {
|
||||
"rdpPort": rdpPort,
|
||||
"rdpUsername": rdpUsername,
|
||||
'loginName': loginName,
|
||||
'device_group_name': device_group_name,
|
||||
'same_server': sameServer,
|
||||
};
|
||||
}
|
||||
@@ -83,6 +86,7 @@ class Peer {
|
||||
"hostname": hostname,
|
||||
"platform": platform,
|
||||
"login_name": loginName,
|
||||
"device_group_name": device_group_name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +103,7 @@ class Peer {
|
||||
required this.rdpPort,
|
||||
required this.rdpUsername,
|
||||
required this.loginName,
|
||||
required this.device_group_name,
|
||||
this.sameServer,
|
||||
});
|
||||
|
||||
@@ -116,6 +121,7 @@ class Peer {
|
||||
rdpPort: '',
|
||||
rdpUsername: '',
|
||||
loginName: '',
|
||||
device_group_name: '',
|
||||
);
|
||||
bool equal(Peer other) {
|
||||
return id == other.id &&
|
||||
@@ -129,6 +135,7 @@ class Peer {
|
||||
forceAlwaysRelay == other.forceAlwaysRelay &&
|
||||
rdpPort == other.rdpPort &&
|
||||
rdpUsername == other.rdpUsername &&
|
||||
device_group_name == other.device_group_name &&
|
||||
loginName == other.loginName;
|
||||
}
|
||||
|
||||
@@ -146,6 +153,7 @@ class Peer {
|
||||
rdpPort: other.rdpPort,
|
||||
rdpUsername: other.rdpUsername,
|
||||
loginName: other.loginName,
|
||||
device_group_name: other.device_group_name,
|
||||
sameServer: other.sameServer);
|
||||
}
|
||||
|
||||
@@ -157,6 +165,11 @@ class Peers extends ChangeNotifier {
|
||||
final String name;
|
||||
final String loadEvent;
|
||||
List<Peer> peers = List.empty(growable: true);
|
||||
// Part of the peers that are not in the rest peers list.
|
||||
// When there're too many peers, we may want to load the front 100 peers first,
|
||||
// so we can see peers in UI quickly. `restPeerIds` is the rest peers' ids.
|
||||
// And then load all peers later.
|
||||
List<String> restPeerIds = List.empty(growable: true);
|
||||
final GetInitPeers? getInitPeers;
|
||||
UpdateEvent event = UpdateEvent.load;
|
||||
static const _cbQueryOnlines = 'callback_query_onlines';
|
||||
@@ -230,6 +243,12 @@ class Peers extends ChangeNotifier {
|
||||
} else {
|
||||
peers = _decodePeers(evt['peers']);
|
||||
}
|
||||
|
||||
restPeerIds = [];
|
||||
if (evt['ids'] != null) {
|
||||
restPeerIds = (evt['ids'] as String).split(',');
|
||||
}
|
||||
|
||||
for (var peer in peers) {
|
||||
final state = onlineStates[peer.id];
|
||||
peer.online = state != null && state != false;
|
||||
|
||||
@@ -28,14 +28,14 @@ class PeerTabModel with ChangeNotifier {
|
||||
'Favorites',
|
||||
'Discovered',
|
||||
'Address book',
|
||||
'Group',
|
||||
'Accessible devices',
|
||||
];
|
||||
static const List<IconData> icons = [
|
||||
Icons.access_time_filled,
|
||||
Icons.star,
|
||||
Icons.explore,
|
||||
IconFont.addressBook,
|
||||
Icons.group,
|
||||
IconFont.deviceGroupFill,
|
||||
];
|
||||
List<bool> isEnabled = List.from([
|
||||
true,
|
||||
|
||||
@@ -2,16 +2,18 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
|
||||
void showPeerSelectionDialog(
|
||||
{bool singleSelection = false,
|
||||
required Function(List<String>) onPeersCallback}) {
|
||||
final peers = bind.mainLoadRecentPeersSync();
|
||||
required Function(List<String>) onPeersCallback}) async {
|
||||
// load recent peers, we can directly use the peers in `gFFI.recentPeersModel`.
|
||||
// The plugin is not used for now, so just left it empty here.
|
||||
final peers = '';
|
||||
if (peers.isEmpty) {
|
||||
debugPrint("load recent peers sync failed.");
|
||||
// debugPrint("load recent peers failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, dynamic> map = jsonDecode(peers);
|
||||
List<dynamic> peersList = map['peers'] ?? [];
|
||||
final selected = List<String>.empty(growable: true);
|
||||
|
||||
@@ -95,17 +95,17 @@ SPEC CHECKSUMS:
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486
|
||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
|
||||
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
|
||||
flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2
|
||||
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
|
||||
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579
|
||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
window_size: 339dafa0b27a95a62a843042038fa6c3c48de195
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
@NSApplicationMain
|
||||
@main
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
var launched = false;
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
|
||||
@@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2024 Purslane Ltd. All rights reserved.
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved.
|
||||
|
||||
@@ -1351,18 +1351,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f"
|
||||
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.2"
|
||||
version: "6.3.14"
|
||||
url_launcher_ios:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.3.6+55
|
||||
version: 1.3.8+57
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
@@ -35,7 +35,7 @@ dependencies:
|
||||
wakelock_plus: ^1.1.3
|
||||
#firebase_analytics: ^9.1.5
|
||||
package_info_plus: ^4.2.0
|
||||
url_launcher: ^6.2.1
|
||||
url_launcher: ^6.3.1
|
||||
url_launcher_ios: ^6.3.2
|
||||
toggle_switch: ^2.1.0
|
||||
dash_chat_2:
|
||||
@@ -161,6 +161,9 @@ flutter:
|
||||
- family: AddressBook
|
||||
fonts:
|
||||
- asset: assets/address_book.ttf
|
||||
- family: DeviceGroup
|
||||
fonts:
|
||||
- asset: assets/device_group.ttf
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
@@ -93,7 +93,7 @@ BEGIN
|
||||
VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "rustdesk" "\0"
|
||||
VALUE "LegalCopyright", "Copyright © 2024 Purslane Ltd. All rights reserved." "\0"
|
||||
VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "rustdesk.exe" "\0"
|
||||
VALUE "ProductName", "RustDesk" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
|
||||
@@ -34,7 +34,6 @@ parking_lot = {version = "0.12"}
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
rand = {version = "0.8", optional = true}
|
||||
fuser = {version = "0.13", optional = true}
|
||||
libc = {version = "0.2", optional = true}
|
||||
dashmap = {version ="5.5", optional = true}
|
||||
utf16string = {version = "0.2", optional = true}
|
||||
@@ -44,6 +43,7 @@ once_cell = {version = "1.18", optional = true}
|
||||
percent-encoding = {version ="2.3", optional = true}
|
||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||
fuser = {version = "0.15", default-features = false, optional = true}
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
#[allow(dead_code)]
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
use hbb_common::{allow_err, bail};
|
||||
#[cfg(target_os = "windows")]
|
||||
use hbb_common::ResultType;
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
use hbb_common::{allow_err, log};
|
||||
use hbb_common::{
|
||||
lazy_static,
|
||||
tokio::sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Mutex as TokioMutex,
|
||||
},
|
||||
ResultType,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod context_send;
|
||||
pub mod platform;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use context_send::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -28,8 +27,10 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002;
|
||||
#[cfg(target_os = "windows")]
|
||||
const ERR_CODE_SEND_MSG: u32 = 0x00000003;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) use platform::create_cliprdr_context;
|
||||
|
||||
// to-do: This trait may be removed, because unix file copy paste does not need it.
|
||||
/// Ability to handle Clipboard File from remote rustdesk client
|
||||
///
|
||||
/// # Note
|
||||
@@ -41,7 +42,6 @@ pub trait CliprdrServiceContext: Send + Sync {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError>;
|
||||
/// clear the content on clipboard
|
||||
fn empty_clipboard(&mut self, conn_id: i32) -> Result<bool, CliprdrError>;
|
||||
|
||||
/// run as a server for clipboard RPC
|
||||
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>;
|
||||
}
|
||||
@@ -63,9 +63,11 @@ pub enum CliprdrError {
|
||||
#[error("failure to read clipboard")]
|
||||
OpenClipboard,
|
||||
#[error("failure to read file metadata or content")]
|
||||
FileError { path: PathBuf, err: std::io::Error },
|
||||
FileError { path: String, err: std::io::Error },
|
||||
#[error("invalid request")]
|
||||
InvalidRequest { description: String },
|
||||
#[error("common request")]
|
||||
CommonError { description: String },
|
||||
#[error("unknown cliprdr error")]
|
||||
Unknown(u32),
|
||||
}
|
||||
@@ -107,6 +109,7 @@ pub enum ClipboardFile {
|
||||
stream_id: i32,
|
||||
requested_data: Vec<u8>,
|
||||
},
|
||||
TryEmpty,
|
||||
}
|
||||
|
||||
struct MsgChannel {
|
||||
@@ -198,42 +201,67 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc<TokioMutex<UnboundedReceiver<C
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
pub fn remove_channel_by_conn_id(conn_id: i32) {
|
||||
let mut lock = VEC_MSG_CHANNEL.write().unwrap();
|
||||
if let Some(index) = lock.iter().position(|x| x.conn_id == conn_id) {
|
||||
lock.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
#[inline]
|
||||
fn send_data(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
return send_data_to_channel(conn_id, data);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if conn_id == 0 {
|
||||
send_data_to_all(data);
|
||||
let _ = send_data_to_all(data);
|
||||
Ok(())
|
||||
} else {
|
||||
send_data_to_channel(conn_id, data);
|
||||
send_data_to_channel(conn_id, data)
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))]
|
||||
|
||||
#[inline]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> {
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
if let Some(msg_channel) = VEC_MSG_CHANNEL
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|x| x.conn_id == conn_id)
|
||||
{
|
||||
msg_channel.sender.send(data)?;
|
||||
Ok(())
|
||||
msg_channel
|
||||
.sender
|
||||
.send(data)
|
||||
.map_err(|e| CliprdrError::CommonError {
|
||||
description: e.to_string(),
|
||||
})
|
||||
} else {
|
||||
bail!("conn_id not found");
|
||||
Err(CliprdrError::InvalidRequest {
|
||||
description: "conn_id not found".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[inline]
|
||||
fn send_data_to_all(data: ClipboardFile) -> ResultType<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) {
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
if msg_channel.conn_id != conn_id {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
fn send_data_to_all(data: ClipboardFile) {
|
||||
// Need more tests to see if it's necessary to handle the error.
|
||||
for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() {
|
||||
allow_err!(msg_channel.sender.send(data.clone()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use crate::{CliprdrError, CliprdrServiceContext};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -16,76 +13,4 @@ pub fn create_cliprdr_context(
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
/// use FUSE for file pasting on these platforms
|
||||
pub mod fuse;
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub mod unix;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn create_cliprdr_context(
|
||||
_enable_files: bool,
|
||||
_enable_others: bool,
|
||||
_response_wait_timeout_secs: u32,
|
||||
) -> crate::ResultType<Box<dyn crate::CliprdrServiceContext>> {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
{
|
||||
use std::{fs::Permissions, os::unix::prelude::PermissionsExt};
|
||||
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
|
||||
if !_enable_files {
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
let timeout = std::time::Duration::from_secs(_response_wait_timeout_secs as u64);
|
||||
|
||||
let app_name = APP_NAME.read().unwrap().clone();
|
||||
|
||||
let mnt_path = format!("/tmp/{}/{}", app_name, "cliprdr");
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
std::fs::create_dir(&mnt_path).ok();
|
||||
std::fs::set_permissions(&mnt_path, Permissions::from_mode(0o777)).ok();
|
||||
|
||||
log::info!("clear previously mounted cliprdr FUSE");
|
||||
if let Err(e) = std::process::Command::new("umount").arg(&mnt_path).status() {
|
||||
log::warn!("umount {:?} may fail: {:?}", mnt_path, e);
|
||||
}
|
||||
|
||||
let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?;
|
||||
log::debug!("start cliprdr FUSE");
|
||||
unix_ctx.run()?;
|
||||
|
||||
Ok(Box::new(unix_ctx) as Box<_>)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "unix-file-copy-paste"))]
|
||||
return Ok(Box::new(DummyCliprdrContext {}) as Box<_>);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
struct DummyCliprdrContext {}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
impl CliprdrServiceContext for DummyCliprdrContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
Ok(())
|
||||
}
|
||||
fn empty_clipboard(&mut self, _conn_id: i32) -> Result<bool, CliprdrError> {
|
||||
Ok(true)
|
||||
}
|
||||
fn server_clip_file(
|
||||
&mut self,
|
||||
_conn_id: i32,
|
||||
_msg: crate::ClipboardFile,
|
||||
) -> Result<(), crate::CliprdrError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
// begin of epoch used by microsoft
|
||||
// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00
|
||||
const LDAP_EPOCH_DELTA: u64 = 116444772610000000;
|
||||
|
||||
188
libs/clipboard/src/platform/unix/filetype.rs
Normal file
188
libs/clipboard/src/platform/unix/filetype.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA};
|
||||
use crate::CliprdrError;
|
||||
use hbb_common::{
|
||||
bytes::{Buf, Bytes},
|
||||
log,
|
||||
};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use utf16string::WStr;
|
||||
|
||||
pub type Inode = u64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
// todo: support symlink
|
||||
Symlink,
|
||||
}
|
||||
|
||||
/// read only permission
|
||||
pub const PERM_READ: u16 = 0o444;
|
||||
/// read and write permission
|
||||
pub const PERM_RW: u16 = 0o644;
|
||||
/// only self can read and readonly
|
||||
pub const PERM_SELF_RO: u16 = 0o400;
|
||||
/// rwx
|
||||
pub const PERM_RWX: u16 = 0o755;
|
||||
/// max length of file name
|
||||
pub const MAX_NAME_LEN: usize = 255;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FileDescription {
|
||||
pub conn_id: i32,
|
||||
pub name: PathBuf,
|
||||
pub kind: FileType,
|
||||
pub atime: SystemTime,
|
||||
pub last_modified: SystemTime,
|
||||
pub last_metadata_changed: SystemTime,
|
||||
pub creation_time: SystemTime,
|
||||
|
||||
pub size: u64,
|
||||
|
||||
pub perm: u16,
|
||||
}
|
||||
|
||||
impl FileDescription {
|
||||
fn parse_file_descriptor(
|
||||
bytes: &mut Bytes,
|
||||
conn_id: i32,
|
||||
) -> Result<FileDescription, CliprdrError> {
|
||||
let flags = bytes.get_u32_le();
|
||||
// skip reserved 32 bytes
|
||||
bytes.advance(32);
|
||||
let attributes = bytes.get_u32_le();
|
||||
|
||||
// in original specification, this is 16 bytes reserved
|
||||
// we use the last 4 bytes to store the file mode
|
||||
// skip reserved 12 bytes
|
||||
bytes.advance(12);
|
||||
let perm = bytes.get_u32_le() as u16;
|
||||
|
||||
// last write time from 1601-01-01 00:00:00, in 100ns
|
||||
let last_write_time = bytes.get_u64_le();
|
||||
// file size
|
||||
let file_size_high = bytes.get_u32_le();
|
||||
let file_size_low = bytes.get_u32_le();
|
||||
// utf16 file name, double \0 terminated, in 520 bytes block
|
||||
// read with another pointer, and advance the main pointer
|
||||
let block = bytes.clone();
|
||||
bytes.advance(520);
|
||||
|
||||
let block = &block[..520];
|
||||
let wstr = WStr::from_utf16le(block).map_err(|e| {
|
||||
log::error!("cannot convert file descriptor path: {:?}", e);
|
||||
CliprdrError::ConversionFailure
|
||||
})?;
|
||||
|
||||
let from_unix = flags & FLAGS_FD_UNIX_MODE != 0;
|
||||
|
||||
let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 0;
|
||||
if !valid_attributes {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file description must have valid attributes".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// todo: check normal, hidden, system, readonly, archive...
|
||||
let directory = attributes & 0x10 != 0;
|
||||
let normal = attributes == 0x80;
|
||||
let hidden = attributes & 0x02 != 0;
|
||||
let readonly = attributes & 0x01 != 0;
|
||||
|
||||
let perm = if from_unix {
|
||||
// as is
|
||||
perm
|
||||
// cannot set as is...
|
||||
} else if normal {
|
||||
PERM_RWX
|
||||
} else if readonly {
|
||||
PERM_READ
|
||||
} else if hidden {
|
||||
PERM_SELF_RO
|
||||
} else if directory {
|
||||
PERM_RWX
|
||||
} else {
|
||||
PERM_RW
|
||||
};
|
||||
|
||||
let kind = if directory {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
};
|
||||
|
||||
// to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;`
|
||||
// We use `true` to for compatibility with Windows.
|
||||
// let valid_size = flags & FLAGS_FD_SIZE != 0;
|
||||
let valid_size = true;
|
||||
let size = if valid_size {
|
||||
((file_size_high as u64) << 32) + file_size_low as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 0;
|
||||
let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA {
|
||||
let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100;
|
||||
let last_write_time = Duration::from_nanos(last_write_time);
|
||||
SystemTime::UNIX_EPOCH + last_write_time
|
||||
} else {
|
||||
SystemTime::UNIX_EPOCH
|
||||
};
|
||||
|
||||
let name = wstr.to_utf8().replace('\\', "/");
|
||||
let name = PathBuf::from(name.trim_end_matches('\0'));
|
||||
|
||||
let desc = FileDescription {
|
||||
conn_id,
|
||||
name,
|
||||
kind,
|
||||
atime: last_modified,
|
||||
last_modified,
|
||||
last_metadata_changed: last_modified,
|
||||
|
||||
creation_time: last_modified,
|
||||
size,
|
||||
perm,
|
||||
};
|
||||
|
||||
Ok(desc)
|
||||
}
|
||||
|
||||
/// parse file descriptions from a format data response PDU
|
||||
/// which containing a CSPTR_FILEDESCRIPTORW indicated format data
|
||||
pub fn parse_file_descriptors(
|
||||
file_descriptor_pdu: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<Self>, CliprdrError> {
|
||||
let mut data = Bytes::from(file_descriptor_pdu);
|
||||
if data.remaining() < 4 {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with infficient length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let count = data.get_u32_le() as usize;
|
||||
if data.remaining() == 0 && count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
if data.remaining() != 592 * count {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with invalid length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut files = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let desc = Self::parse_file_descriptor(&mut data, conn_id)?;
|
||||
files.push(desc);
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
@@ -31,33 +31,29 @@ use std::{
|
||||
};
|
||||
|
||||
use fuser::{ReplyDirectory, FUSE_ROOT_ID};
|
||||
use hbb_common::{
|
||||
bytes::{Buf, Bytes},
|
||||
log,
|
||||
};
|
||||
use hbb_common::log;
|
||||
use parking_lot::{Condvar, Mutex};
|
||||
use utf16string::WStr;
|
||||
|
||||
use crate::{send_data, ClipboardFile, CliprdrError};
|
||||
|
||||
use super::LDAP_EPOCH_DELTA;
|
||||
use crate::{
|
||||
platform::unix::{
|
||||
filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX},
|
||||
BLOCK_SIZE,
|
||||
},
|
||||
send_data, ClipboardFile, CliprdrError,
|
||||
};
|
||||
|
||||
/// fuse server ready retry max times
|
||||
const READ_RETRY: i32 = 3;
|
||||
|
||||
/// block size for fuse, align to our asynchronic request size over FileContentsRequest.
|
||||
pub const BLOCK_SIZE: u32 = 4 * 1024 * 1024;
|
||||
|
||||
/// read only permission
|
||||
const PERM_READ: u16 = 0o444;
|
||||
/// read and write permission
|
||||
const PERM_RW: u16 = 0o644;
|
||||
/// only self can read and readonly
|
||||
const PERM_SELF_RO: u16 = 0o400;
|
||||
/// rwx
|
||||
const PERM_RWX: u16 = 0o755;
|
||||
/// max length of file name
|
||||
const MAX_NAME_LEN: usize = 255;
|
||||
impl From<FileType> for fuser::FileType {
|
||||
fn from(value: FileType) -> Self {
|
||||
match value {
|
||||
FileType::File => Self::RegularFile,
|
||||
FileType::Directory => Self::Directory,
|
||||
FileType::Symlink => Self::Symlink,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// fuse client
|
||||
/// this is a proxy to the fuse server
|
||||
@@ -150,9 +146,15 @@ impl fuser::Filesystem for FuseClient {
|
||||
server.release(req, ino, fh, _flags, _lock_owner, _flush, reply)
|
||||
}
|
||||
|
||||
fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
|
||||
fn getattr(
|
||||
&mut self,
|
||||
req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
fh: Option<u64>,
|
||||
reply: fuser::ReplyAttr,
|
||||
) {
|
||||
let mut server = self.server.lock();
|
||||
server.getattr(req, ino, reply)
|
||||
server.getattr(req, ino, fh, reply)
|
||||
}
|
||||
|
||||
fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) {
|
||||
@@ -247,7 +249,6 @@ impl fuser::Filesystem for FuseServer {
|
||||
|
||||
if parent_entry.attributes.kind != FileType::Directory {
|
||||
log::error!("fuse: parent is not a directory");
|
||||
|
||||
reply.error(libc::ENOTDIR);
|
||||
return;
|
||||
}
|
||||
@@ -480,7 +481,13 @@ impl fuser::Filesystem for FuseServer {
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
|
||||
fn getattr(
|
||||
&mut self,
|
||||
_req: &fuser::Request<'_>,
|
||||
ino: u64,
|
||||
_fh: Option<u64>,
|
||||
reply: fuser::ReplyAttr,
|
||||
) {
|
||||
let files = &self.files;
|
||||
let Some(entry) = files.get(ino as usize - 1) else {
|
||||
reply.error(libc::ENOENT);
|
||||
@@ -527,14 +534,6 @@ impl FuseServer {
|
||||
size: u32,
|
||||
) -> Result<Vec<u8>, std::io::Error> {
|
||||
// todo: async and concurrent read, generate stream_id per request
|
||||
log::debug!(
|
||||
"reading {:?} offset {} size {} on stream: {}",
|
||||
node.name,
|
||||
offset,
|
||||
size,
|
||||
node.stream_id
|
||||
);
|
||||
|
||||
let cb_requested = unsafe {
|
||||
// convert `size` from u32 to i32
|
||||
// yet with same bit representation
|
||||
@@ -554,16 +553,14 @@ impl FuseServer {
|
||||
clip_data_id: 0,
|
||||
};
|
||||
|
||||
send_data(node.conn_id, request.clone());
|
||||
|
||||
log::debug!(
|
||||
"waiting for read reply for {:?} on stream: {}",
|
||||
node.name,
|
||||
node.stream_id
|
||||
);
|
||||
send_data(node.conn_id, request.clone()).map_err(|e| {
|
||||
log::error!("failed to send file list to channel: {:?}", e);
|
||||
std::io::Error::new(std::io::ErrorKind::Other, e)
|
||||
})?;
|
||||
|
||||
let mut retry_times = 0;
|
||||
|
||||
// to-do: more tests needed
|
||||
loop {
|
||||
let reply = self.rx.recv_timeout(self.timeout).map_err(|e| {
|
||||
log::error!("failed to receive file list from channel: {:?}", e);
|
||||
@@ -590,7 +587,10 @@ impl FuseServer {
|
||||
));
|
||||
}
|
||||
|
||||
send_data(node.conn_id, request.clone());
|
||||
send_data(node.conn_id, request.clone()).map_err(|e| {
|
||||
log::error!("failed to send file list to channel: {:?}", e);
|
||||
std::io::Error::new(std::io::ErrorKind::Other, e)
|
||||
})?;
|
||||
continue;
|
||||
}
|
||||
return Ok(requested_data);
|
||||
@@ -605,160 +605,6 @@ impl FuseServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FileDescription {
|
||||
pub conn_id: i32,
|
||||
pub name: PathBuf,
|
||||
pub kind: FileType,
|
||||
pub atime: SystemTime,
|
||||
pub last_modified: SystemTime,
|
||||
pub last_metadata_changed: SystemTime,
|
||||
pub creation_time: SystemTime,
|
||||
|
||||
pub size: u64,
|
||||
|
||||
pub perm: u16,
|
||||
}
|
||||
|
||||
impl FileDescription {
|
||||
fn parse_file_descriptor(
|
||||
bytes: &mut Bytes,
|
||||
conn_id: i32,
|
||||
) -> Result<FileDescription, CliprdrError> {
|
||||
let flags = bytes.get_u32_le();
|
||||
// skip reserved 32 bytes
|
||||
bytes.advance(32);
|
||||
let attributes = bytes.get_u32_le();
|
||||
|
||||
// in original specification, this is 16 bytes reserved
|
||||
// we use the last 4 bytes to store the file mode
|
||||
// skip reserved 12 bytes
|
||||
bytes.advance(12);
|
||||
let perm = bytes.get_u32_le() as u16;
|
||||
|
||||
// last write time from 1601-01-01 00:00:00, in 100ns
|
||||
let last_write_time = bytes.get_u64_le();
|
||||
// file size
|
||||
let file_size_high = bytes.get_u32_le();
|
||||
let file_size_low = bytes.get_u32_le();
|
||||
// utf16 file name, double \0 terminated, in 520 bytes block
|
||||
// read with another pointer, and advance the main pointer
|
||||
let block = bytes.clone();
|
||||
bytes.advance(520);
|
||||
|
||||
let block = &block[..520];
|
||||
let wstr = WStr::from_utf16le(block).map_err(|e| {
|
||||
log::error!("cannot convert file descriptor path: {:?}", e);
|
||||
CliprdrError::ConversionFailure
|
||||
})?;
|
||||
|
||||
let from_unix = flags & 0x08 != 0;
|
||||
|
||||
let valid_attributes = flags & 0x04 != 0;
|
||||
if !valid_attributes {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file description must have valid attributes".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// todo: check normal, hidden, system, readonly, archive...
|
||||
let directory = attributes & 0x10 != 0;
|
||||
let normal = attributes == 0x80;
|
||||
let hidden = attributes & 0x02 != 0;
|
||||
let readonly = attributes & 0x01 != 0;
|
||||
|
||||
let perm = if from_unix {
|
||||
// as is
|
||||
perm
|
||||
// cannot set as is...
|
||||
} else if normal {
|
||||
PERM_RWX
|
||||
} else if readonly {
|
||||
PERM_READ
|
||||
} else if hidden {
|
||||
PERM_SELF_RO
|
||||
} else if directory {
|
||||
PERM_RWX
|
||||
} else {
|
||||
PERM_RW
|
||||
};
|
||||
|
||||
let kind = if directory {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::File
|
||||
};
|
||||
|
||||
let valid_size = flags & 0x40 != 0;
|
||||
let size = if valid_size {
|
||||
((file_size_high as u64) << 32) + file_size_low as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let valid_write_time = flags & 0x20 != 0;
|
||||
let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA {
|
||||
let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100;
|
||||
let last_write_time = Duration::from_nanos(last_write_time);
|
||||
SystemTime::UNIX_EPOCH + last_write_time
|
||||
} else {
|
||||
SystemTime::UNIX_EPOCH
|
||||
};
|
||||
|
||||
let name = wstr.to_utf8().replace('\\', "/");
|
||||
let name = PathBuf::from(name.trim_end_matches('\0'));
|
||||
|
||||
let desc = FileDescription {
|
||||
conn_id,
|
||||
name,
|
||||
kind,
|
||||
atime: last_modified,
|
||||
last_modified,
|
||||
last_metadata_changed: last_modified,
|
||||
|
||||
creation_time: last_modified,
|
||||
size,
|
||||
perm,
|
||||
};
|
||||
|
||||
Ok(desc)
|
||||
}
|
||||
|
||||
/// parse file descriptions from a format data response PDU
|
||||
/// which containing a CSPTR_FILEDESCRIPTORW indicated format data
|
||||
pub fn parse_file_descriptors(
|
||||
file_descriptor_pdu: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<Self>, CliprdrError> {
|
||||
let mut data = Bytes::from(file_descriptor_pdu);
|
||||
if data.remaining() < 4 {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with infficient length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let count = data.get_u32_le() as usize;
|
||||
if data.remaining() == 0 && count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
if data.remaining() != 592 * count {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: "file descriptor request with invalid length".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut files = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let desc = Self::parse_file_descriptor(&mut data, conn_id)?;
|
||||
files.push(desc);
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
/// a node in the FUSE file tree
|
||||
#[derive(Debug)]
|
||||
struct FuseNode {
|
||||
@@ -881,7 +727,7 @@ impl FuseNode {
|
||||
format!("invalid file name {}", file.name.display()),
|
||||
);
|
||||
CliprdrError::FileError {
|
||||
path: file.name.clone(),
|
||||
path: file.name.to_string_lossy().to_string(),
|
||||
err,
|
||||
}
|
||||
})?;
|
||||
@@ -902,26 +748,6 @@ impl FuseNode {
|
||||
}
|
||||
}
|
||||
|
||||
pub type Inode = u64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
// todo: support symlink
|
||||
Symlink,
|
||||
}
|
||||
|
||||
impl From<FileType> for fuser::FileType {
|
||||
fn from(value: FileType) -> Self {
|
||||
match value {
|
||||
FileType::File => Self::RegularFile,
|
||||
FileType::Directory => Self::Directory,
|
||||
FileType::Symlink => Self::Symlink,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InodeAttributes {
|
||||
inode: Inode,
|
||||
@@ -1064,8 +890,6 @@ impl FileHandles {
|
||||
|
||||
#[cfg(test)]
|
||||
mod fuse_test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
// todo: more tests needed!
|
||||
225
libs/clipboard/src/platform/unix/fuse/mod.rs
Normal file
225
libs/clipboard/src/platform/unix/fuse/mod.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
mod cs;
|
||||
|
||||
use super::filetype::FileDescription;
|
||||
use crate::{ClipboardFile, CliprdrError};
|
||||
use cs::FuseServer;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{config::APP_NAME, log};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
|
||||
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
|
||||
// No need to run `canonicalize()` here.
|
||||
Arc::new(mnt_path)
|
||||
};
|
||||
|
||||
static ref FUSE_CONTEXT_CLIENT: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
|
||||
static ref FUSE_CONTEXT_SERVER: Arc<Mutex<Option<FuseContext>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
|
||||
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
|
||||
if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
} else {
|
||||
FUSE_MOUNT_POINT_SERVER.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_fuse_context_inited(is_client: bool) -> bool {
|
||||
if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock().is_some()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
|
||||
let mut fuse_context_lock = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
if fuse_context_lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let mount_point = if is_client {
|
||||
FUSE_MOUNT_POINT_CLIENT.clone()
|
||||
} else {
|
||||
FUSE_MOUNT_POINT_SERVER.clone()
|
||||
};
|
||||
|
||||
let mount_point = std::path::PathBuf::from(&*mount_point);
|
||||
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
|
||||
let server = Arc::new(Mutex::new(server));
|
||||
|
||||
prepare_fuse_mount_point(&mount_point);
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
MountOption::RO,
|
||||
];
|
||||
log::info!("mounting clipboard FUSE to {}", mount_point.display());
|
||||
// to-do: ignore the error if the mount point is already mounted
|
||||
// Because the sciter version uses separate processes as the controlling side.
|
||||
let session = fuser::spawn_mount2(
|
||||
FuseServer::client(server.clone()),
|
||||
mount_point.clone(),
|
||||
&mnt_opts,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to mount cliprdr fuse: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
let session = Mutex::new(Some(session));
|
||||
|
||||
let ctx = FuseContext {
|
||||
server,
|
||||
tx,
|
||||
mount_point,
|
||||
session,
|
||||
conn_id: 0,
|
||||
};
|
||||
*fuse_context_lock = Some(ctx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninit_fuse_context(is_client: bool) {
|
||||
uninit_fuse_context_(is_client)
|
||||
}
|
||||
|
||||
pub fn format_data_response_to_urls(
|
||||
is_client: bool,
|
||||
format_data: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<String>, CliprdrError> {
|
||||
let mut ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_mut()
|
||||
.ok_or(CliprdrError::CliprdrInit)?
|
||||
.format_data_response_to_urls(format_data, conn_id)
|
||||
}
|
||||
|
||||
pub fn handle_file_content_response(
|
||||
is_client: bool,
|
||||
clip: ClipboardFile,
|
||||
) -> Result<(), CliprdrError> {
|
||||
// we don't know its corresponding request, no resend can be performed
|
||||
let ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_ref()
|
||||
.ok_or(CliprdrError::CliprdrInit)?
|
||||
.tx
|
||||
.send(clip)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to send file contents response to fuse: {:?}", e);
|
||||
CliprdrError::ClipboardInternalError
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool {
|
||||
let ctx = if is_client {
|
||||
FUSE_CONTEXT_CLIENT.lock()
|
||||
} else {
|
||||
FUSE_CONTEXT_SERVER.lock()
|
||||
};
|
||||
ctx.as_ref()
|
||||
.map(|c| c.empty_local_files(conn_id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
struct FuseContext {
|
||||
server: Arc<Mutex<FuseServer>>,
|
||||
tx: Sender<ClipboardFile>,
|
||||
mount_point: PathBuf,
|
||||
// stores fuse background session handle
|
||||
session: Mutex<Option<fuser::BackgroundSession>>,
|
||||
// Indicates the connection ID of that set the clipboard content
|
||||
conn_id: i32,
|
||||
}
|
||||
|
||||
// this function must be called after the main IPC is up
|
||||
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
|
||||
use std::{
|
||||
fs::{self, Permissions},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
};
|
||||
|
||||
fs::create_dir(mount_point).ok();
|
||||
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
|
||||
|
||||
if let Err(e) = std::process::Command::new("umount")
|
||||
.arg(mount_point)
|
||||
.status()
|
||||
{
|
||||
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
|
||||
}
|
||||
}
|
||||
|
||||
fn uninit_fuse_context_(is_client: bool) {
|
||||
if is_client {
|
||||
let _ = FUSE_CONTEXT_CLIENT.lock().take();
|
||||
} else {
|
||||
let _ = FUSE_CONTEXT_SERVER.lock().take();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FuseContext {
|
||||
fn drop(&mut self) {
|
||||
self.session.lock().take().map(|s| s.join());
|
||||
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
|
||||
}
|
||||
}
|
||||
|
||||
impl FuseContext {
|
||||
pub fn empty_local_files(&self, conn_id: i32) -> bool {
|
||||
if conn_id != 0 && self.conn_id != conn_id {
|
||||
return false;
|
||||
}
|
||||
let mut fuse_guard = self.server.lock();
|
||||
let _ = fuse_guard.load_file_list(vec![]);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn format_data_response_to_urls(
|
||||
&mut self,
|
||||
format_data: Vec<u8>,
|
||||
conn_id: i32,
|
||||
) -> Result<Vec<String>, CliprdrError> {
|
||||
let files = FileDescription::parse_file_descriptors(format_data, conn_id)?;
|
||||
|
||||
let paths = {
|
||||
let mut fuse_guard = self.server.lock();
|
||||
fuse_guard.load_file_list(files)?;
|
||||
self.conn_id = conn_id;
|
||||
|
||||
fuse_guard.list_root()
|
||||
};
|
||||
|
||||
let prefix = self.mount_point.clone();
|
||||
Ok(paths
|
||||
.into_iter()
|
||||
.map(|p| prefix.join(p).to_string_lossy().to_string())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,29 @@
|
||||
use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA};
|
||||
use crate::{
|
||||
platform::unix::{
|
||||
FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE,
|
||||
FLAGS_FD_UNIX_MODE,
|
||||
},
|
||||
CliprdrError,
|
||||
};
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read, Seek},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use utf16string::WString;
|
||||
|
||||
use crate::{
|
||||
platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA},
|
||||
CliprdrError,
|
||||
};
|
||||
|
||||
/// has valid file attributes
|
||||
const FLAGS_FD_ATTRIBUTES: u32 = 0x04;
|
||||
/// has valid file size
|
||||
const FLAGS_FD_SIZE: u32 = 0x40;
|
||||
/// has valid last write time
|
||||
const FLAGS_FD_LAST_WRITE: u32 = 0x20;
|
||||
/// show progress
|
||||
const FLAGS_FD_PROGRESSUI: u32 = 0x4000;
|
||||
/// transferred from unix, contains file mode
|
||||
/// P.S. this flag is not used in windows
|
||||
const FLAGS_FD_UNIX_MODE: u32 = 0x08;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LocalFile {
|
||||
pub relative_root: PathBuf,
|
||||
pub path: PathBuf,
|
||||
|
||||
pub handle: Option<BufReader<File>>,
|
||||
@@ -51,9 +42,9 @@ pub(super) struct LocalFile {
|
||||
}
|
||||
|
||||
impl LocalFile {
|
||||
pub fn try_open(path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
pub fn try_open(relative_root: &Path, path: &Path) -> Result<Self, CliprdrError> {
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.clone(),
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let size = mt.len() as u64;
|
||||
@@ -79,7 +70,8 @@ impl LocalFile {
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
path: path.clone(),
|
||||
relative_root: relative_root.to_path_buf(),
|
||||
path: path.to_path_buf(),
|
||||
handle,
|
||||
offset,
|
||||
size,
|
||||
@@ -121,7 +113,12 @@ impl LocalFile {
|
||||
let size_high = (self.size >> 32) as u32;
|
||||
let size_low = (self.size & (u32::MAX as u64)) as u32;
|
||||
|
||||
let path = self.path.to_string_lossy().to_string();
|
||||
let path = self
|
||||
.path
|
||||
.strip_prefix(&self.relative_root)
|
||||
.unwrap_or(&self.path)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
let wstr: WString<utf16string::LE> = WString::from(&path);
|
||||
let name = wstr.as_bytes();
|
||||
@@ -172,12 +169,12 @@ impl LocalFile {
|
||||
pub fn load_handle(&mut self) -> Result<(), CliprdrError> {
|
||||
if !self.is_dir && self.handle.is_none() {
|
||||
let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle);
|
||||
reader.fill_buf().map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
self.handle = Some(reader);
|
||||
@@ -188,20 +185,25 @@ impl LocalFile {
|
||||
pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> {
|
||||
self.load_handle()?;
|
||||
|
||||
let handle = self.handle.as_mut()?;
|
||||
let Some(handle) = self.handle.as_mut() else {
|
||||
return Err(CliprdrError::FileError {
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"),
|
||||
});
|
||||
};
|
||||
|
||||
if offset != self.offset.load(Ordering::Relaxed) {
|
||||
handle
|
||||
.seek(std::io::SeekFrom::Start(offset))
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
}
|
||||
handle
|
||||
.read_exact(buf)
|
||||
.map_err(|e| CliprdrError::FileError {
|
||||
path: self.path.clone(),
|
||||
path: self.path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let new_offset = offset + (buf.len() as u64);
|
||||
@@ -219,7 +221,8 @@ impl LocalFile {
|
||||
|
||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||
fn constr_file_lst(
|
||||
path: &PathBuf,
|
||||
relative_root: &Path,
|
||||
path: &Path,
|
||||
file_list: &mut Vec<LocalFile>,
|
||||
visited: &mut HashSet<PathBuf>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
@@ -227,22 +230,28 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
||||
if visited.contains(path) {
|
||||
return Ok(());
|
||||
}
|
||||
visited.insert(path.clone());
|
||||
visited.insert(path.to_path_buf());
|
||||
|
||||
let local_file = LocalFile::try_open(path)?;
|
||||
let local_file = LocalFile::try_open(relative_root, path)?;
|
||||
file_list.push(local_file);
|
||||
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.clone(),
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
if mt.is_dir() {
|
||||
let dir = std::fs::read_dir(path)?;
|
||||
let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
for entry in dir {
|
||||
let entry = entry?;
|
||||
let entry = entry.map_err(|e| CliprdrError::FileError {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
let path = entry.path();
|
||||
constr_file_lst(&path, file_list, visited)?;
|
||||
constr_file_lst(relative_root, &path, file_list, visited)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -251,8 +260,18 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
|
||||
let mut file_list = Vec::new();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
let relative_root = paths
|
||||
.first()
|
||||
.ok_or(CliprdrError::InvalidRequest {
|
||||
description: "empty file list".to_string(),
|
||||
})?
|
||||
.parent()
|
||||
.ok_or(CliprdrError::InvalidRequest {
|
||||
description: "empty parent".to_string(),
|
||||
})?
|
||||
.to_path_buf();
|
||||
for path in paths {
|
||||
constr_file_lst(path, &mut file_list, &mut visited)?;
|
||||
constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?;
|
||||
}
|
||||
Ok(file_list)
|
||||
}
|
||||
@@ -263,7 +282,7 @@ mod file_list_test {
|
||||
|
||||
use hbb_common::bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::{platform::fuse::FileDescription, CliprdrError};
|
||||
use crate::{platform::unix::filetype::FileDescription, CliprdrError};
|
||||
|
||||
use super::LocalFile;
|
||||
|
||||
@@ -277,6 +296,7 @@ mod file_list_test {
|
||||
#[inline]
|
||||
fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile {
|
||||
LocalFile {
|
||||
relative_root: PathBuf::from("."),
|
||||
path: PathBuf::from(path),
|
||||
handle: None,
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -1,48 +1,38 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use fuser::MountOption;
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{
|
||||
platform::{fuse::FileDescription, unix::local_file::construct_file_list},
|
||||
send_data, ClipboardFile, CliprdrError, CliprdrServiceContext,
|
||||
};
|
||||
|
||||
use self::local_file::LocalFile;
|
||||
mod filetype;
|
||||
/// use FUSE for file pasting on these platforms
|
||||
#[cfg(target_os = "linux")]
|
||||
use self::url::{encode_path_to_uri, parse_plain_uri_list};
|
||||
|
||||
use super::fuse::FuseServer;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// clipboard implementation of x11
|
||||
pub mod x11;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
/// clipboard implementation of macos
|
||||
pub mod ns_clipboard;
|
||||
|
||||
pub mod fuse;
|
||||
pub mod local_file;
|
||||
pub mod serv_files;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod url;
|
||||
/// has valid file attributes
|
||||
pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04;
|
||||
/// has valid file size
|
||||
pub const FLAGS_FD_SIZE: u32 = 0x40;
|
||||
/// has valid last write time
|
||||
pub const FLAGS_FD_LAST_WRITE: u32 = 0x20;
|
||||
/// show progress
|
||||
pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000;
|
||||
/// transferred from unix, contains file mode
|
||||
/// P.S. this flag is not used in windows
|
||||
pub const FLAGS_FD_UNIX_MODE: u32 = 0x08;
|
||||
|
||||
// not actual format id, just a placeholder
|
||||
const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
||||
const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
|
||||
pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334;
|
||||
pub const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW";
|
||||
// not actual format id, just a placeholder
|
||||
const FILECONTENTS_FORMAT_ID: i32 = 49267;
|
||||
const FILECONTENTS_FORMAT_NAME: &str = "FileContents";
|
||||
pub const FILECONTENTS_FORMAT_ID: i32 = 49267;
|
||||
pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents";
|
||||
|
||||
/// block size for fuse, align to our asynchronic request size over FileContentsRequest.
|
||||
pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024;
|
||||
|
||||
// begin of epoch used by microsoft
|
||||
// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00
|
||||
const LDAP_EPOCH_DELTA: u64 = 116444772610000000;
|
||||
|
||||
lazy_static! {
|
||||
static ref REMOTE_FORMAT_MAP: DashMap<i32, String> = DashMap::from_iter(
|
||||
@@ -58,541 +48,7 @@ lazy_static! {
|
||||
);
|
||||
}
|
||||
|
||||
fn get_local_format(remote_id: i32) -> Option<String> {
|
||||
#[inline]
|
||||
pub fn get_local_format(remote_id: i32) -> Option<String> {
|
||||
REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone())
|
||||
}
|
||||
|
||||
fn add_remote_format(local_name: &str, remote_id: i32) {
|
||||
REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string());
|
||||
}
|
||||
|
||||
trait SysClipboard: Send + Sync {
|
||||
fn start(&self);
|
||||
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>;
|
||||
fn get_file_list(&self) -> Vec<PathBuf>;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
#[cfg(not(feature = "wayland"))]
|
||||
{
|
||||
use x11::*;
|
||||
let x11_clip = X11Clipboard::new(ignore_path)?;
|
||||
Ok(Box::new(x11_clip) as Box<_>)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
use ns_clipboard::*;
|
||||
let ns_pb = NsPasteboard::new(ignore_path)?;
|
||||
Ok(Box::new(ns_pb) as Box<_>)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FileContentsRequest {
|
||||
Size {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
},
|
||||
|
||||
Range {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ClipboardContext {
|
||||
pub fuse_mount_point: PathBuf,
|
||||
/// stores fuse background session handle
|
||||
fuse_handle: Mutex<Option<fuser::BackgroundSession>>,
|
||||
|
||||
/// a sender of clipboard file contents pdu to fuse server
|
||||
fuse_tx: Sender<ClipboardFile>,
|
||||
fuse_server: Arc<Mutex<FuseServer>>,
|
||||
|
||||
clipboard: Arc<dyn SysClipboard>,
|
||||
local_files: Mutex<Vec<LocalFile>>,
|
||||
}
|
||||
|
||||
impl ClipboardContext {
|
||||
pub fn new(timeout: Duration, mount_path: PathBuf) -> Result<Self, CliprdrError> {
|
||||
// assert mount path exists
|
||||
let fuse_mount_point = mount_path.canonicalize().map_err(|e| {
|
||||
log::error!("failed to canonicalize mount path: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
|
||||
let (fuse_server, fuse_tx) = FuseServer::new(timeout);
|
||||
|
||||
let fuse_server = Arc::new(Mutex::new(fuse_server));
|
||||
|
||||
let clipboard = get_sys_clipboard(&fuse_mount_point)?;
|
||||
let clipboard = Arc::from(clipboard) as Arc<_>;
|
||||
let local_files = Mutex::new(vec![]);
|
||||
|
||||
Ok(Self {
|
||||
fuse_mount_point,
|
||||
fuse_server,
|
||||
fuse_tx,
|
||||
fuse_handle: Mutex::new(None),
|
||||
clipboard,
|
||||
local_files,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<(), CliprdrError> {
|
||||
if !self.is_stopped() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut fuse_handle = self.fuse_handle.lock();
|
||||
|
||||
let mount_path = &self.fuse_mount_point;
|
||||
|
||||
let mnt_opts = [
|
||||
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
|
||||
MountOption::NoAtime,
|
||||
MountOption::RO,
|
||||
];
|
||||
log::info!(
|
||||
"mounting clipboard FUSE to {}",
|
||||
self.fuse_mount_point.display()
|
||||
);
|
||||
|
||||
let new_handle = fuser::spawn_mount2(
|
||||
FuseServer::client(self.fuse_server.clone()),
|
||||
mount_path,
|
||||
&mnt_opts,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("failed to mount cliprdr fuse: {:?}", e);
|
||||
CliprdrError::CliprdrInit
|
||||
})?;
|
||||
*fuse_handle = Some(new_handle);
|
||||
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
log::debug!("start listening clipboard");
|
||||
clipboard.start();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// set clipboard data from file list
|
||||
pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
let prefix = self.fuse_mount_point.clone();
|
||||
let paths: Vec<PathBuf> = paths.iter().cloned().map(|p| prefix.join(p)).collect();
|
||||
log::debug!("setting clipboard with paths: {:?}", paths);
|
||||
self.clipboard.set_file_list(&paths)?;
|
||||
log::debug!("clipboard set, paths: {:?}", paths);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serve_file_contents(
|
||||
&self,
|
||||
conn_id: i32,
|
||||
request: FileContentsRequest,
|
||||
) -> Result<(), CliprdrError> {
|
||||
let mut file_list = self.local_files.lock();
|
||||
|
||||
let (file_idx, file_contents_resp) = match request {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx,
|
||||
} => {
|
||||
log::debug!("file contents (size) requested from conn: {}", conn_id);
|
||||
let Some(file) = file_list.get(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
let size = file.size;
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: size.to_le_bytes().to_vec(),
|
||||
},
|
||||
)
|
||||
}
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
log::debug!(
|
||||
"file contents (range from {} length {}) request from conn: {}",
|
||||
offset,
|
||||
length,
|
||||
conn_id
|
||||
);
|
||||
let Some(file) = file_list.get_mut(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
if offset > file.size {
|
||||
log::error!("invalid reading offset requested from conn: {}", conn_id);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid reading offset requested from conn: {}",
|
||||
conn_id
|
||||
),
|
||||
});
|
||||
}
|
||||
let read_size = if offset + length > file.size {
|
||||
file.size - offset
|
||||
} else {
|
||||
length
|
||||
};
|
||||
|
||||
let mut buf = vec![0u8; read_size as usize];
|
||||
|
||||
file.read_exact_at(&mut buf, offset)?;
|
||||
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: buf,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
send_data(conn_id, file_contents_resp);
|
||||
log::debug!("file contents sent to conn: {}", conn_id);
|
||||
// hot reload next file
|
||||
for next_file in file_list.iter_mut().skip(file_idx + 1) {
|
||||
if !next_file.is_dir {
|
||||
next_file.load_handle()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn resp_file_contents_fail(conn_id: i32, stream_id: i32) {
|
||||
let resp = ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x2,
|
||||
stream_id,
|
||||
requested_data: vec![],
|
||||
};
|
||||
send_data(conn_id, resp)
|
||||
}
|
||||
|
||||
impl ClipboardContext {
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
self.fuse_handle.lock().is_none()
|
||||
}
|
||||
|
||||
pub fn sync_local_files(&self) -> Result<(), CliprdrError> {
|
||||
let mut local_files = self.local_files.lock();
|
||||
let clipboard_files = self.clipboard.get_file_list();
|
||||
let local_file_list: Vec<PathBuf> = local_files.iter().map(|f| f.path.clone()).collect();
|
||||
if local_file_list == clipboard_files {
|
||||
return Ok(());
|
||||
}
|
||||
let new_files = construct_file_list(&clipboard_files)?;
|
||||
*local_files = new_files;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
log::debug!("serve clipboard file from conn: {}", conn_id);
|
||||
if self.is_stopped() {
|
||||
log::debug!("cliprdr stopped, restart it");
|
||||
self.run()?;
|
||||
}
|
||||
match msg {
|
||||
ClipboardFile::NotifyCallback { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
ClipboardFile::MonitorReady => {
|
||||
log::debug!("server_monitor_ready called");
|
||||
|
||||
self.send_file_list(conn_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
ClipboardFile::FormatList { format_list } => {
|
||||
log::debug!("server_format_list called");
|
||||
// filter out "FileGroupDescriptorW" and "FileContents"
|
||||
let fmt_lst: Vec<(i32, String)> = format_list
|
||||
.into_iter()
|
||||
.filter(|(_, name)| {
|
||||
name == FILEDESCRIPTORW_FORMAT_NAME || name == FILECONTENTS_FORMAT_NAME
|
||||
})
|
||||
.collect();
|
||||
if fmt_lst.len() != 2 {
|
||||
log::debug!("no supported formats");
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!("supported formats: {:?}", fmt_lst);
|
||||
let file_contents_id = fmt_lst
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILECONTENTS_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)?;
|
||||
let file_descriptor_id = fmt_lst
|
||||
.iter()
|
||||
.find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME)
|
||||
.map(|(id, _)| *id)?;
|
||||
|
||||
add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id);
|
||||
add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id);
|
||||
|
||||
// sync file system from peer
|
||||
let data = ClipboardFile::FormatDataRequest {
|
||||
requested_format_id: file_descriptor_id,
|
||||
};
|
||||
send_data(conn_id, data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FormatListResponse { msg_flags } => {
|
||||
log::debug!("server_format_list_response called");
|
||||
if msg_flags != 0x1 {
|
||||
send_format_list(conn_id)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
ClipboardFile::FormatDataRequest {
|
||||
requested_format_id,
|
||||
} => {
|
||||
log::debug!("server_format_data_request called");
|
||||
let Some(format) = get_local_format(requested_format_id) else {
|
||||
log::error!(
|
||||
"got unsupported format data request: id={} from conn={}",
|
||||
requested_format_id,
|
||||
conn_id
|
||||
);
|
||||
resp_format_data_failure(conn_id);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if format == FILEDESCRIPTORW_FORMAT_NAME {
|
||||
self.send_file_list(conn_id)?;
|
||||
} else if format == FILECONTENTS_FORMAT_NAME {
|
||||
log::error!(
|
||||
"try to read file contents with FormatDataRequest from conn={}",
|
||||
conn_id
|
||||
);
|
||||
resp_format_data_failure(conn_id);
|
||||
} else {
|
||||
log::error!(
|
||||
"got unsupported format data request: id={} from conn={}",
|
||||
requested_format_id,
|
||||
conn_id
|
||||
);
|
||||
resp_format_data_failure(conn_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags,
|
||||
format_data,
|
||||
} => {
|
||||
log::debug!(
|
||||
"server_format_data_response called, msg_flags={}",
|
||||
msg_flags
|
||||
);
|
||||
|
||||
if msg_flags != 0x1 {
|
||||
resp_format_data_failure(conn_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("parsing file descriptors");
|
||||
// this must be a file descriptor format data
|
||||
let files = FileDescription::parse_file_descriptors(format_data, conn_id)?;
|
||||
|
||||
let paths = {
|
||||
let mut fuse_guard = self.fuse_server.lock();
|
||||
fuse_guard.load_file_list(files)?;
|
||||
|
||||
fuse_guard.list_root()
|
||||
};
|
||||
|
||||
log::debug!("load file list: {:?}", paths);
|
||||
self.set_clipboard(&paths)?;
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FileContentsResponse { .. } => {
|
||||
log::debug!("server_file_contents_response called");
|
||||
// we don't know its corresponding request, no resend can be performed
|
||||
self.fuse_tx.send(msg).map_err(|e| {
|
||||
log::error!("failed to send file contents response to fuse: {:?}", e);
|
||||
CliprdrError::ClipboardInternalError
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
ClipboardFile::FileContentsRequest {
|
||||
stream_id,
|
||||
list_index,
|
||||
dw_flags,
|
||||
n_position_low,
|
||||
n_position_high,
|
||||
cb_requested,
|
||||
..
|
||||
} => {
|
||||
log::debug!("server_file_contents_request called");
|
||||
let fcr = if dw_flags == 0x1 {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
}
|
||||
} else if dw_flags == 0x2 {
|
||||
let offset = (n_position_high as u64) << 32 | n_position_low as u64;
|
||||
let length = cb_requested as u64;
|
||||
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
offset,
|
||||
length,
|
||||
}
|
||||
} else {
|
||||
log::error!("got invalid FileContentsRequest from conn={}", conn_id);
|
||||
resp_file_contents_fail(conn_id, stream_id);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.serve_file_contents(conn_id, fcr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> {
|
||||
self.sync_local_files()?;
|
||||
|
||||
let file_list = self.local_files.lock();
|
||||
send_file_list(&*file_list, conn_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl CliprdrServiceContext for ClipboardContext {
|
||||
fn set_is_stopped(&mut self) -> Result<(), CliprdrError> {
|
||||
// unmount the fuse
|
||||
if let Some(fuse_handle) = self.fuse_handle.lock().take() {
|
||||
fuse_handle.join();
|
||||
}
|
||||
// we don't stop the clipboard, keep listening in case of restart
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn empty_clipboard(&mut self, _conn_id: i32) -> Result<bool, CliprdrError> {
|
||||
self.clipboard.set_file_list(&[])?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> {
|
||||
self.serve(conn_id, msg)
|
||||
}
|
||||
}
|
||||
|
||||
fn resp_format_data_failure(conn_id: i32) {
|
||||
let data = ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 0x2,
|
||||
format_data: vec![],
|
||||
};
|
||||
send_data(conn_id, data)
|
||||
}
|
||||
|
||||
fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> {
|
||||
log::debug!("send format list to remote, conn={}", conn_id);
|
||||
let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID)
|
||||
.unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string());
|
||||
let fc_format_name =
|
||||
get_local_format(FILECONTENTS_FORMAT_ID).unwrap_or(FILECONTENTS_FORMAT_NAME.to_string());
|
||||
let format_list = ClipboardFile::FormatList {
|
||||
format_list: vec![
|
||||
(FILEDESCRIPTOR_FORMAT_ID, fd_format_name),
|
||||
(FILECONTENTS_FORMAT_ID, fc_format_name),
|
||||
],
|
||||
};
|
||||
|
||||
send_data(conn_id, format_list);
|
||||
log::debug!("format list to remote dispatched, conn={}", conn_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_file_list_pdu(files: &[LocalFile]) -> Vec<u8> {
|
||||
let mut data = BytesMut::with_capacity(4 + 592 * files.len());
|
||||
data.put_u32_le(files.len() as u32);
|
||||
for file in files.iter() {
|
||||
data.put(file.as_bin().as_slice());
|
||||
}
|
||||
|
||||
data.to_vec()
|
||||
}
|
||||
|
||||
fn send_file_list(files: &[LocalFile], conn_id: i32) -> Result<(), CliprdrError> {
|
||||
log::debug!(
|
||||
"send file list to remote, conn={}, list={:?}",
|
||||
conn_id,
|
||||
files.iter().map(|f| f.path.display()).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let format_data = build_file_list_pdu(files);
|
||||
|
||||
send_data(
|
||||
conn_id,
|
||||
ClipboardFile::FormatDataResponse {
|
||||
msg_flags: 1,
|
||||
format_data,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
use std::{collections::BTreeSet, path::PathBuf};
|
||||
|
||||
use cacao::pasteboard::{Pasteboard, PasteboardName};
|
||||
use hbb_common::log;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::{platform::unix::send_format_list, CliprdrError};
|
||||
|
||||
use super::SysClipboard;
|
||||
|
||||
#[inline]
|
||||
fn wait_file_list() -> Option<Vec<PathBuf>> {
|
||||
let pb = Pasteboard::named(PasteboardName::General);
|
||||
pb.get_file_urls()
|
||||
.ok()
|
||||
.map(|v| v.into_iter().map(|nsurl| nsurl.pathbuf()).collect())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
let pb = Pasteboard::named(PasteboardName::General);
|
||||
pb.set_files(file_list.to_vec())
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
pub struct NsPasteboard {
|
||||
ignore_path: PathBuf,
|
||||
|
||||
former_file_list: Mutex<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl NsPasteboard {
|
||||
pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
Ok(Self {
|
||||
ignore_path: ignore_path.to_owned(),
|
||||
former_file_list: Mutex::new(vec![]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SysClipboard for NsPasteboard {
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
*self.former_file_list.lock() = paths.to_vec();
|
||||
set_file_list(paths)
|
||||
}
|
||||
|
||||
fn start(&self) {
|
||||
{
|
||||
*self.former_file_list.lock() = vec![];
|
||||
}
|
||||
|
||||
loop {
|
||||
let file_list = match wait_file_list() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let filtered = file_list
|
||||
.into_iter()
|
||||
.filter(|pb| !pb.starts_with(&self.ignore_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if filtered.is_empty() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut former = self.former_file_list.lock();
|
||||
|
||||
let filtered_st: BTreeSet<_> = filtered.iter().collect();
|
||||
let former_st = former.iter().collect::<BTreeSet<_>>();
|
||||
if filtered_st == former_st {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
*former = filtered;
|
||||
}
|
||||
|
||||
if let Err(e) = send_format_list(0) {
|
||||
log::warn!("failed to send format list: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
log::debug!("stop listening file related atoms on clipboard");
|
||||
}
|
||||
|
||||
fn get_file_list(&self) -> Vec<PathBuf> {
|
||||
self.former_file_list.lock().clone()
|
||||
}
|
||||
}
|
||||
231
libs/clipboard/src/platform/unix/serv_files.rs
Normal file
231
libs/clipboard/src/platform/unix/serv_files.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use super::local_file::LocalFile;
|
||||
use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError};
|
||||
use hbb_common::{
|
||||
bytes::{BufMut, BytesMut},
|
||||
log,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// local files are cached, this value should not be changed when copying files
|
||||
// Because `CliprdrFileContentsRequest` only contains the index of the file in the list.
|
||||
// We need to keep the file list in the same order as the remote side.
|
||||
// We may add a `FileId` field to `CliprdrFileContentsRequest` in the future.
|
||||
static ref CLIP_FILES: Arc<Mutex<ClipFiles>> = Default::default();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum FileContentsRequest {
|
||||
Size {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
},
|
||||
|
||||
Range {
|
||||
stream_id: i32,
|
||||
file_idx: usize,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClipFiles {
|
||||
files: Vec<String>,
|
||||
file_list: Vec<LocalFile>,
|
||||
files_pdu: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ClipFiles {
|
||||
fn clear(&mut self) {
|
||||
self.files.clear();
|
||||
self.file_list.clear();
|
||||
self.files_pdu.clear();
|
||||
}
|
||||
|
||||
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
|
||||
let clipboard_paths = clipboard_files
|
||||
.iter()
|
||||
.map(|s| PathBuf::from(s))
|
||||
.collect::<Vec<_>>();
|
||||
self.file_list = construct_file_list(&clipboard_paths)?;
|
||||
self.files = clipboard_files.to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_file_list_pdu(&mut self) {
|
||||
let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len());
|
||||
data.put_u32_le(self.file_list.len() as u32);
|
||||
for file in self.file_list.iter() {
|
||||
data.put(file.as_bin().as_slice());
|
||||
}
|
||||
self.files_pdu = data.to_vec()
|
||||
}
|
||||
|
||||
fn serve_file_contents(
|
||||
&mut self,
|
||||
conn_id: i32,
|
||||
request: FileContentsRequest,
|
||||
) -> Result<ClipboardFile, CliprdrError> {
|
||||
let (file_idx, file_contents_resp) = match request {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx,
|
||||
} => {
|
||||
log::debug!("file contents (size) requested from conn: {}", conn_id);
|
||||
let Some(file) = self.file_list.get(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
let size = file.size;
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: size.to_le_bytes().to_vec(),
|
||||
},
|
||||
)
|
||||
}
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx,
|
||||
offset,
|
||||
length,
|
||||
} => {
|
||||
log::debug!(
|
||||
"file contents (range from {} length {}) request from conn: {}",
|
||||
offset,
|
||||
length,
|
||||
conn_id
|
||||
);
|
||||
let Some(file) = self.file_list.get_mut(file_idx) else {
|
||||
log::error!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx,
|
||||
conn_id
|
||||
);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid file index {} requested from conn: {}",
|
||||
file_idx, conn_id
|
||||
),
|
||||
});
|
||||
};
|
||||
log::debug!(
|
||||
"conn {} requested file-{}: {}",
|
||||
conn_id,
|
||||
file_idx,
|
||||
file.name
|
||||
);
|
||||
|
||||
if offset > file.size {
|
||||
log::error!("invalid reading offset requested from conn: {}", conn_id);
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!(
|
||||
"invalid reading offset requested from conn: {}",
|
||||
conn_id
|
||||
),
|
||||
});
|
||||
}
|
||||
let read_size = if offset + length > file.size {
|
||||
file.size - offset
|
||||
} else {
|
||||
length
|
||||
};
|
||||
|
||||
let mut buf = vec![0u8; read_size as usize];
|
||||
|
||||
file.read_exact_at(&mut buf, offset)?;
|
||||
|
||||
(
|
||||
file_idx,
|
||||
ClipboardFile::FileContentsResponse {
|
||||
msg_flags: 0x1,
|
||||
stream_id,
|
||||
requested_data: buf,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("file contents sent to conn: {}", conn_id);
|
||||
// hot reload next file
|
||||
for next_file in self.file_list.iter_mut().skip(file_idx + 1) {
|
||||
if !next_file.is_dir {
|
||||
next_file.load_handle()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(file_contents_resp)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear_files() {
|
||||
CLIP_FILES.lock().clear();
|
||||
}
|
||||
|
||||
pub fn read_file_contents(
|
||||
conn_id: i32,
|
||||
stream_id: i32,
|
||||
list_index: i32,
|
||||
dw_flags: i32,
|
||||
n_position_low: i32,
|
||||
n_position_high: i32,
|
||||
cb_requested: i32,
|
||||
) -> Result<ClipboardFile, CliprdrError> {
|
||||
let fcr = if dw_flags == 0x1 {
|
||||
FileContentsRequest::Size {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
}
|
||||
} else if dw_flags == 0x2 {
|
||||
let offset = (n_position_high as u64) << 32 | n_position_low as u64;
|
||||
let length = cb_requested as u64;
|
||||
|
||||
FileContentsRequest::Range {
|
||||
stream_id,
|
||||
file_idx: list_index as usize,
|
||||
offset,
|
||||
length,
|
||||
}
|
||||
} else {
|
||||
return Err(CliprdrError::InvalidRequest {
|
||||
description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"),
|
||||
});
|
||||
};
|
||||
|
||||
CLIP_FILES.lock().serve_file_contents(conn_id, fcr)
|
||||
}
|
||||
|
||||
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
||||
let mut files_lock = CLIP_FILES.lock();
|
||||
if files_lock.files == files {
|
||||
return Ok(());
|
||||
}
|
||||
files_lock.sync_files(files)?;
|
||||
Ok(files_lock.build_file_list_pdu())
|
||||
}
|
||||
|
||||
pub fn get_file_list_pdu() -> Vec<u8> {
|
||||
CLIP_FILES.lock().files_pdu.clone()
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::CliprdrError;
|
||||
|
||||
// on x11, path will be encode as
|
||||
// "/home/rustdesk/pictures/🖼️.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||
// url encode and decode is needed
|
||||
const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/');
|
||||
|
||||
pub(super) fn encode_path_to_uri(path: &PathBuf) -> io::Result<String> {
|
||||
let encoded =
|
||||
percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
|
||||
format!("file://{}", encoded)
|
||||
}
|
||||
|
||||
pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result<PathBuf, CliprdrError> {
|
||||
let encoded_path = encoded_uri.trim_start_matches("file://");
|
||||
let path_str = percent_encoding::percent_decode_str(encoded_path)
|
||||
.decode_utf8()
|
||||
.map_err(|_| CliprdrError::ConversionFailure)?;
|
||||
let path_str = path_str.to_string();
|
||||
|
||||
Ok(Path::new(&path_str).to_path_buf())
|
||||
}
|
||||
|
||||
// helper parse function
|
||||
// convert 'text/uri-list' data to a list of valid Paths
|
||||
// # Note
|
||||
// - none utf8 data will lead to error
|
||||
pub(super) fn parse_plain_uri_list(v: Vec<u8>) -> Result<Vec<PathBuf>, CliprdrError> {
|
||||
let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?;
|
||||
parse_uri_list(&text)
|
||||
}
|
||||
|
||||
// helper parse function
|
||||
// convert 'text/uri-list' data to a list of valid Paths
|
||||
// # Note
|
||||
// - none utf8 data will lead to error
|
||||
pub(super) fn parse_uri_list(text: &str) -> Result<Vec<PathBuf>, CliprdrError> {
|
||||
let mut list = Vec::new();
|
||||
|
||||
for line in text.lines() {
|
||||
if !line.starts_with("file://") {
|
||||
continue;
|
||||
}
|
||||
let decoded = parse_uri_to_path(line)?;
|
||||
list.push(decoded)
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod uri_test {
|
||||
#[test]
|
||||
fn test_conversion() {
|
||||
let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png");
|
||||
let uri = super::encode_path_to_uri(&path).unwrap();
|
||||
assert_eq!(
|
||||
uri,
|
||||
"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png"
|
||||
);
|
||||
let convert_back = super::parse_uri_to_path(&uri).unwrap();
|
||||
assert_eq!(path, convert_back);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_list() {
|
||||
let uri_list = r#"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png
|
||||
file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png
|
||||
"#;
|
||||
let list = super::parse_uri_list(uri_list.into()).unwrap();
|
||||
assert!(list.len() == 2);
|
||||
assert_eq!(list[0], list[1]);
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
use std::{collections::BTreeSet, path::PathBuf};
|
||||
|
||||
use hbb_common::log;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use x11_clipboard::Clipboard;
|
||||
use x11rb::protocol::xproto::Atom;
|
||||
|
||||
use crate::{platform::unix::send_format_list, CliprdrError};
|
||||
|
||||
use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard};
|
||||
|
||||
static X11_CLIPBOARD: OnceCell<Clipboard> = OnceCell::new();
|
||||
|
||||
fn get_clip() -> Result<&'static Clipboard, CliprdrError> {
|
||||
X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit))
|
||||
}
|
||||
|
||||
pub struct X11Clipboard {
|
||||
ignore_path: PathBuf,
|
||||
text_uri_list: Atom,
|
||||
gnome_copied_files: Atom,
|
||||
nautilus_clipboard: Atom,
|
||||
|
||||
former_file_list: Mutex<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
impl X11Clipboard {
|
||||
pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
let clipboard = get_clip()?;
|
||||
let text_uri_list = clipboard
|
||||
.setter
|
||||
.get_atom("text/uri-list")
|
||||
.map_err(|_| CliprdrError::CliprdrInit)?;
|
||||
let gnome_copied_files = clipboard
|
||||
.setter
|
||||
.get_atom("x-special/gnome-copied-files")
|
||||
.map_err(|_| CliprdrError::CliprdrInit)?;
|
||||
let nautilus_clipboard = clipboard
|
||||
.setter
|
||||
.get_atom("x-special/nautilus-clipboard")
|
||||
.map_err(|_| CliprdrError::CliprdrInit)?;
|
||||
Ok(Self {
|
||||
ignore_path: ignore_path.to_owned(),
|
||||
text_uri_list,
|
||||
gnome_copied_files,
|
||||
nautilus_clipboard,
|
||||
former_file_list: Mutex::new(vec![]),
|
||||
})
|
||||
}
|
||||
|
||||
fn load(&self, target: Atom) -> Result<Vec<u8>, CliprdrError> {
|
||||
let clip = get_clip()?.setter.atoms.clipboard;
|
||||
let prop = get_clip()?.setter.atoms.property;
|
||||
// NOTE:
|
||||
// # why not use `load_wait`
|
||||
// load_wait is likely to wait forever, which is not what we want
|
||||
let res = get_clip()?.load_wait(clip, target, prop);
|
||||
match res {
|
||||
Ok(res) => Ok(res),
|
||||
Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]),
|
||||
Err(x11_clipboard::error::Error::Timeout) => {
|
||||
log::debug!("x11 clipboard get content timeout.");
|
||||
Err(CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("x11 clipboard get content fail: {:?}", e);
|
||||
Err(CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn store_batch(&self, batch: Vec<(Atom, Vec<u8>)>) -> Result<(), CliprdrError> {
|
||||
let clip = get_clip()?.setter.atoms.clipboard;
|
||||
log::debug!("try to store clipboard content");
|
||||
get_clip()?
|
||||
.store_batch(clip, batch)
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
fn wait_file_list(&self) -> Result<Option<Vec<PathBuf>>, CliprdrError> {
|
||||
let v = self.load(self.text_uri_list)?;
|
||||
let p = parse_plain_uri_list(v)?;
|
||||
Ok(Some(p))
|
||||
}
|
||||
}
|
||||
|
||||
impl SysClipboard for X11Clipboard {
|
||||
fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> {
|
||||
*self.former_file_list.lock() = paths.to_vec();
|
||||
|
||||
let uri_list: Vec<String> = {
|
||||
let mut v = Vec::new();
|
||||
for path in paths {
|
||||
v.push(encode_path_to_uri(path)?);
|
||||
}
|
||||
v
|
||||
};
|
||||
let uri_list = uri_list.join("\n");
|
||||
let text_uri_list_data = uri_list.as_bytes().to_vec();
|
||||
let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat();
|
||||
let batch = vec![
|
||||
(self.text_uri_list, text_uri_list_data),
|
||||
(self.gnome_copied_files, gnome_copied_files_data.clone()),
|
||||
(self.nautilus_clipboard, gnome_copied_files_data),
|
||||
];
|
||||
self.store_batch(batch)
|
||||
.map_err(|_| CliprdrError::ClipboardInternalError)
|
||||
}
|
||||
|
||||
fn start(&self) {
|
||||
{
|
||||
// clear cached file list
|
||||
*self.former_file_list.lock() = vec![];
|
||||
}
|
||||
loop {
|
||||
let sth = match self.wait_file_list() {
|
||||
Ok(sth) => sth,
|
||||
Err(e) => {
|
||||
log::warn!("failed to get file list from clipboard: {}", e);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(paths) = sth else {
|
||||
// just sleep
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
};
|
||||
|
||||
let filtered = paths
|
||||
.into_iter()
|
||||
.filter(|pb| !pb.starts_with(&self.ignore_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if filtered.is_empty() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut former = self.former_file_list.lock();
|
||||
|
||||
let filtered_st: BTreeSet<_> = filtered.iter().collect();
|
||||
let former_st = former.iter().collect::<BTreeSet<_>>();
|
||||
if filtered_st == former_st {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
|
||||
*former = filtered;
|
||||
}
|
||||
|
||||
if let Err(e) = send_format_list(0) {
|
||||
log::warn!("failed to send format list: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
log::debug!("stop listening file related atoms on clipboard");
|
||||
}
|
||||
|
||||
fn get_file_list(&self) -> Vec<PathBuf> {
|
||||
self.former_file_list.lock().clone()
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@
|
||||
#![allow(deref_nullptr)]
|
||||
|
||||
use crate::{
|
||||
allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
||||
send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType,
|
||||
ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL,
|
||||
};
|
||||
use hbb_common::log;
|
||||
use hbb_common::{allow_err, log};
|
||||
use std::{
|
||||
boxed::Box,
|
||||
ffi::{CStr, CString},
|
||||
@@ -614,6 +614,7 @@ fn ret_to_result(ret: u32) -> Result<(), CliprdrError> {
|
||||
e => Err(CliprdrError::Unknown(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool {
|
||||
unsafe { TRUE == empty_cliprdr(context, conn_id as u32) }
|
||||
}
|
||||
@@ -643,6 +644,7 @@ pub fn server_clip_file(
|
||||
conn_id,
|
||||
&format_list
|
||||
);
|
||||
send_data_exclude(conn_id as _, ClipboardFile::TryEmpty);
|
||||
ret = server_format_list(context, conn_id, format_list);
|
||||
log::debug!(
|
||||
"server_format_list called, conn_id {}, return {}",
|
||||
@@ -740,6 +742,11 @@ pub fn server_clip_file(
|
||||
ret
|
||||
);
|
||||
}
|
||||
ClipboardFile::TryEmpty => {
|
||||
log::debug!("empty_clipboard called");
|
||||
let ret = empty_clipboard(context, conn_id);
|
||||
log::debug!("empty_clipboard called, conn_id {}, return {}", conn_id, ret);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 con
|
||||
DWORD positionlow, ULONG request);
|
||||
|
||||
static BOOL is_file_descriptor_from_remote();
|
||||
static BOOL is_set_by_instance(wfClipboard *clipboard);
|
||||
|
||||
static void CliprdrDataObject_Delete(CliprdrDataObject *instance);
|
||||
|
||||
@@ -600,8 +601,11 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
|
||||
clipboard->req_fdata = NULL;
|
||||
}
|
||||
}
|
||||
else
|
||||
else {
|
||||
instance->m_lSize.QuadPart =
|
||||
((UINT64)instance->m_Dsc.nFileSizeHigh << 32) | instance->m_Dsc.nFileSizeLow;
|
||||
success = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1745,8 +1749,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM
|
||||
DEBUG_CLIPRDR("info: WM_CLIPBOARDUPDATE");
|
||||
// if (clipboard->sync)
|
||||
{
|
||||
if ((GetClipboardOwner() != clipboard->hwnd) &&
|
||||
(S_FALSE == OleIsCurrentClipboard(clipboard->data_obj)))
|
||||
if (!is_set_by_instance(clipboard))
|
||||
{
|
||||
if (clipboard->hmem)
|
||||
{
|
||||
@@ -2086,6 +2089,8 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// to-do: use `fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI`.
|
||||
// We keep `fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI` for compatibility.
|
||||
// fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI;
|
||||
fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI;
|
||||
fd->dwFileAttributes = GetFileAttributesW(file_name);
|
||||
@@ -2849,6 +2854,31 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
|
||||
goto exit;
|
||||
}
|
||||
|
||||
// If the clipboard is set by the instance, or the file descriptor is from remote,
|
||||
// we should not process the request.
|
||||
// Because this may be the following cases:
|
||||
// 1. `A` -> `B`, `C`
|
||||
// 2. Copy in `A`
|
||||
// 3. Copy in `B`
|
||||
// 4. Paste in `C`
|
||||
// In this case, `C` should not get the file content from `A`. The clipboard is set by `B`.
|
||||
//
|
||||
// Or
|
||||
// 1. `B` -> `A` -> `C`
|
||||
// 2. Copy in `A`
|
||||
// 2. Copy in `B`
|
||||
// 3. Paste in `C`
|
||||
// In this case, `C` should not get the file content from `A`. The clipboard is set by `B`.
|
||||
//
|
||||
// We can simply notify `C` to clear the clipboard when `A` received copy message from `B`,
|
||||
// if connections are in the same process.
|
||||
// But if connections are in different processes, it is not easy to notify the other process.
|
||||
// So we just ignore the request from `C` in this case.
|
||||
if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) {
|
||||
rc = ERROR_INTERNAL_ERROR;
|
||||
goto exit;
|
||||
}
|
||||
|
||||
cbRequested = fileContentsRequest->cbRequested;
|
||||
if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE)
|
||||
cbRequested = sizeof(UINT64);
|
||||
@@ -3089,6 +3119,14 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
|
||||
return rc;
|
||||
}
|
||||
|
||||
BOOL is_set_by_instance(wfClipboard *clipboard)
|
||||
{
|
||||
if (GetClipboardOwner() == clipboard->hwnd || S_OK == OleIsCurrentClipboard(clipboard->data_obj)) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
BOOL is_file_descriptor_from_remote()
|
||||
{
|
||||
UINT fsid = 0;
|
||||
@@ -3175,7 +3213,7 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr)
|
||||
/* discard all contexts in clipboard */
|
||||
if (try_open_clipboard(clipboard->hwnd))
|
||||
{
|
||||
if (is_file_descriptor_from_remote())
|
||||
if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote())
|
||||
{
|
||||
if (!EmptyClipboard())
|
||||
{
|
||||
|
||||
1
libs/hbb_common
Submodule
1
libs/hbb_common
Submodule
Submodule libs/hbb_common added at 16900b9b06
3
libs/hbb_common/.gitignore
vendored
3
libs/hbb_common/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
@@ -1,65 +0,0 @@
|
||||
[package]
|
||||
name = "hbb_common"
|
||||
version = "0.1.0"
|
||||
authors = ["open-trade <info@opentradesolutions.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
flexi_logger = { version = "0.27", features = ["async"] }
|
||||
protobuf = { version = "3.4", features = ["with-bytes"] }
|
||||
tokio = { version = "1.38", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["full"] }
|
||||
futures = "0.3"
|
||||
bytes = { version = "1.6", features = ["serde"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
socket2 = { version = "0.3", features = ["reuseport"] }
|
||||
zstd = "0.13"
|
||||
anyhow = "1.0"
|
||||
futures-util = "0.3"
|
||||
directories-next = "2.0"
|
||||
rand = "0.8"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
lazy_static = "1.4"
|
||||
confy = { git = "https://github.com/rustdesk-org/confy" }
|
||||
dirs-next = "2.0"
|
||||
filetime = "0.2"
|
||||
sodiumoxide = "0.2"
|
||||
regex = "1.8"
|
||||
tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" }
|
||||
chrono = "0.4"
|
||||
backtrace = "0.3"
|
||||
libc = "0.2"
|
||||
dlopen = "0.1"
|
||||
toml = "0.7"
|
||||
uuid = { version = "1.3", features = ["v4"] }
|
||||
# new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442
|
||||
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" }
|
||||
thiserror = "1.0"
|
||||
httparse = "1.5"
|
||||
base64 = "0.22"
|
||||
url = "2.2"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
mac_address = "1.1"
|
||||
machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" }
|
||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
|
||||
rustls-platform-verifier = "0.3.1"
|
||||
rustls-pki-types = "1.4"
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||
tokio-native-tls ="0.3"
|
||||
|
||||
[build-dependencies]
|
||||
protobuf-codegen = { version = "3.4" }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi", "sysinfoapi"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
osascript = "0.3"
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
fn main() {
|
||||
let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap());
|
||||
|
||||
std::fs::create_dir_all(&out_dir).unwrap();
|
||||
|
||||
protobuf_codegen::Codegen::new()
|
||||
.pure()
|
||||
.out_dir(out_dir)
|
||||
.inputs(["protos/rendezvous.proto", "protos/message.proto"])
|
||||
.include("protos")
|
||||
.customize(protobuf_codegen::Customize::default().tokio_bytes(true))
|
||||
.run()
|
||||
.expect("Codegen failed.");
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
extern crate hbb_common;
|
||||
|
||||
fn main() {
|
||||
println!("{:?}", hbb_common::config::PeerConfig::load("455058072"));
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
extern crate hbb_common;
|
||||
#[cfg(target_os = "linux")]
|
||||
use hbb_common::platform::linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
use hbb_common::platform::macos;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
let res = linux::system_message("test title", "test message", true);
|
||||
#[cfg(target_os = "macos")]
|
||||
let res = macos::alert(
|
||||
"System Preferences".to_owned(),
|
||||
"warning".to_owned(),
|
||||
"test title".to_owned(),
|
||||
"test message".to_owned(),
|
||||
["Ok".to_owned()].to_vec(),
|
||||
);
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
println!("result {:?}", &res);
|
||||
}
|
||||
@@ -1,861 +0,0 @@
|
||||
syntax = "proto3";
|
||||
package hbb;
|
||||
|
||||
message EncodedVideoFrame {
|
||||
bytes data = 1;
|
||||
bool key = 2;
|
||||
int64 pts = 3;
|
||||
}
|
||||
|
||||
message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
|
||||
|
||||
message RGB { bool compress = 1; }
|
||||
|
||||
// planes data send directly in binary for better use arraybuffer on web
|
||||
message YUV {
|
||||
bool compress = 1;
|
||||
int32 stride = 2;
|
||||
}
|
||||
|
||||
enum Chroma {
|
||||
I420 = 0;
|
||||
I444 = 1;
|
||||
}
|
||||
|
||||
message VideoFrame {
|
||||
oneof union {
|
||||
EncodedVideoFrames vp9s = 6;
|
||||
RGB rgb = 7;
|
||||
YUV yuv = 8;
|
||||
EncodedVideoFrames h264s = 10;
|
||||
EncodedVideoFrames h265s = 11;
|
||||
EncodedVideoFrames vp8s = 12;
|
||||
EncodedVideoFrames av1s = 13;
|
||||
}
|
||||
int32 display = 14;
|
||||
}
|
||||
|
||||
message IdPk {
|
||||
string id = 1;
|
||||
bytes pk = 2;
|
||||
}
|
||||
|
||||
message DisplayInfo {
|
||||
sint32 x = 1;
|
||||
sint32 y = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
string name = 5;
|
||||
bool online = 6;
|
||||
bool cursor_embedded = 7;
|
||||
Resolution original_resolution = 8;
|
||||
double scale = 9;
|
||||
}
|
||||
|
||||
message PortForward {
|
||||
string host = 1;
|
||||
int32 port = 2;
|
||||
}
|
||||
|
||||
message FileTransfer {
|
||||
string dir = 1;
|
||||
bool show_hidden = 2;
|
||||
}
|
||||
|
||||
message OSLogin {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
bytes password = 2;
|
||||
string my_id = 4;
|
||||
string my_name = 5;
|
||||
OptionMessage option = 6;
|
||||
oneof union {
|
||||
FileTransfer file_transfer = 7;
|
||||
PortForward port_forward = 8;
|
||||
}
|
||||
bool video_ack_required = 9;
|
||||
uint64 session_id = 10;
|
||||
string version = 11;
|
||||
OSLogin os_login = 12;
|
||||
string my_platform = 13;
|
||||
bytes hwid = 14;
|
||||
}
|
||||
|
||||
message Auth2FA {
|
||||
string code = 1;
|
||||
bytes hwid = 2;
|
||||
}
|
||||
|
||||
message ChatMessage { string text = 1; }
|
||||
|
||||
message Features {
|
||||
bool privacy_mode = 1;
|
||||
}
|
||||
|
||||
message CodecAbility {
|
||||
bool vp8 = 1;
|
||||
bool vp9 = 2;
|
||||
bool av1 = 3;
|
||||
bool h264 = 4;
|
||||
bool h265 = 5;
|
||||
}
|
||||
|
||||
message SupportedEncoding {
|
||||
bool h264 = 1;
|
||||
bool h265 = 2;
|
||||
bool vp8 = 3;
|
||||
bool av1 = 4;
|
||||
CodecAbility i444 = 5;
|
||||
}
|
||||
|
||||
message PeerInfo {
|
||||
string username = 1;
|
||||
string hostname = 2;
|
||||
string platform = 3;
|
||||
repeated DisplayInfo displays = 4;
|
||||
int32 current_display = 5;
|
||||
bool sas_enabled = 6;
|
||||
string version = 7;
|
||||
Features features = 9;
|
||||
SupportedEncoding encoding = 10;
|
||||
SupportedResolutions resolutions = 11;
|
||||
// Use JSON's key-value format which is friendly for peer to handle.
|
||||
// NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string.
|
||||
string platform_additions = 12;
|
||||
WindowsSessions windows_sessions = 13;
|
||||
}
|
||||
|
||||
message WindowsSession {
|
||||
uint32 sid = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
oneof union {
|
||||
string error = 1;
|
||||
PeerInfo peer_info = 2;
|
||||
}
|
||||
bool enable_trusted_devices = 3;
|
||||
}
|
||||
|
||||
message TouchScaleUpdate {
|
||||
// The delta scale factor relative to the previous scale.
|
||||
// delta * 1000
|
||||
// 0 means scale end
|
||||
int32 scale = 1;
|
||||
}
|
||||
|
||||
message TouchPanStart {
|
||||
int32 x = 1;
|
||||
int32 y = 2;
|
||||
}
|
||||
|
||||
message TouchPanUpdate {
|
||||
// The delta x position relative to the previous position.
|
||||
int32 x = 1;
|
||||
// The delta y position relative to the previous position.
|
||||
int32 y = 2;
|
||||
}
|
||||
|
||||
message TouchPanEnd {
|
||||
int32 x = 1;
|
||||
int32 y = 2;
|
||||
}
|
||||
|
||||
message TouchEvent {
|
||||
oneof union {
|
||||
TouchScaleUpdate scale_update = 1;
|
||||
TouchPanStart pan_start = 2;
|
||||
TouchPanUpdate pan_update = 3;
|
||||
TouchPanEnd pan_end = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message PointerDeviceEvent {
|
||||
oneof union {
|
||||
TouchEvent touch_event = 1;
|
||||
}
|
||||
repeated ControlKey modifiers = 2;
|
||||
}
|
||||
|
||||
message MouseEvent {
|
||||
int32 mask = 1;
|
||||
sint32 x = 2;
|
||||
sint32 y = 3;
|
||||
repeated ControlKey modifiers = 4;
|
||||
}
|
||||
|
||||
enum KeyboardMode{
|
||||
Legacy = 0;
|
||||
Map = 1;
|
||||
Translate = 2;
|
||||
Auto = 3;
|
||||
}
|
||||
|
||||
enum ControlKey {
|
||||
Unknown = 0;
|
||||
Alt = 1;
|
||||
Backspace = 2;
|
||||
CapsLock = 3;
|
||||
Control = 4;
|
||||
Delete = 5;
|
||||
DownArrow = 6;
|
||||
End = 7;
|
||||
Escape = 8;
|
||||
F1 = 9;
|
||||
F10 = 10;
|
||||
F11 = 11;
|
||||
F12 = 12;
|
||||
F2 = 13;
|
||||
F3 = 14;
|
||||
F4 = 15;
|
||||
F5 = 16;
|
||||
F6 = 17;
|
||||
F7 = 18;
|
||||
F8 = 19;
|
||||
F9 = 20;
|
||||
Home = 21;
|
||||
LeftArrow = 22;
|
||||
/// meta key (also known as "windows"; "super"; and "command")
|
||||
Meta = 23;
|
||||
/// option key on macOS (alt key on Linux and Windows)
|
||||
Option = 24; // deprecated, use Alt instead
|
||||
PageDown = 25;
|
||||
PageUp = 26;
|
||||
Return = 27;
|
||||
RightArrow = 28;
|
||||
Shift = 29;
|
||||
Space = 30;
|
||||
Tab = 31;
|
||||
UpArrow = 32;
|
||||
Numpad0 = 33;
|
||||
Numpad1 = 34;
|
||||
Numpad2 = 35;
|
||||
Numpad3 = 36;
|
||||
Numpad4 = 37;
|
||||
Numpad5 = 38;
|
||||
Numpad6 = 39;
|
||||
Numpad7 = 40;
|
||||
Numpad8 = 41;
|
||||
Numpad9 = 42;
|
||||
Cancel = 43;
|
||||
Clear = 44;
|
||||
Menu = 45; // deprecated, use Alt instead
|
||||
Pause = 46;
|
||||
Kana = 47;
|
||||
Hangul = 48;
|
||||
Junja = 49;
|
||||
Final = 50;
|
||||
Hanja = 51;
|
||||
Kanji = 52;
|
||||
Convert = 53;
|
||||
Select = 54;
|
||||
Print = 55;
|
||||
Execute = 56;
|
||||
Snapshot = 57;
|
||||
Insert = 58;
|
||||
Help = 59;
|
||||
Sleep = 60;
|
||||
Separator = 61;
|
||||
Scroll = 62;
|
||||
NumLock = 63;
|
||||
RWin = 64;
|
||||
Apps = 65;
|
||||
Multiply = 66;
|
||||
Add = 67;
|
||||
Subtract = 68;
|
||||
Decimal = 69;
|
||||
Divide = 70;
|
||||
Equals = 71;
|
||||
NumpadEnter = 72;
|
||||
RShift = 73;
|
||||
RControl = 74;
|
||||
RAlt = 75;
|
||||
VolumeMute = 76; // mainly used on mobile devices as controlled side
|
||||
VolumeUp = 77;
|
||||
VolumeDown = 78;
|
||||
Power = 79; // mainly used on mobile devices as controlled side
|
||||
CtrlAltDel = 100;
|
||||
LockScreen = 101;
|
||||
}
|
||||
|
||||
message KeyEvent {
|
||||
// `down` indicates the key's state(down or up).
|
||||
bool down = 1;
|
||||
// `press` indicates a click event(down and up).
|
||||
bool press = 2;
|
||||
oneof union {
|
||||
ControlKey control_key = 3;
|
||||
// position key code. win: scancode, linux: key code, macos: key code
|
||||
uint32 chr = 4;
|
||||
uint32 unicode = 5;
|
||||
string seq = 6;
|
||||
// high word. virtual keycode
|
||||
// low word. unicode
|
||||
uint32 win2win_hotkey = 7;
|
||||
}
|
||||
repeated ControlKey modifiers = 8;
|
||||
KeyboardMode mode = 9;
|
||||
}
|
||||
|
||||
message CursorData {
|
||||
uint64 id = 1;
|
||||
sint32 hotx = 2;
|
||||
sint32 hoty = 3;
|
||||
int32 width = 4;
|
||||
int32 height = 5;
|
||||
bytes colors = 6;
|
||||
}
|
||||
|
||||
message CursorPosition {
|
||||
sint32 x = 1;
|
||||
sint32 y = 2;
|
||||
}
|
||||
|
||||
message Hash {
|
||||
string salt = 1;
|
||||
string challenge = 2;
|
||||
}
|
||||
|
||||
enum ClipboardFormat {
|
||||
Text = 0;
|
||||
Rtf = 1;
|
||||
Html = 2;
|
||||
ImageRgba = 21;
|
||||
ImagePng = 22;
|
||||
ImageSvg = 23;
|
||||
Special = 31;
|
||||
}
|
||||
|
||||
message Clipboard {
|
||||
bool compress = 1;
|
||||
bytes content = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
ClipboardFormat format = 5;
|
||||
// Special format name, only used when format is Special.
|
||||
string special_name = 6;
|
||||
}
|
||||
|
||||
message MultiClipboards { repeated Clipboard clipboards = 1; }
|
||||
|
||||
enum FileType {
|
||||
Dir = 0;
|
||||
DirLink = 2;
|
||||
DirDrive = 3;
|
||||
File = 4;
|
||||
FileLink = 5;
|
||||
}
|
||||
|
||||
message FileEntry {
|
||||
FileType entry_type = 1;
|
||||
string name = 2;
|
||||
bool is_hidden = 3;
|
||||
uint64 size = 4;
|
||||
uint64 modified_time = 5;
|
||||
}
|
||||
|
||||
message FileDirectory {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
repeated FileEntry entries = 3;
|
||||
}
|
||||
|
||||
message ReadDir {
|
||||
string path = 1;
|
||||
bool include_hidden = 2;
|
||||
}
|
||||
|
||||
message ReadEmptyDirs {
|
||||
string path = 1;
|
||||
bool include_hidden = 2;
|
||||
}
|
||||
|
||||
message ReadEmptyDirsResponse {
|
||||
string path = 1;
|
||||
repeated FileDirectory empty_dirs = 2;
|
||||
}
|
||||
|
||||
message ReadAllFiles {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
bool include_hidden = 3;
|
||||
}
|
||||
|
||||
message FileRename {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
string new_name = 3;
|
||||
}
|
||||
|
||||
message FileAction {
|
||||
oneof union {
|
||||
ReadDir read_dir = 1;
|
||||
FileTransferSendRequest send = 2;
|
||||
FileTransferReceiveRequest receive = 3;
|
||||
FileDirCreate create = 4;
|
||||
FileRemoveDir remove_dir = 5;
|
||||
FileRemoveFile remove_file = 6;
|
||||
ReadAllFiles all_files = 7;
|
||||
FileTransferCancel cancel = 8;
|
||||
FileTransferSendConfirmRequest send_confirm = 9;
|
||||
FileRename rename = 10;
|
||||
ReadEmptyDirs read_empty_dirs = 11;
|
||||
}
|
||||
}
|
||||
|
||||
message FileTransferCancel { int32 id = 1; }
|
||||
|
||||
message FileResponse {
|
||||
oneof union {
|
||||
FileDirectory dir = 1;
|
||||
FileTransferBlock block = 2;
|
||||
FileTransferError error = 3;
|
||||
FileTransferDone done = 4;
|
||||
FileTransferDigest digest = 5;
|
||||
ReadEmptyDirsResponse empty_dirs = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message FileTransferDigest {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
uint64 last_modified = 3;
|
||||
uint64 file_size = 4;
|
||||
bool is_upload = 5;
|
||||
bool is_identical = 6;
|
||||
}
|
||||
|
||||
message FileTransferBlock {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
bytes data = 3;
|
||||
bool compressed = 4;
|
||||
uint32 blk_id = 5;
|
||||
}
|
||||
|
||||
message FileTransferError {
|
||||
int32 id = 1;
|
||||
string error = 2;
|
||||
sint32 file_num = 3;
|
||||
}
|
||||
|
||||
message FileTransferSendRequest {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
bool include_hidden = 3;
|
||||
int32 file_num = 4;
|
||||
}
|
||||
|
||||
message FileTransferSendConfirmRequest {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
oneof union {
|
||||
bool skip = 3;
|
||||
uint32 offset_blk = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message FileTransferDone {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
}
|
||||
|
||||
message FileTransferReceiveRequest {
|
||||
int32 id = 1;
|
||||
string path = 2; // path written to
|
||||
repeated FileEntry files = 3;
|
||||
int32 file_num = 4;
|
||||
uint64 total_size = 5;
|
||||
}
|
||||
|
||||
message FileRemoveDir {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
bool recursive = 3;
|
||||
}
|
||||
|
||||
message FileRemoveFile {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
sint32 file_num = 3;
|
||||
}
|
||||
|
||||
message FileDirCreate {
|
||||
int32 id = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
// main logic from freeRDP
|
||||
message CliprdrMonitorReady {
|
||||
}
|
||||
|
||||
message CliprdrFormat {
|
||||
int32 id = 2;
|
||||
string format = 3;
|
||||
}
|
||||
|
||||
message CliprdrServerFormatList {
|
||||
repeated CliprdrFormat formats = 2;
|
||||
}
|
||||
|
||||
message CliprdrServerFormatListResponse {
|
||||
int32 msg_flags = 2;
|
||||
}
|
||||
|
||||
message CliprdrServerFormatDataRequest {
|
||||
int32 requested_format_id = 2;
|
||||
}
|
||||
|
||||
message CliprdrServerFormatDataResponse {
|
||||
int32 msg_flags = 2;
|
||||
bytes format_data = 3;
|
||||
}
|
||||
|
||||
message CliprdrFileContentsRequest {
|
||||
int32 stream_id = 2;
|
||||
int32 list_index = 3;
|
||||
int32 dw_flags = 4;
|
||||
int32 n_position_low = 5;
|
||||
int32 n_position_high = 6;
|
||||
int32 cb_requested = 7;
|
||||
bool have_clip_data_id = 8;
|
||||
int32 clip_data_id = 9;
|
||||
}
|
||||
|
||||
message CliprdrFileContentsResponse {
|
||||
int32 msg_flags = 3;
|
||||
int32 stream_id = 4;
|
||||
bytes requested_data = 5;
|
||||
}
|
||||
|
||||
message Cliprdr {
|
||||
oneof union {
|
||||
CliprdrMonitorReady ready = 1;
|
||||
CliprdrServerFormatList format_list = 2;
|
||||
CliprdrServerFormatListResponse format_list_response = 3;
|
||||
CliprdrServerFormatDataRequest format_data_request = 4;
|
||||
CliprdrServerFormatDataResponse format_data_response = 5;
|
||||
CliprdrFileContentsRequest file_contents_request = 6;
|
||||
CliprdrFileContentsResponse file_contents_response = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message Resolution {
|
||||
int32 width = 1;
|
||||
int32 height = 2;
|
||||
}
|
||||
|
||||
message DisplayResolution {
|
||||
int32 display = 1;
|
||||
Resolution resolution = 2;
|
||||
}
|
||||
|
||||
message SupportedResolutions { repeated Resolution resolutions = 1; }
|
||||
|
||||
message SwitchDisplay {
|
||||
int32 display = 1;
|
||||
sint32 x = 2;
|
||||
sint32 y = 3;
|
||||
int32 width = 4;
|
||||
int32 height = 5;
|
||||
bool cursor_embedded = 6;
|
||||
SupportedResolutions resolutions = 7;
|
||||
// Do not care about the origin point for now.
|
||||
Resolution original_resolution = 8;
|
||||
}
|
||||
|
||||
message CaptureDisplays {
|
||||
repeated int32 add = 1;
|
||||
repeated int32 sub = 2;
|
||||
repeated int32 set = 3;
|
||||
}
|
||||
|
||||
message ToggleVirtualDisplay {
|
||||
int32 display = 1;
|
||||
bool on = 2;
|
||||
}
|
||||
|
||||
message TogglePrivacyMode {
|
||||
string impl_key = 1;
|
||||
bool on = 2;
|
||||
}
|
||||
|
||||
message PermissionInfo {
|
||||
enum Permission {
|
||||
Keyboard = 0;
|
||||
Clipboard = 2;
|
||||
Audio = 3;
|
||||
File = 4;
|
||||
Restart = 5;
|
||||
Recording = 6;
|
||||
BlockInput = 7;
|
||||
}
|
||||
|
||||
Permission permission = 1;
|
||||
bool enabled = 2;
|
||||
}
|
||||
|
||||
enum ImageQuality {
|
||||
NotSet = 0;
|
||||
Low = 2;
|
||||
Balanced = 3;
|
||||
Best = 4;
|
||||
}
|
||||
|
||||
message SupportedDecoding {
|
||||
enum PreferCodec {
|
||||
Auto = 0;
|
||||
VP9 = 1;
|
||||
H264 = 2;
|
||||
H265 = 3;
|
||||
VP8 = 4;
|
||||
AV1 = 5;
|
||||
}
|
||||
|
||||
int32 ability_vp9 = 1;
|
||||
int32 ability_h264 = 2;
|
||||
int32 ability_h265 = 3;
|
||||
PreferCodec prefer = 4;
|
||||
int32 ability_vp8 = 5;
|
||||
int32 ability_av1 = 6;
|
||||
CodecAbility i444 = 7;
|
||||
Chroma prefer_chroma = 8;
|
||||
}
|
||||
|
||||
message OptionMessage {
|
||||
enum BoolOption {
|
||||
NotSet = 0;
|
||||
No = 1;
|
||||
Yes = 2;
|
||||
}
|
||||
ImageQuality image_quality = 1;
|
||||
BoolOption lock_after_session_end = 2;
|
||||
BoolOption show_remote_cursor = 3;
|
||||
BoolOption privacy_mode = 4;
|
||||
BoolOption block_input = 5;
|
||||
int32 custom_image_quality = 6;
|
||||
BoolOption disable_audio = 7;
|
||||
BoolOption disable_clipboard = 8;
|
||||
BoolOption enable_file_transfer = 9;
|
||||
SupportedDecoding supported_decoding = 10;
|
||||
int32 custom_fps = 11;
|
||||
BoolOption disable_keyboard = 12;
|
||||
// Position 13 is used for Resolution. Remove later.
|
||||
// Resolution custom_resolution = 13;
|
||||
// BoolOption support_windows_specific_session = 14;
|
||||
// starting from 15 please, do not use removed fields
|
||||
BoolOption follow_remote_cursor = 15;
|
||||
BoolOption follow_remote_window = 16;
|
||||
}
|
||||
|
||||
message TestDelay {
|
||||
int64 time = 1;
|
||||
bool from_client = 2;
|
||||
uint32 last_delay = 3;
|
||||
uint32 target_bitrate = 4;
|
||||
}
|
||||
|
||||
message PublicKey {
|
||||
bytes asymmetric_value = 1;
|
||||
bytes symmetric_value = 2;
|
||||
}
|
||||
|
||||
message SignedId { bytes id = 1; }
|
||||
|
||||
message AudioFormat {
|
||||
uint32 sample_rate = 1;
|
||||
uint32 channels = 2;
|
||||
}
|
||||
|
||||
message AudioFrame {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
// Notify peer to show message box.
|
||||
message MessageBox {
|
||||
// Message type. Refer to flutter/lib/common.dart/msgBox().
|
||||
string msgtype = 1;
|
||||
string title = 2;
|
||||
// English
|
||||
string text = 3;
|
||||
// If not empty, msgbox provides a button to following the link.
|
||||
// The link here can't be directly http url.
|
||||
// It must be the key of http url configed in peer side or "rustdesk://*" (jump in app).
|
||||
string link = 4;
|
||||
}
|
||||
|
||||
message BackNotification {
|
||||
// no need to consider block input by someone else
|
||||
enum BlockInputState {
|
||||
BlkStateUnknown = 0;
|
||||
BlkOnSucceeded = 2;
|
||||
BlkOnFailed = 3;
|
||||
BlkOffSucceeded = 4;
|
||||
BlkOffFailed = 5;
|
||||
}
|
||||
enum PrivacyModeState {
|
||||
PrvStateUnknown = 0;
|
||||
// Privacy mode on by someone else
|
||||
PrvOnByOther = 2;
|
||||
// Privacy mode is not supported on the remote side
|
||||
PrvNotSupported = 3;
|
||||
// Privacy mode on by self
|
||||
PrvOnSucceeded = 4;
|
||||
// Privacy mode on by self, but denied
|
||||
PrvOnFailedDenied = 5;
|
||||
// Some plugins are not found
|
||||
PrvOnFailedPlugin = 6;
|
||||
// Privacy mode on by self, but failed
|
||||
PrvOnFailed = 7;
|
||||
// Privacy mode off by self
|
||||
PrvOffSucceeded = 8;
|
||||
// Ctrl + P
|
||||
PrvOffByPeer = 9;
|
||||
// Privacy mode off by self, but failed
|
||||
PrvOffFailed = 10;
|
||||
PrvOffUnknown = 11;
|
||||
}
|
||||
|
||||
oneof union {
|
||||
PrivacyModeState privacy_mode_state = 1;
|
||||
BlockInputState block_input_state = 2;
|
||||
}
|
||||
// Supplementary message, for "PrvOnFailed" and "PrvOffFailed"
|
||||
string details = 3;
|
||||
// The key of the implementation
|
||||
string impl_key = 4;
|
||||
}
|
||||
|
||||
message ElevationRequestWithLogon {
|
||||
string username = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message ElevationRequest {
|
||||
oneof union {
|
||||
bool direct = 1;
|
||||
ElevationRequestWithLogon logon = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SwitchSidesRequest {
|
||||
bytes uuid = 1;
|
||||
}
|
||||
|
||||
message SwitchSidesResponse {
|
||||
bytes uuid = 1;
|
||||
LoginRequest lr = 2;
|
||||
}
|
||||
|
||||
message SwitchBack {}
|
||||
|
||||
message PluginRequest {
|
||||
string id = 1;
|
||||
bytes content = 2;
|
||||
}
|
||||
|
||||
message PluginFailure {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string msg = 3;
|
||||
}
|
||||
|
||||
message WindowsSessions {
|
||||
repeated WindowsSession sessions = 1;
|
||||
uint32 current_sid = 2;
|
||||
}
|
||||
|
||||
// Query messages from peer.
|
||||
message MessageQuery {
|
||||
// The SwitchDisplay message of the target display.
|
||||
// If the target display is not found, the message will be ignored.
|
||||
int32 switch_display = 1;
|
||||
}
|
||||
|
||||
message Misc {
|
||||
oneof union {
|
||||
ChatMessage chat_message = 4;
|
||||
SwitchDisplay switch_display = 5;
|
||||
PermissionInfo permission_info = 6;
|
||||
OptionMessage option = 7;
|
||||
AudioFormat audio_format = 8;
|
||||
string close_reason = 9;
|
||||
bool refresh_video = 10;
|
||||
bool video_received = 12;
|
||||
BackNotification back_notification = 13;
|
||||
bool restart_remote_device = 14;
|
||||
bool uac = 15;
|
||||
bool foreground_window_elevated = 16;
|
||||
bool stop_service = 17;
|
||||
ElevationRequest elevation_request = 18;
|
||||
string elevation_response = 19;
|
||||
bool portable_service_running = 20;
|
||||
SwitchSidesRequest switch_sides_request = 21;
|
||||
SwitchBack switch_back = 22;
|
||||
// Deprecated since 1.2.4, use `change_display_resolution` (36) instead.
|
||||
// But we must keep it for compatibility when peer version < 1.2.4.
|
||||
Resolution change_resolution = 24;
|
||||
PluginRequest plugin_request = 25;
|
||||
PluginFailure plugin_failure = 26;
|
||||
uint32 full_speed_fps = 27; // deprecated
|
||||
uint32 auto_adjust_fps = 28;
|
||||
bool client_record_status = 29;
|
||||
CaptureDisplays capture_displays = 30;
|
||||
int32 refresh_video_display = 31;
|
||||
ToggleVirtualDisplay toggle_virtual_display = 32;
|
||||
TogglePrivacyMode toggle_privacy_mode = 33;
|
||||
SupportedEncoding supported_encoding = 34;
|
||||
uint32 selected_sid = 35;
|
||||
DisplayResolution change_display_resolution = 36;
|
||||
MessageQuery message_query = 37;
|
||||
int32 follow_current_display = 38;
|
||||
}
|
||||
}
|
||||
|
||||
message VoiceCallRequest {
|
||||
int64 req_timestamp = 1;
|
||||
// Indicates whether the request is a connect action or a disconnect action.
|
||||
bool is_connect = 2;
|
||||
}
|
||||
|
||||
message VoiceCallResponse {
|
||||
bool accepted = 1;
|
||||
int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp].
|
||||
int64 ack_timestamp = 3;
|
||||
}
|
||||
|
||||
message Message {
|
||||
oneof union {
|
||||
SignedId signed_id = 3;
|
||||
PublicKey public_key = 4;
|
||||
TestDelay test_delay = 5;
|
||||
VideoFrame video_frame = 6;
|
||||
LoginRequest login_request = 7;
|
||||
LoginResponse login_response = 8;
|
||||
Hash hash = 9;
|
||||
MouseEvent mouse_event = 10;
|
||||
AudioFrame audio_frame = 11;
|
||||
CursorData cursor_data = 12;
|
||||
CursorPosition cursor_position = 13;
|
||||
uint64 cursor_id = 14;
|
||||
KeyEvent key_event = 15;
|
||||
Clipboard clipboard = 16;
|
||||
FileAction file_action = 17;
|
||||
FileResponse file_response = 18;
|
||||
Misc misc = 19;
|
||||
Cliprdr cliprdr = 20;
|
||||
MessageBox message_box = 21;
|
||||
SwitchSidesResponse switch_sides_response = 22;
|
||||
VoiceCallRequest voice_call_request = 23;
|
||||
VoiceCallResponse voice_call_response = 24;
|
||||
PeerInfo peer_info = 25;
|
||||
PointerDeviceEvent pointer_device_event = 26;
|
||||
Auth2FA auth_2fa = 27;
|
||||
MultiClipboards multi_clipboards = 28;
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
syntax = "proto3";
|
||||
package hbb;
|
||||
|
||||
message RegisterPeer {
|
||||
string id = 1;
|
||||
int32 serial = 2;
|
||||
}
|
||||
|
||||
enum ConnType {
|
||||
DEFAULT_CONN = 0;
|
||||
FILE_TRANSFER = 1;
|
||||
PORT_FORWARD = 2;
|
||||
RDP = 3;
|
||||
}
|
||||
|
||||
message RegisterPeerResponse { bool request_pk = 2; }
|
||||
|
||||
message PunchHoleRequest {
|
||||
string id = 1;
|
||||
NatType nat_type = 2;
|
||||
string licence_key = 3;
|
||||
ConnType conn_type = 4;
|
||||
string token = 5;
|
||||
string version = 6;
|
||||
}
|
||||
|
||||
message PunchHole {
|
||||
bytes socket_addr = 1;
|
||||
string relay_server = 2;
|
||||
NatType nat_type = 3;
|
||||
}
|
||||
|
||||
message TestNatRequest {
|
||||
int32 serial = 1;
|
||||
}
|
||||
|
||||
// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative
|
||||
message TestNatResponse {
|
||||
int32 port = 1;
|
||||
ConfigUpdate cu = 2; // for mobile
|
||||
}
|
||||
|
||||
enum NatType {
|
||||
UNKNOWN_NAT = 0;
|
||||
ASYMMETRIC = 1;
|
||||
SYMMETRIC = 2;
|
||||
}
|
||||
|
||||
message PunchHoleSent {
|
||||
bytes socket_addr = 1;
|
||||
string id = 2;
|
||||
string relay_server = 3;
|
||||
NatType nat_type = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message RegisterPk {
|
||||
string id = 1;
|
||||
bytes uuid = 2;
|
||||
bytes pk = 3;
|
||||
string old_id = 4;
|
||||
}
|
||||
|
||||
message RegisterPkResponse {
|
||||
enum Result {
|
||||
OK = 0;
|
||||
UUID_MISMATCH = 2;
|
||||
ID_EXISTS = 3;
|
||||
TOO_FREQUENT = 4;
|
||||
INVALID_ID_FORMAT = 5;
|
||||
NOT_SUPPORT = 6;
|
||||
SERVER_ERROR = 7;
|
||||
}
|
||||
Result result = 1;
|
||||
int32 keep_alive = 2;
|
||||
}
|
||||
|
||||
message PunchHoleResponse {
|
||||
bytes socket_addr = 1;
|
||||
bytes pk = 2;
|
||||
enum Failure {
|
||||
ID_NOT_EXIST = 0;
|
||||
OFFLINE = 2;
|
||||
LICENSE_MISMATCH = 3;
|
||||
LICENSE_OVERUSE = 4;
|
||||
}
|
||||
Failure failure = 3;
|
||||
string relay_server = 4;
|
||||
oneof union {
|
||||
NatType nat_type = 5;
|
||||
bool is_local = 6;
|
||||
}
|
||||
string other_failure = 7;
|
||||
int32 feedback = 8;
|
||||
}
|
||||
|
||||
message ConfigUpdate {
|
||||
int32 serial = 1;
|
||||
repeated string rendezvous_servers = 2;
|
||||
}
|
||||
|
||||
message RequestRelay {
|
||||
string id = 1;
|
||||
string uuid = 2;
|
||||
bytes socket_addr = 3;
|
||||
string relay_server = 4;
|
||||
bool secure = 5;
|
||||
string licence_key = 6;
|
||||
ConnType conn_type = 7;
|
||||
string token = 8;
|
||||
}
|
||||
|
||||
message RelayResponse {
|
||||
bytes socket_addr = 1;
|
||||
string uuid = 2;
|
||||
string relay_server = 3;
|
||||
oneof union {
|
||||
string id = 4;
|
||||
bytes pk = 5;
|
||||
}
|
||||
string refuse_reason = 6;
|
||||
string version = 7;
|
||||
int32 feedback = 9;
|
||||
}
|
||||
|
||||
message SoftwareUpdate { string url = 1; }
|
||||
|
||||
// if in same intranet, punch hole won't work both for udp and tcp,
|
||||
// even some router has below connection error if we connect itself,
|
||||
// { kind: Other, error: "could not resolve to any address" },
|
||||
// so we request local address to connect.
|
||||
message FetchLocalAddr {
|
||||
bytes socket_addr = 1;
|
||||
string relay_server = 2;
|
||||
}
|
||||
|
||||
message LocalAddr {
|
||||
bytes socket_addr = 1;
|
||||
bytes local_addr = 2;
|
||||
string relay_server = 3;
|
||||
string id = 4;
|
||||
string version = 5;
|
||||
}
|
||||
|
||||
message PeerDiscovery {
|
||||
string cmd = 1;
|
||||
string mac = 2;
|
||||
string id = 3;
|
||||
string username = 4;
|
||||
string hostname = 5;
|
||||
string platform = 6;
|
||||
string misc = 7;
|
||||
}
|
||||
|
||||
message OnlineRequest {
|
||||
string id = 1;
|
||||
repeated string peers = 2;
|
||||
}
|
||||
|
||||
message OnlineResponse {
|
||||
bytes states = 1;
|
||||
}
|
||||
|
||||
message KeyExchange {
|
||||
repeated bytes keys = 1;
|
||||
}
|
||||
|
||||
message HealthCheck {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message RendezvousMessage {
|
||||
oneof union {
|
||||
RegisterPeer register_peer = 6;
|
||||
RegisterPeerResponse register_peer_response = 7;
|
||||
PunchHoleRequest punch_hole_request = 8;
|
||||
PunchHole punch_hole = 9;
|
||||
PunchHoleSent punch_hole_sent = 10;
|
||||
PunchHoleResponse punch_hole_response = 11;
|
||||
FetchLocalAddr fetch_local_addr = 12;
|
||||
LocalAddr local_addr = 13;
|
||||
ConfigUpdate configure_update = 14;
|
||||
RegisterPk register_pk = 15;
|
||||
RegisterPkResponse register_pk_response = 16;
|
||||
SoftwareUpdate software_update = 17;
|
||||
RequestRelay request_relay = 18;
|
||||
RelayResponse relay_response = 19;
|
||||
TestNatRequest test_nat_request = 20;
|
||||
TestNatResponse test_nat_response = 21;
|
||||
PeerDiscovery peer_discovery = 22;
|
||||
OnlineRequest online_request = 23;
|
||||
OnlineResponse online_response = 24;
|
||||
KeyExchange key_exchange = 25;
|
||||
HealthCheck hc = 26;
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use std::io;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BytesCodec {
|
||||
state: DecodeState,
|
||||
raw: bool,
|
||||
max_packet_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum DecodeState {
|
||||
Head,
|
||||
Data(usize),
|
||||
}
|
||||
|
||||
impl Default for BytesCodec {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BytesCodec {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: DecodeState::Head,
|
||||
raw: false,
|
||||
max_packet_length: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self) {
|
||||
self.raw = true;
|
||||
}
|
||||
|
||||
pub fn set_max_packet_length(&mut self, n: usize) {
|
||||
self.max_packet_length = n;
|
||||
}
|
||||
|
||||
fn decode_head(&mut self, src: &mut BytesMut) -> io::Result<Option<usize>> {
|
||||
if src.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let head_len = ((src[0] & 0x3) + 1) as usize;
|
||||
if src.len() < head_len {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut n = src[0] as usize;
|
||||
if head_len > 1 {
|
||||
n |= (src[1] as usize) << 8;
|
||||
}
|
||||
if head_len > 2 {
|
||||
n |= (src[2] as usize) << 16;
|
||||
}
|
||||
if head_len > 3 {
|
||||
n |= (src[3] as usize) << 24;
|
||||
}
|
||||
n >>= 2;
|
||||
if n > self.max_packet_length {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet"));
|
||||
}
|
||||
src.advance(head_len);
|
||||
src.reserve(n);
|
||||
Ok(Some(n))
|
||||
}
|
||||
|
||||
fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result<Option<BytesMut>> {
|
||||
if src.len() < n {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(src.split_to(n)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for BytesCodec {
|
||||
type Item = BytesMut;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<BytesMut>, io::Error> {
|
||||
if self.raw {
|
||||
if !src.is_empty() {
|
||||
let len = src.len();
|
||||
return Ok(Some(src.split_to(len)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let n = match self.state {
|
||||
DecodeState::Head => match self.decode_head(src)? {
|
||||
Some(n) => {
|
||||
self.state = DecodeState::Data(n);
|
||||
n
|
||||
}
|
||||
None => return Ok(None),
|
||||
},
|
||||
DecodeState::Data(n) => n,
|
||||
};
|
||||
|
||||
match self.decode_data(n, src)? {
|
||||
Some(data) => {
|
||||
self.state = DecodeState::Head;
|
||||
Ok(Some(data))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<Bytes> for BytesCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> {
|
||||
if self.raw {
|
||||
buf.reserve(data.len());
|
||||
buf.put(data);
|
||||
return Ok(());
|
||||
}
|
||||
if data.len() <= 0x3F {
|
||||
buf.put_u8((data.len() << 2) as u8);
|
||||
} else if data.len() <= 0x3FFF {
|
||||
buf.put_u16_le((data.len() << 2) as u16 | 0x1);
|
||||
} else if data.len() <= 0x3FFFFF {
|
||||
let h = (data.len() << 2) as u32 | 0x2;
|
||||
buf.put_u16_le((h & 0xFFFF) as u16);
|
||||
buf.put_u8((h >> 16) as u8);
|
||||
} else if data.len() <= 0x3FFFFFFF {
|
||||
buf.put_u32_le((data.len() << 2) as u32 | 0x3);
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow"));
|
||||
}
|
||||
buf.extend(data);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_codec1() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3F, 1);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
let buf_saved = buf.clone();
|
||||
assert_eq!(buf.len(), 0x3F + 1);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3F);
|
||||
assert_eq!(res[0], 1);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
let mut codec2 = BytesCodec::new();
|
||||
let mut buf2 = BytesMut::new();
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[0..1]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[1..]);
|
||||
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
|
||||
assert_eq!(res.len(), 0x3F);
|
||||
assert_eq!(res[0], 1);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec2() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
assert!(codec.encode("".into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 1);
|
||||
bytes.resize(0x3F + 1, 2);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3F + 2 + 2);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3F + 1);
|
||||
assert_eq!(res[0], 2);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec3() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3F - 1, 3);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3F + 1 - 1);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3F - 1);
|
||||
assert_eq!(res[0], 3);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_codec4() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3FFF, 4);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3FFF + 2);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3FFF);
|
||||
assert_eq!(res[0], 4);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec5() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3FFFFF, 5);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
assert_eq!(buf.len(), 0x3FFFFF + 3);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3FFFFF);
|
||||
assert_eq!(res[0], 5);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codec6() {
|
||||
let mut codec = BytesCodec::new();
|
||||
let mut buf = BytesMut::new();
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.resize(0x3FFFFF + 1, 6);
|
||||
assert!(codec.encode(bytes.into(), &mut buf).is_ok());
|
||||
let buf_saved = buf.clone();
|
||||
assert_eq!(buf.len(), 0x3FFFFF + 4 + 1);
|
||||
if let Ok(Some(res)) = codec.decode(&mut buf) {
|
||||
assert_eq!(res.len(), 0x3FFFFF + 1);
|
||||
assert_eq!(res[0], 6);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
let mut codec2 = BytesCodec::new();
|
||||
let mut buf2 = BytesMut::new();
|
||||
buf2.extend(&buf_saved[0..1]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[1..6]);
|
||||
if let Ok(None) = codec2.decode(&mut buf2) {
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
buf2.extend(&buf_saved[6..]);
|
||||
if let Ok(Some(res)) = codec2.decode(&mut buf2) {
|
||||
assert_eq!(res.len(), 0x3FFFFF + 1);
|
||||
assert_eq!(res[0], 6);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use std::{cell::RefCell, io};
|
||||
use zstd::bulk::Compressor;
|
||||
|
||||
// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(),
|
||||
// which is currently 22. Levels >= 20
|
||||
// Default level is ZSTD_CLEVEL_DEFAULT==3.
|
||||
// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT
|
||||
thread_local! {
|
||||
static COMPRESSOR: RefCell<io::Result<Compressor<'static>>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL));
|
||||
}
|
||||
|
||||
pub fn compress(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
COMPRESSOR.with(|c| {
|
||||
if let Ok(mut c) = c.try_borrow_mut() {
|
||||
match &mut *c {
|
||||
Ok(c) => match c.compress(data) {
|
||||
Ok(res) => out = res,
|
||||
Err(err) => {
|
||||
crate::log::debug!("Failed to compress: {}", err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
crate::log::debug!("Failed to get compressor: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
pub fn decompress(data: &[u8]) -> Vec<u8> {
|
||||
zstd::decode_all(data).unwrap_or_default()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,960 +0,0 @@
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tokio::{fs::File, io::*};
|
||||
|
||||
use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream};
|
||||
// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
|
||||
use crate::{
|
||||
compress::{compress, decompress},
|
||||
config::Config,
|
||||
};
|
||||
|
||||
pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType<FileDirectory> {
|
||||
let mut dir = FileDirectory {
|
||||
path: get_string(path),
|
||||
..Default::default()
|
||||
};
|
||||
#[cfg(windows)]
|
||||
if "/" == &get_string(path) {
|
||||
let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() };
|
||||
for i in 0..32 {
|
||||
if drives & (1 << i) != 0 {
|
||||
let name = format!(
|
||||
"{}:",
|
||||
std::char::from_u32('A' as u32 + i as u32).unwrap_or('A')
|
||||
);
|
||||
dir.entries.push(FileEntry {
|
||||
name,
|
||||
entry_type: FileType::DirDrive.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
return Ok(dir);
|
||||
}
|
||||
for entry in path.read_dir()?.flatten() {
|
||||
let p = entry.path();
|
||||
let name = p
|
||||
.file_name()
|
||||
.map(|p| p.to_str().unwrap_or(""))
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut is_hidden = false;
|
||||
let meta;
|
||||
if let Ok(tmp) = std::fs::symlink_metadata(&p) {
|
||||
meta = tmp;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
// docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants
|
||||
#[cfg(windows)]
|
||||
if meta.file_attributes() & 0x2 != 0 {
|
||||
is_hidden = true;
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
if name.find('.').unwrap_or(usize::MAX) == 0 {
|
||||
is_hidden = true;
|
||||
}
|
||||
if is_hidden && !include_hidden {
|
||||
continue;
|
||||
}
|
||||
let (entry_type, size) = {
|
||||
if p.is_dir() {
|
||||
if meta.file_type().is_symlink() {
|
||||
(FileType::DirLink.into(), 0)
|
||||
} else {
|
||||
(FileType::Dir.into(), 0)
|
||||
}
|
||||
} else if meta.file_type().is_symlink() {
|
||||
(FileType::FileLink.into(), 0)
|
||||
} else {
|
||||
(FileType::File.into(), meta.len())
|
||||
}
|
||||
};
|
||||
let modified_time = meta
|
||||
.modified()
|
||||
.map(|x| {
|
||||
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.map(|x| x.as_secs())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
dir.entries.push(FileEntry {
|
||||
name: get_file_name(&p),
|
||||
entry_type,
|
||||
is_hidden,
|
||||
size,
|
||||
modified_time,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_file_name(p: &Path) -> String {
|
||||
p.file_name()
|
||||
.map(|p| p.to_str().unwrap_or(""))
|
||||
.unwrap_or("")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_string(path: &Path) -> String {
|
||||
path.to_str().unwrap_or("").to_owned()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_path(path: &str) -> PathBuf {
|
||||
Path::new(path).to_path_buf()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_home_as_string() -> String {
|
||||
get_string(&Config::get_home())
|
||||
}
|
||||
|
||||
fn read_dir_recursive(
|
||||
path: &PathBuf,
|
||||
prefix: &Path,
|
||||
include_hidden: bool,
|
||||
) -> ResultType<Vec<FileEntry>> {
|
||||
let mut files = Vec::new();
|
||||
if path.is_dir() {
|
||||
// to-do: symbol link handling, cp the link rather than the content
|
||||
// to-do: file mode, for unix
|
||||
let fd = read_dir(path, include_hidden)?;
|
||||
for entry in fd.entries.iter() {
|
||||
match entry.entry_type.enum_value() {
|
||||
Ok(FileType::File) => {
|
||||
let mut entry = entry.clone();
|
||||
entry.name = get_string(&prefix.join(entry.name));
|
||||
files.push(entry);
|
||||
}
|
||||
Ok(FileType::Dir) => {
|
||||
if let Ok(mut tmp) = read_dir_recursive(
|
||||
&path.join(&entry.name),
|
||||
&prefix.join(&entry.name),
|
||||
include_hidden,
|
||||
) {
|
||||
for entry in tmp.drain(0..) {
|
||||
files.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(files)
|
||||
} else if path.is_file() {
|
||||
let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) {
|
||||
(
|
||||
meta.len(),
|
||||
meta.modified()
|
||||
.map(|x| {
|
||||
x.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.map(|x| x.as_secs())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.unwrap_or(0),
|
||||
)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
files.push(FileEntry {
|
||||
entry_type: FileType::File.into(),
|
||||
size,
|
||||
modified_time,
|
||||
..Default::default()
|
||||
});
|
||||
Ok(files)
|
||||
} else {
|
||||
bail!("Not exists");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<FileEntry>> {
|
||||
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
|
||||
}
|
||||
|
||||
fn read_empty_dirs_recursive(
|
||||
path: &PathBuf,
|
||||
prefix: &Path,
|
||||
include_hidden: bool,
|
||||
) -> ResultType<Vec<FileDirectory>> {
|
||||
let mut dirs = Vec::new();
|
||||
if path.is_dir() {
|
||||
// to-do: symbol link handling, cp the link rather than the content
|
||||
// to-do: file mode, for unix
|
||||
let fd = read_dir(path, include_hidden)?;
|
||||
if fd.entries.is_empty() {
|
||||
dirs.push(fd);
|
||||
} else {
|
||||
for entry in fd.entries.iter() {
|
||||
match entry.entry_type.enum_value() {
|
||||
Ok(FileType::Dir) => {
|
||||
if let Ok(mut tmp) = read_empty_dirs_recursive(
|
||||
&path.join(&entry.name),
|
||||
&prefix.join(&entry.name),
|
||||
include_hidden,
|
||||
) {
|
||||
for entry in tmp.drain(0..) {
|
||||
dirs.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(dirs)
|
||||
} else if path.is_file() {
|
||||
Ok(dirs)
|
||||
} else {
|
||||
bail!("Not exists");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_empty_dirs_recursive(
|
||||
path: &str,
|
||||
include_hidden: bool,
|
||||
) -> ResultType<Vec<FileDirectory>> {
|
||||
read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_file_exists(file_path: &str) -> bool {
|
||||
return Path::new(file_path).exists();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn can_enable_overwrite_detection(version: i64) -> bool {
|
||||
version >= get_version_number("1.1.10")
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransferJob {
|
||||
pub id: i32,
|
||||
pub remote: String,
|
||||
pub path: PathBuf,
|
||||
pub show_hidden: bool,
|
||||
pub is_remote: bool,
|
||||
pub is_last_job: bool,
|
||||
pub file_num: i32,
|
||||
#[serde(skip_serializing)]
|
||||
pub files: Vec<FileEntry>,
|
||||
pub conn_id: i32, // server only
|
||||
|
||||
#[serde(skip_serializing)]
|
||||
file: Option<File>,
|
||||
pub total_size: u64,
|
||||
finished_size: u64,
|
||||
transferred: u64,
|
||||
enable_overwrite_detection: bool,
|
||||
file_confirmed: bool,
|
||||
// indicating the last file is skipped
|
||||
file_skipped: bool,
|
||||
file_is_waiting: bool,
|
||||
default_overwrite_strategy: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct TransferJobMeta {
|
||||
#[serde(default)]
|
||||
pub id: i32,
|
||||
#[serde(default)]
|
||||
pub remote: String,
|
||||
#[serde(default)]
|
||||
pub to: String,
|
||||
#[serde(default)]
|
||||
pub show_hidden: bool,
|
||||
#[serde(default)]
|
||||
pub file_num: i32,
|
||||
#[serde(default)]
|
||||
pub is_remote: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct RemoveJobMeta {
|
||||
#[serde(default)]
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub is_remote: bool,
|
||||
#[serde(default)]
|
||||
pub no_confirm: bool,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_ext(name: &str) -> &str {
|
||||
if let Some(i) = name.rfind('.') {
|
||||
return &name[i + 1..];
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_compressed_file(name: &str) -> bool {
|
||||
let ext = get_ext(name);
|
||||
ext == "xz"
|
||||
|| ext == "gz"
|
||||
|| ext == "zip"
|
||||
|| ext == "7z"
|
||||
|| ext == "rar"
|
||||
|| ext == "bz2"
|
||||
|| ext == "tgz"
|
||||
|| ext == "png"
|
||||
|| ext == "jpg"
|
||||
}
|
||||
|
||||
impl TransferJob {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_write(
|
||||
id: i32,
|
||||
remote: String,
|
||||
path: String,
|
||||
file_num: i32,
|
||||
show_hidden: bool,
|
||||
is_remote: bool,
|
||||
files: Vec<FileEntry>,
|
||||
enable_overwrite_detection: bool,
|
||||
) -> Self {
|
||||
log::info!("new write {}", path);
|
||||
let total_size = files.iter().map(|x| x.size).sum();
|
||||
Self {
|
||||
id,
|
||||
remote,
|
||||
path: get_path(&path),
|
||||
file_num,
|
||||
show_hidden,
|
||||
is_remote,
|
||||
files,
|
||||
total_size,
|
||||
enable_overwrite_detection,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_read(
|
||||
id: i32,
|
||||
remote: String,
|
||||
path: String,
|
||||
file_num: i32,
|
||||
show_hidden: bool,
|
||||
is_remote: bool,
|
||||
enable_overwrite_detection: bool,
|
||||
) -> ResultType<Self> {
|
||||
log::info!("new read {}", path);
|
||||
let files = get_recursive_files(&path, show_hidden)?;
|
||||
let total_size = files.iter().map(|x| x.size).sum();
|
||||
Ok(Self {
|
||||
id,
|
||||
remote,
|
||||
path: get_path(&path),
|
||||
file_num,
|
||||
show_hidden,
|
||||
is_remote,
|
||||
files,
|
||||
total_size,
|
||||
enable_overwrite_detection,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn files(&self) -> &Vec<FileEntry> {
|
||||
&self.files
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_files(&mut self, files: Vec<FileEntry>) {
|
||||
self.files = files;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn total_size(&self) -> u64 {
|
||||
self.total_size
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finished_size(&self) -> u64 {
|
||||
self.finished_size
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn transferred(&self) -> u64 {
|
||||
self.transferred
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn file_num(&self) -> i32 {
|
||||
self.file_num
|
||||
}
|
||||
|
||||
pub fn modify_time(&self) {
|
||||
let file_num = self.file_num as usize;
|
||||
if file_num < self.files.len() {
|
||||
let entry = &self.files[file_num];
|
||||
let path = self.join(&entry.name);
|
||||
let download_path = format!("{}.download", get_string(&path));
|
||||
std::fs::rename(download_path, &path).ok();
|
||||
filetime::set_file_mtime(
|
||||
&path,
|
||||
filetime::FileTime::from_unix_time(entry.modified_time as _, 0),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_download_file(&self) {
|
||||
let file_num = self.file_num as usize;
|
||||
if file_num < self.files.len() {
|
||||
let entry = &self.files[file_num];
|
||||
let path = self.join(&entry.name);
|
||||
let download_path = format!("{}.download", get_string(&path));
|
||||
std::fs::remove_file(download_path).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> {
|
||||
if block.id != self.id {
|
||||
bail!("Wrong id");
|
||||
}
|
||||
let file_num = block.file_num as usize;
|
||||
if file_num >= self.files.len() {
|
||||
bail!("Wrong file number");
|
||||
}
|
||||
if file_num != self.file_num as usize || self.file.is_none() {
|
||||
self.modify_time();
|
||||
if let Some(file) = self.file.as_mut() {
|
||||
file.sync_all().await?;
|
||||
}
|
||||
self.file_num = block.file_num;
|
||||
let entry = &self.files[file_num];
|
||||
let path = self.join(&entry.name);
|
||||
if let Some(p) = path.parent() {
|
||||
std::fs::create_dir_all(p).ok();
|
||||
}
|
||||
let path = format!("{}.download", get_string(&path));
|
||||
self.file = Some(File::create(&path).await?);
|
||||
}
|
||||
if block.compressed {
|
||||
let tmp = decompress(&block.data);
|
||||
self.file
|
||||
.as_mut()
|
||||
.ok_or(anyhow!("file is None"))?
|
||||
.write_all(&tmp)
|
||||
.await?;
|
||||
self.finished_size += tmp.len() as u64;
|
||||
} else {
|
||||
self.file
|
||||
.as_mut()
|
||||
.ok_or(anyhow!("file is None"))?
|
||||
.write_all(&block.data)
|
||||
.await?;
|
||||
self.finished_size += block.data.len() as u64;
|
||||
}
|
||||
self.transferred += block.data.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn join(&self, name: &str) -> PathBuf {
|
||||
if name.is_empty() {
|
||||
self.path.clone()
|
||||
} else {
|
||||
self.path.join(name)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read(&mut self, stream: &mut Stream) -> ResultType<Option<FileTransferBlock>> {
|
||||
let file_num = self.file_num as usize;
|
||||
if file_num >= self.files.len() {
|
||||
self.file.take();
|
||||
return Ok(None);
|
||||
}
|
||||
let name = &self.files[file_num].name;
|
||||
if self.file.is_none() {
|
||||
match File::open(self.join(name)).await {
|
||||
Ok(file) => {
|
||||
self.file = Some(file);
|
||||
self.file_confirmed = false;
|
||||
self.file_is_waiting = false;
|
||||
}
|
||||
Err(err) => {
|
||||
self.file_num += 1;
|
||||
self.file_confirmed = false;
|
||||
self.file_is_waiting = false;
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.enable_overwrite_detection && !self.file_confirmed() {
|
||||
if !self.file_is_waiting() {
|
||||
self.send_current_digest(stream).await?;
|
||||
self.set_file_is_waiting(true);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
const BUF_SIZE: usize = 128 * 1024;
|
||||
let mut buf: Vec<u8> = vec![0; BUF_SIZE];
|
||||
let mut compressed = false;
|
||||
let mut offset: usize = 0;
|
||||
loop {
|
||||
match self
|
||||
.file
|
||||
.as_mut()
|
||||
.ok_or(anyhow!("file is None"))?
|
||||
.read(&mut buf[offset..])
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
self.file_num += 1;
|
||||
self.file = None;
|
||||
self.file_confirmed = false;
|
||||
self.file_is_waiting = false;
|
||||
return Err(err.into());
|
||||
}
|
||||
Ok(n) => {
|
||||
offset += n;
|
||||
if n == 0 || offset == BUF_SIZE {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { buf.set_len(offset) };
|
||||
if offset == 0 {
|
||||
self.file_num += 1;
|
||||
self.file = None;
|
||||
self.file_confirmed = false;
|
||||
self.file_is_waiting = false;
|
||||
} else {
|
||||
self.finished_size += offset as u64;
|
||||
if !is_compressed_file(name) {
|
||||
let tmp = compress(&buf);
|
||||
if tmp.len() < buf.len() {
|
||||
buf = tmp;
|
||||
compressed = true;
|
||||
}
|
||||
}
|
||||
self.transferred += buf.len() as u64;
|
||||
}
|
||||
Ok(Some(FileTransferBlock {
|
||||
id: self.id,
|
||||
file_num: file_num as _,
|
||||
data: buf.into(),
|
||||
compressed,
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> {
|
||||
let mut msg = Message::new();
|
||||
let mut resp = FileResponse::new();
|
||||
let meta = self
|
||||
.file
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("file is None"))?
|
||||
.metadata()
|
||||
.await?;
|
||||
let last_modified = meta
|
||||
.modified()?
|
||||
.duration_since(SystemTime::UNIX_EPOCH)?
|
||||
.as_secs();
|
||||
resp.set_digest(FileTransferDigest {
|
||||
id: self.id,
|
||||
file_num: self.file_num,
|
||||
last_modified,
|
||||
file_size: meta.len(),
|
||||
..Default::default()
|
||||
});
|
||||
msg.set_file_response(resp);
|
||||
stream.send(&msg).await?;
|
||||
log::info!(
|
||||
"id: {}, file_num: {}, digest message is sent. waiting for confirm. msg: {:?}",
|
||||
self.id,
|
||||
self.file_num,
|
||||
msg
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option<bool>) {
|
||||
self.default_overwrite_strategy = overwrite_strategy;
|
||||
}
|
||||
|
||||
pub fn default_overwrite_strategy(&self) -> Option<bool> {
|
||||
self.default_overwrite_strategy
|
||||
}
|
||||
|
||||
pub fn set_file_confirmed(&mut self, file_confirmed: bool) {
|
||||
log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed);
|
||||
self.file_confirmed = file_confirmed;
|
||||
self.file_skipped = false;
|
||||
}
|
||||
|
||||
pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) {
|
||||
self.file_is_waiting = file_is_waiting;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn file_is_waiting(&self) -> bool {
|
||||
self.file_is_waiting
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn file_confirmed(&self) -> bool {
|
||||
self.file_confirmed
|
||||
}
|
||||
|
||||
/// Indicating whether the last file is skipped
|
||||
#[inline]
|
||||
pub fn file_skipped(&self) -> bool {
|
||||
self.file_skipped
|
||||
}
|
||||
|
||||
/// Indicating whether the whole task is skipped
|
||||
#[inline]
|
||||
pub fn job_skipped(&self) -> bool {
|
||||
self.file_skipped() && self.files.len() == 1
|
||||
}
|
||||
|
||||
/// Check whether the job is completed after `read` returns `None`
|
||||
/// This is a helper function which gives additional lifecycle when the job reads `None`.
|
||||
/// If returns `true`, it means we can delete the job automatically. `False` otherwise.
|
||||
///
|
||||
/// [`Note`]
|
||||
/// Conditions:
|
||||
/// 1. Files are not waiting for confirmation by peers.
|
||||
#[inline]
|
||||
pub fn job_completed(&self) -> bool {
|
||||
// has no error, Condition 2
|
||||
!self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting)
|
||||
}
|
||||
|
||||
/// Get job error message, useful for getting status when job had finished
|
||||
pub fn job_error(&self) -> Option<String> {
|
||||
if self.job_skipped() {
|
||||
return Some("skipped".to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_file_skipped(&mut self) -> bool {
|
||||
log::debug!("skip file {} in job {}", self.file_num, self.id);
|
||||
self.file.take();
|
||||
self.set_file_confirmed(false);
|
||||
self.set_file_is_waiting(false);
|
||||
self.file_num += 1;
|
||||
self.file_skipped = true;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool {
|
||||
if self.file_num() != r.file_num {
|
||||
log::info!("file num truncated, ignoring");
|
||||
} else {
|
||||
match r.union {
|
||||
Some(file_transfer_send_confirm_request::Union::Skip(s)) => {
|
||||
if s {
|
||||
self.set_file_skipped();
|
||||
} else {
|
||||
self.set_file_confirmed(true);
|
||||
}
|
||||
}
|
||||
Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => {
|
||||
self.set_file_confirmed(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn gen_meta(&self) -> TransferJobMeta {
|
||||
TransferJobMeta {
|
||||
id: self.id,
|
||||
remote: self.remote.to_string(),
|
||||
to: self.path.to_string_lossy().to_string(),
|
||||
file_num: self.file_num,
|
||||
show_hidden: self.show_hidden,
|
||||
is_remote: self.is_remote,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_error<T: std::string::ToString>(id: i32, err: T, file_num: i32) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_error(FileTransferError {
|
||||
id,
|
||||
error: err.to_string(),
|
||||
file_num,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_dir(id: i32, path: String, files: Vec<FileEntry>) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_dir(FileDirectory {
|
||||
id,
|
||||
path,
|
||||
entries: files,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_block(block: FileTransferBlock) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_block(block);
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
|
||||
let mut msg_out = Message::new();
|
||||
let mut action = FileAction::new();
|
||||
action.set_send_confirm(r);
|
||||
msg_out.set_file_action(action);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_receive(
|
||||
id: i32,
|
||||
path: String,
|
||||
file_num: i32,
|
||||
files: Vec<FileEntry>,
|
||||
total_size: u64,
|
||||
) -> Message {
|
||||
let mut action = FileAction::new();
|
||||
action.set_receive(FileTransferReceiveRequest {
|
||||
id,
|
||||
path,
|
||||
files,
|
||||
file_num,
|
||||
total_size,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_action(action);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message {
|
||||
log::info!("new send: {}, id: {}", path, id);
|
||||
let mut action = FileAction::new();
|
||||
action.set_send(FileTransferSendRequest {
|
||||
id,
|
||||
path,
|
||||
include_hidden,
|
||||
file_num,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_action(action);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn new_done(id: i32, file_num: i32) -> Message {
|
||||
let mut resp = FileResponse::new();
|
||||
resp.set_done(FileTransferDone {
|
||||
id,
|
||||
file_num,
|
||||
..Default::default()
|
||||
});
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_file_response(resp);
|
||||
msg_out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remove_job(id: i32, jobs: &mut Vec<TransferJob>) {
|
||||
*jobs = jobs.drain(0..).filter(|x| x.id() != id).collect();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
|
||||
jobs.iter_mut().find(|x| x.id() == id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> {
|
||||
jobs.iter().find(|x| x.id() == id)
|
||||
}
|
||||
|
||||
pub async fn handle_read_jobs(
|
||||
jobs: &mut Vec<TransferJob>,
|
||||
stream: &mut crate::Stream,
|
||||
) -> ResultType<String> {
|
||||
let mut job_log = Default::default();
|
||||
let mut finished = Vec::new();
|
||||
for job in jobs.iter_mut() {
|
||||
if job.is_last_job {
|
||||
continue;
|
||||
}
|
||||
match job.read(stream).await {
|
||||
Err(err) => {
|
||||
stream
|
||||
.send(&new_error(job.id(), err, job.file_num()))
|
||||
.await?;
|
||||
}
|
||||
Ok(Some(block)) => {
|
||||
stream.send(&new_block(block)).await?;
|
||||
}
|
||||
Ok(None) => {
|
||||
if job.job_completed() {
|
||||
job_log = serialize_transfer_job(job, true, false, "");
|
||||
finished.push(job.id());
|
||||
match job.job_error() {
|
||||
Some(err) => {
|
||||
job_log = serialize_transfer_job(job, false, false, &err);
|
||||
stream
|
||||
.send(&new_error(job.id(), err, job.file_num()))
|
||||
.await?
|
||||
}
|
||||
None => stream.send(&new_done(job.id(), job.file_num())).await?,
|
||||
}
|
||||
} else {
|
||||
// waiting confirmation.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in finished {
|
||||
remove_job(id, jobs);
|
||||
}
|
||||
Ok(job_log)
|
||||
}
|
||||
|
||||
pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
|
||||
let fd = read_dir(path, true)?;
|
||||
for entry in fd.entries.iter() {
|
||||
match entry.entry_type.enum_value() {
|
||||
Ok(FileType::Dir) => {
|
||||
remove_all_empty_dir(&path.join(&entry.name)).ok();
|
||||
}
|
||||
Ok(FileType::DirLink) | Ok(FileType::FileLink) => {
|
||||
std::fs::remove_file(path.join(&entry.name)).ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
std::fs::remove_dir(path).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remove_file(file: &str) -> ResultType<()> {
|
||||
std::fs::remove_file(get_path(file))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn create_dir(dir: &str) -> ResultType<()> {
|
||||
std::fs::create_dir_all(get_path(dir))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> {
|
||||
let path = std::path::Path::new(&path);
|
||||
if path.exists() {
|
||||
let dir = path
|
||||
.parent()
|
||||
.ok_or(anyhow!("Parent directoy of {path:?} not exists"))?;
|
||||
let new_path = dir.join(&new_name);
|
||||
std::fs::rename(&path, &new_path)?;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("{path:?} not exists");
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn transform_windows_path(entries: &mut Vec<FileEntry>) {
|
||||
for entry in entries {
|
||||
entry.name = entry.name.replace('\\', "/");
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DigestCheckResult {
|
||||
IsSame,
|
||||
NeedConfirm(FileTransferDigest),
|
||||
NoSuchFile,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_write_need_confirmation(
|
||||
file_path: &str,
|
||||
digest: &FileTransferDigest,
|
||||
) -> ResultType<DigestCheckResult> {
|
||||
let path = Path::new(file_path);
|
||||
if path.exists() && path.is_file() {
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let modified_time = metadata.modified()?;
|
||||
let remote_mt = Duration::from_secs(digest.last_modified);
|
||||
let local_mt = modified_time.duration_since(UNIX_EPOCH)?;
|
||||
// [Note]
|
||||
// We decide to give the decision whether to override the existing file to users,
|
||||
// which obey the behavior of the file manager in our system.
|
||||
let mut is_identical = false;
|
||||
if remote_mt == local_mt && digest.file_size == metadata.len() {
|
||||
is_identical = true;
|
||||
}
|
||||
Ok(DigestCheckResult::NeedConfirm(FileTransferDigest {
|
||||
id: digest.id,
|
||||
file_num: digest.file_num,
|
||||
last_modified: local_mt.as_secs(),
|
||||
file_size: metadata.len(),
|
||||
is_identical,
|
||||
..Default::default()
|
||||
}))
|
||||
} else {
|
||||
Ok(DigestCheckResult::NoSuchFile)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String {
|
||||
let mut v = vec![];
|
||||
for job in jobs {
|
||||
let value = serde_json::to_value(job).unwrap_or_default();
|
||||
v.push(value);
|
||||
}
|
||||
serde_json::to_string(&v).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String {
|
||||
let mut value = serde_json::to_value(job).unwrap_or_default();
|
||||
value["done"] = json!(done);
|
||||
value["cancel"] = json!(cancel);
|
||||
value["error"] = json!(error);
|
||||
serde_json::to_string(&value).unwrap_or_default()
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use std::{fmt, slice::Iter, str::FromStr};
|
||||
|
||||
use crate::protos::message::KeyboardMode;
|
||||
|
||||
impl fmt::Display for KeyboardMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
KeyboardMode::Legacy => write!(f, "legacy"),
|
||||
KeyboardMode::Map => write!(f, "map"),
|
||||
KeyboardMode::Translate => write!(f, "translate"),
|
||||
KeyboardMode::Auto => write!(f, "auto"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for KeyboardMode {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"legacy" => Ok(KeyboardMode::Legacy),
|
||||
"map" => Ok(KeyboardMode::Map),
|
||||
"translate" => Ok(KeyboardMode::Translate),
|
||||
"auto" => Ok(KeyboardMode::Auto),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardMode {
|
||||
pub fn iter() -> Iter<'static, KeyboardMode> {
|
||||
static KEYBOARD_MODES: [KeyboardMode; 4] = [
|
||||
KeyboardMode::Legacy,
|
||||
KeyboardMode::Map,
|
||||
KeyboardMode::Translate,
|
||||
KeyboardMode::Auto,
|
||||
];
|
||||
KEYBOARD_MODES.iter()
|
||||
}
|
||||
}
|
||||
@@ -1,500 +0,0 @@
|
||||
pub mod compress;
|
||||
pub mod platform;
|
||||
pub mod protos;
|
||||
pub use bytes;
|
||||
use config::Config;
|
||||
pub use futures;
|
||||
pub use protobuf;
|
||||
pub use protos::message as message_proto;
|
||||
pub use protos::rendezvous as rendezvous_proto;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
path::Path,
|
||||
time::{self, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
pub use tokio;
|
||||
pub use tokio_util;
|
||||
pub mod proxy;
|
||||
pub mod socket_client;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
pub use env_logger;
|
||||
pub use log;
|
||||
pub mod bytes_codec;
|
||||
pub use anyhow::{self, bail};
|
||||
pub use futures_util;
|
||||
pub mod config;
|
||||
pub mod fs;
|
||||
pub mod mem;
|
||||
pub use lazy_static;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use mac_address;
|
||||
pub use rand;
|
||||
pub use regex;
|
||||
pub use sodiumoxide;
|
||||
pub use tokio_socks;
|
||||
pub use tokio_socks::IntoTargetAddr;
|
||||
pub use tokio_socks::TargetAddr;
|
||||
pub mod password_security;
|
||||
pub use chrono;
|
||||
pub use directories_next;
|
||||
pub use libc;
|
||||
pub mod keyboard;
|
||||
pub use base64;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use dlopen;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub use machine_uid;
|
||||
pub use serde_derive;
|
||||
pub use serde_json;
|
||||
pub use sysinfo;
|
||||
pub use thiserror;
|
||||
pub use toml;
|
||||
pub use uuid;
|
||||
|
||||
pub type Stream = tcp::FramedStream;
|
||||
pub type SessionID = uuid::Uuid;
|
||||
|
||||
#[inline]
|
||||
pub async fn sleep(sec: f32) {
|
||||
tokio::time::sleep(time::Duration::from_secs_f32(sec)).await;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! allow_err {
|
||||
($e:expr) => {
|
||||
if let Err(err) = $e {
|
||||
log::debug!(
|
||||
"{:?}, {}:{}:{}:{}",
|
||||
err,
|
||||
module_path!(),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
($e:expr, $($arg:tt)*) => {
|
||||
if let Err(err) = $e {
|
||||
log::debug!(
|
||||
"{:?}, {}, {}:{}:{}:{}",
|
||||
err,
|
||||
format_args!($($arg)*),
|
||||
module_path!(),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn timeout<T: std::future::Future>(ms: u64, future: T) -> tokio::time::Timeout<T> {
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), future)
|
||||
}
|
||||
|
||||
pub type ResultType<F, E = anyhow::Error> = anyhow::Result<F, E>;
|
||||
|
||||
/// Certain router and firewalls scan the packet and if they
|
||||
/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address
|
||||
|
||||
pub struct AddrMangle();
|
||||
|
||||
#[inline]
|
||||
pub fn try_into_v4(addr: SocketAddr) -> SocketAddr {
|
||||
match addr {
|
||||
SocketAddr::V6(v6) if !addr.ip().is_loopback() => {
|
||||
if let Some(v4) = v6.ip().to_ipv4() {
|
||||
SocketAddr::new(IpAddr::V4(v4), addr.port())
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
}
|
||||
_ => addr,
|
||||
}
|
||||
}
|
||||
|
||||
impl AddrMangle {
|
||||
pub fn encode(addr: SocketAddr) -> Vec<u8> {
|
||||
// not work with [:1]:<port>
|
||||
let addr = try_into_v4(addr);
|
||||
match addr {
|
||||
SocketAddr::V4(addr_v4) => {
|
||||
let tm = (SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or(std::time::Duration::ZERO)
|
||||
.as_micros() as u32) as u128;
|
||||
let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128;
|
||||
let port = addr.port() as u128;
|
||||
let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF));
|
||||
let bytes = v.to_le_bytes();
|
||||
let mut n_padding = 0;
|
||||
for i in bytes.iter().rev() {
|
||||
if i == &0u8 {
|
||||
n_padding += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bytes[..(16 - n_padding)].to_vec()
|
||||
}
|
||||
SocketAddr::V6(addr_v6) => {
|
||||
let mut x = addr_v6.ip().octets().to_vec();
|
||||
let port: [u8; 2] = addr_v6.port().to_le_bytes();
|
||||
x.push(port[0]);
|
||||
x.push(port[1]);
|
||||
x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> SocketAddr {
|
||||
use std::convert::TryInto;
|
||||
|
||||
if bytes.len() > 16 {
|
||||
if bytes.len() != 18 {
|
||||
return Config::get_any_listen_addr(false);
|
||||
}
|
||||
let tmp: [u8; 2] = bytes[16..].try_into().unwrap_or_default();
|
||||
let port = u16::from_le_bytes(tmp);
|
||||
let tmp: [u8; 16] = bytes[..16].try_into().unwrap_or_default();
|
||||
let ip = std::net::Ipv6Addr::from(tmp);
|
||||
return SocketAddr::new(IpAddr::V6(ip), port);
|
||||
}
|
||||
let mut padded = [0u8; 16];
|
||||
padded[..bytes.len()].copy_from_slice(bytes);
|
||||
let number = u128::from_le_bytes(padded);
|
||||
let tm = (number >> 17) & (u32::max_value() as u128);
|
||||
let ip = (((number >> 49) - tm) as u32).to_le_bytes();
|
||||
let port = (number & 0xFFFFFF) - (tm & 0xFFFF);
|
||||
SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
|
||||
port as u16,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_version_from_url(url: &str) -> String {
|
||||
let n = url.chars().count();
|
||||
let a = url.chars().rev().position(|x| x == '-');
|
||||
if let Some(a) = a {
|
||||
let b = url.chars().rev().position(|x| x == '.');
|
||||
if let Some(b) = b {
|
||||
if a > b {
|
||||
if url
|
||||
.chars()
|
||||
.skip(n - b)
|
||||
.collect::<String>()
|
||||
.parse::<i32>()
|
||||
.is_ok()
|
||||
{
|
||||
return url.chars().skip(n - a).collect();
|
||||
} else {
|
||||
return url.chars().skip(n - a).take(a - b - 1).collect();
|
||||
}
|
||||
} else {
|
||||
return url.chars().skip(n - a).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
pub fn gen_version() {
|
||||
println!("cargo:rerun-if-changed=Cargo.toml");
|
||||
use std::io::prelude::*;
|
||||
let mut file = File::create("./src/version.rs").unwrap();
|
||||
for line in read_lines("Cargo.toml").unwrap().flatten() {
|
||||
let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect();
|
||||
if ab.len() == 2 && ab[0] == "version" {
|
||||
file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes())
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// generate build date
|
||||
let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M"));
|
||||
file.write_all(
|
||||
format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(),
|
||||
)
|
||||
.ok();
|
||||
file.sync_all().ok();
|
||||
}
|
||||
|
||||
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(filename)?;
|
||||
Ok(io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
pub fn is_valid_custom_id(id: &str) -> bool {
|
||||
regex::Regex::new(r"^[a-zA-Z]\w{5,15}$")
|
||||
.unwrap()
|
||||
.is_match(id)
|
||||
}
|
||||
|
||||
// Support 1.1.10-1, the number after - is a patch version.
|
||||
pub fn get_version_number(v: &str) -> i64 {
|
||||
let mut versions = v.split('-');
|
||||
|
||||
let mut n = 0;
|
||||
|
||||
// The first part is the version number.
|
||||
// 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10
|
||||
// to leave space for patch version.
|
||||
if let Some(v) = versions.next() {
|
||||
let mut last = 0;
|
||||
for x in v.split('.') {
|
||||
last = x.parse::<i64>().unwrap_or(0);
|
||||
n = n * 1000 + last;
|
||||
}
|
||||
n -= last;
|
||||
n += last * 10;
|
||||
}
|
||||
|
||||
if let Some(v) = versions.next() {
|
||||
n += v.parse::<i64>().unwrap_or(0);
|
||||
}
|
||||
|
||||
// Ignore the rest
|
||||
|
||||
n
|
||||
}
|
||||
|
||||
pub fn get_modified_time(path: &std::path::Path) -> SystemTime {
|
||||
std::fs::metadata(path)
|
||||
.map(|m| m.modified().unwrap_or(UNIX_EPOCH))
|
||||
.unwrap_or(UNIX_EPOCH)
|
||||
}
|
||||
|
||||
pub fn get_created_time(path: &std::path::Path) -> SystemTime {
|
||||
std::fs::metadata(path)
|
||||
.map(|m| m.created().unwrap_or(UNIX_EPOCH))
|
||||
.unwrap_or(UNIX_EPOCH)
|
||||
}
|
||||
|
||||
pub fn get_exe_time() -> SystemTime {
|
||||
std::env::current_exe().map_or(UNIX_EPOCH, |path| {
|
||||
let m = get_modified_time(&path);
|
||||
let c = get_created_time(&path);
|
||||
if m > c {
|
||||
m
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_uuid() -> Vec<u8> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if let Ok(id) = machine_uid::get() {
|
||||
return id.into();
|
||||
}
|
||||
Config::get_key_pair().1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_time() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0) as _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_ipv4_str(id: &str) -> bool {
|
||||
if let Ok(reg) = regex::Regex::new(
|
||||
r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$",
|
||||
) {
|
||||
reg.is_match(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_ipv6_str(id: &str) -> bool {
|
||||
if let Ok(reg) = regex::Regex::new(
|
||||
r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$",
|
||||
) {
|
||||
reg.is_match(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_ip_str(id: &str) -> bool {
|
||||
is_ipv4_str(id) || is_ipv6_str(id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_domain_port_str(id: &str) -> bool {
|
||||
// modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname.
|
||||
// according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700,
|
||||
// there is no digits in TLD, and length is 2~63.
|
||||
if let Ok(reg) = regex::Regex::new(
|
||||
r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$",
|
||||
) {
|
||||
reg.is_match(id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHandle> {
|
||||
static INIT: std::sync::Once = std::sync::Once::new();
|
||||
#[allow(unused_mut)]
|
||||
let mut logger_holder: Option<flexi_logger::LoggerHandle> = None;
|
||||
INIT.call_once(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use env_logger::*;
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
// https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write
|
||||
// though async logger more efficient, but it also causes more problems, disable it for now
|
||||
let mut path = config::Config::log_path();
|
||||
#[cfg(target_os = "android")]
|
||||
if !config::Config::get_home().exists() {
|
||||
return;
|
||||
}
|
||||
if !_name.is_empty() {
|
||||
path.push(_name);
|
||||
}
|
||||
use flexi_logger::*;
|
||||
if let Ok(x) = Logger::try_with_env_or_str("debug") {
|
||||
logger_holder = x
|
||||
.log_to_file(FileSpec::default().directory(path))
|
||||
.write_mode(if _is_async {
|
||||
WriteMode::Async
|
||||
} else {
|
||||
WriteMode::Direct
|
||||
})
|
||||
.format(opt_format)
|
||||
.rotate(
|
||||
Criterion::Age(Age::Day),
|
||||
Naming::Timestamps,
|
||||
Cleanup::KeepLogFiles(31),
|
||||
)
|
||||
.start()
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
logger_holder
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mangle() {
|
||||
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116));
|
||||
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
|
||||
|
||||
let addr = "[2001:db8::1]:8080".parse::<SocketAddr>().unwrap();
|
||||
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
|
||||
|
||||
let addr = "[2001:db8:ff::1111]:80".parse::<SocketAddr>().unwrap();
|
||||
assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_err() {
|
||||
allow_err!(Err("test err") as Result<(), &str>);
|
||||
allow_err!(
|
||||
Err("test err with msg") as Result<(), &str>,
|
||||
"prompt {}",
|
||||
"failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipv6() {
|
||||
assert!(is_ipv6_str("1:2:3"));
|
||||
assert!(is_ipv6_str("[ab:2:3]:12"));
|
||||
assert!(is_ipv6_str("[ABEF:2a:3]:12"));
|
||||
assert!(!is_ipv6_str("[ABEG:2a:3]:12"));
|
||||
assert!(!is_ipv6_str("1[ab:2:3]:12"));
|
||||
assert!(!is_ipv6_str("1.1.1.1"));
|
||||
assert!(is_ip_str("1.1.1.1"));
|
||||
assert!(!is_ipv6_str("1:2:"));
|
||||
assert!(is_ipv6_str("1:2::0"));
|
||||
assert!(is_ipv6_str("[1:2::0]:1"));
|
||||
assert!(!is_ipv6_str("[1:2::0]:"));
|
||||
assert!(!is_ipv6_str("1:2::0]:1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ipv4() {
|
||||
assert!(is_ipv4_str("1.2.3.4"));
|
||||
assert!(is_ipv4_str("1.2.3.4:90"));
|
||||
assert!(is_ipv4_str("192.168.0.1"));
|
||||
assert!(is_ipv4_str("0.0.0.0"));
|
||||
assert!(is_ipv4_str("255.255.255.255"));
|
||||
assert!(!is_ipv4_str("256.0.0.0"));
|
||||
assert!(!is_ipv4_str("256.256.256.256"));
|
||||
assert!(!is_ipv4_str("1:2:"));
|
||||
assert!(!is_ipv4_str("192.168.0.256"));
|
||||
assert!(!is_ipv4_str("192.168.0.1/24"));
|
||||
assert!(!is_ipv4_str("192.168.0."));
|
||||
assert!(!is_ipv4_str("192.168..1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hostname_port() {
|
||||
assert!(!is_domain_port_str("a:12"));
|
||||
assert!(!is_domain_port_str("a.b.c:12"));
|
||||
assert!(is_domain_port_str("test.com:12"));
|
||||
assert!(is_domain_port_str("test-UPPER.com:12"));
|
||||
assert!(is_domain_port_str("some-other.domain.com:12"));
|
||||
assert!(!is_domain_port_str("under_score:12"));
|
||||
assert!(!is_domain_port_str("a@bc:12"));
|
||||
assert!(!is_domain_port_str("1.1.1.1:12"));
|
||||
assert!(!is_domain_port_str("1.2.3:12"));
|
||||
assert!(!is_domain_port_str("1.2.3.45:12"));
|
||||
assert!(!is_domain_port_str("a.b.c:123456"));
|
||||
assert!(!is_domain_port_str("---:12"));
|
||||
assert!(!is_domain_port_str(".:12"));
|
||||
// todo: should we also check for these edge cases?
|
||||
// out-of-range port
|
||||
assert!(is_domain_port_str("test.com:0"));
|
||||
assert!(is_domain_port_str("test.com:98989"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mangle2() {
|
||||
let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap();
|
||||
let addr_v4 = "127.0.0.1:8080".parse().unwrap();
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4);
|
||||
assert_eq!(
|
||||
AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())),
|
||||
addr_v4
|
||||
);
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4);
|
||||
let addr_v6 = "[ef::fe]:8080".parse().unwrap();
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
|
||||
let addr_v6 = "[::1]:8080".parse().unwrap();
|
||||
assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_version_number() {
|
||||
assert_eq!(get_version_number("1.1.10"), 1001100);
|
||||
assert_eq!(get_version_number("1.1.10-1"), 1001101);
|
||||
assert_eq!(get_version_number("1.1.11-1"), 1001111);
|
||||
assert_eq!(get_version_number("1.2.3"), 1002030);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/// SAFETY: the returned Vec must not be resized or reserverd
|
||||
pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec<u8> {
|
||||
use std::alloc::*;
|
||||
|
||||
let layout =
|
||||
Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2");
|
||||
unsafe {
|
||||
let ptr = alloc(layout);
|
||||
if ptr.is_null() {
|
||||
panic!("failed to allocate {} bytes", cap);
|
||||
}
|
||||
Vec::from_raw_parts(ptr, 0, cap)
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use sodiumoxide::base64;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPORARY_PASSWORD:Arc<RwLock<String>> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length())));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum VerificationMethod {
|
||||
OnlyUseTemporaryPassword,
|
||||
OnlyUsePermanentPassword,
|
||||
UseBothPasswords,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ApproveMode {
|
||||
Both,
|
||||
Password,
|
||||
Click,
|
||||
}
|
||||
|
||||
// Should only be called in server
|
||||
pub fn update_temporary_password() {
|
||||
*TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length());
|
||||
}
|
||||
|
||||
// Should only be called in server
|
||||
pub fn temporary_password() -> String {
|
||||
TEMPORARY_PASSWORD.read().unwrap().clone()
|
||||
}
|
||||
|
||||
fn verification_method() -> VerificationMethod {
|
||||
let method = Config::get_option("verification-method");
|
||||
if method == "use-temporary-password" {
|
||||
VerificationMethod::OnlyUseTemporaryPassword
|
||||
} else if method == "use-permanent-password" {
|
||||
VerificationMethod::OnlyUsePermanentPassword
|
||||
} else {
|
||||
VerificationMethod::UseBothPasswords // default
|
||||
}
|
||||
}
|
||||
|
||||
pub fn temporary_password_length() -> usize {
|
||||
let length = Config::get_option("temporary-password-length");
|
||||
if length == "8" {
|
||||
8
|
||||
} else if length == "10" {
|
||||
10
|
||||
} else {
|
||||
6 // default
|
||||
}
|
||||
}
|
||||
|
||||
pub fn temporary_enabled() -> bool {
|
||||
verification_method() != VerificationMethod::OnlyUsePermanentPassword
|
||||
}
|
||||
|
||||
pub fn permanent_enabled() -> bool {
|
||||
verification_method() != VerificationMethod::OnlyUseTemporaryPassword
|
||||
}
|
||||
|
||||
pub fn has_valid_password() -> bool {
|
||||
temporary_enabled() && !temporary_password().is_empty()
|
||||
|| permanent_enabled() && !Config::get_permanent_password().is_empty()
|
||||
}
|
||||
|
||||
pub fn approve_mode() -> ApproveMode {
|
||||
let mode = Config::get_option("approve-mode");
|
||||
if mode == "password" {
|
||||
ApproveMode::Password
|
||||
} else if mode == "click" {
|
||||
ApproveMode::Click
|
||||
} else {
|
||||
ApproveMode::Both
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_cm() -> bool {
|
||||
approve_mode() == ApproveMode::Password
|
||||
&& verification_method() == VerificationMethod::OnlyUsePermanentPassword
|
||||
&& crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm"))
|
||||
}
|
||||
|
||||
const VERSION_LEN: usize = 2;
|
||||
|
||||
pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String {
|
||||
if decrypt_str_or_original(s, version).1 {
|
||||
log::error!("Duplicate encryption!");
|
||||
return s.to_owned();
|
||||
}
|
||||
if s.chars().count() > max_len {
|
||||
return String::default();
|
||||
}
|
||||
if version == "00" {
|
||||
if let Ok(s) = encrypt(s.as_bytes()) {
|
||||
return version.to_owned() + &s;
|
||||
}
|
||||
}
|
||||
s.to_owned()
|
||||
}
|
||||
|
||||
// String: password
|
||||
// bool: whether decryption is successful
|
||||
// bool: whether should store to re-encrypt when load
|
||||
// note: s.len() return length in bytes, s.chars().count() return char count
|
||||
// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars
|
||||
pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
|
||||
if s.len() > VERSION_LEN {
|
||||
if s.starts_with("00") {
|
||||
if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) {
|
||||
return (
|
||||
String::from_utf8_lossy(&v).to_string(),
|
||||
true,
|
||||
"00" != current_version,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(s.to_owned(), false, !s.is_empty())
|
||||
}
|
||||
|
||||
pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u8> {
|
||||
if decrypt_vec_or_original(v, version).1 {
|
||||
log::error!("Duplicate encryption!");
|
||||
return v.to_owned();
|
||||
}
|
||||
if v.len() > max_len {
|
||||
return vec![];
|
||||
}
|
||||
if version == "00" {
|
||||
if let Ok(s) = encrypt(v) {
|
||||
let mut version = version.to_owned().into_bytes();
|
||||
version.append(&mut s.into_bytes());
|
||||
return version;
|
||||
}
|
||||
}
|
||||
v.to_owned()
|
||||
}
|
||||
|
||||
// Vec<u8>: password
|
||||
// bool: whether decryption is successful
|
||||
// bool: whether should store to re-encrypt when load
|
||||
pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, bool, bool) {
|
||||
if v.len() > VERSION_LEN {
|
||||
let version = String::from_utf8_lossy(&v[..VERSION_LEN]);
|
||||
if version == "00" {
|
||||
if let Ok(v) = decrypt(&v[VERSION_LEN..]) {
|
||||
return (v, true, version != current_version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(v.to_owned(), false, !v.is_empty())
|
||||
}
|
||||
|
||||
fn encrypt(v: &[u8]) -> Result<String, ()> {
|
||||
if !v.is_empty() {
|
||||
symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
|
||||
if !v.is_empty() {
|
||||
base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
|
||||
use sodiumoxide::crypto::secretbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
let mut keybuf = crate::get_uuid();
|
||||
keybuf.resize(secretbox::KEYBYTES, 0);
|
||||
let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?);
|
||||
let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]);
|
||||
|
||||
if encrypt {
|
||||
Ok(secretbox::seal(data, &nonce, &key))
|
||||
} else {
|
||||
secretbox::open(data, &nonce, &key)
|
||||
}
|
||||
}
|
||||
|
||||
mod test {
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use super::*;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::time::Instant;
|
||||
|
||||
let version = "00";
|
||||
let max_len = 128;
|
||||
|
||||
println!("test str");
|
||||
let data = "1ü1111";
|
||||
let encrypted = encrypt_str_or_original(data, version, max_len);
|
||||
let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
|
||||
println!("data: {data}");
|
||||
println!("encrypted: {encrypted}");
|
||||
println!("decrypted: {decrypted}");
|
||||
assert_eq!(data, decrypted);
|
||||
assert_eq!(version, &encrypted[..2]);
|
||||
assert!(succ);
|
||||
assert!(!store);
|
||||
let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
|
||||
assert!(store);
|
||||
assert!(!decrypt_str_or_original(&decrypted, version).1);
|
||||
assert_eq!(
|
||||
encrypt_str_or_original(&encrypted, version, max_len),
|
||||
encrypted
|
||||
);
|
||||
|
||||
println!("test vec");
|
||||
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
|
||||
let encrypted = encrypt_vec_or_original(&data, version, max_len);
|
||||
let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
|
||||
println!("data: {data:?}");
|
||||
println!("encrypted: {encrypted:?}");
|
||||
println!("decrypted: {decrypted:?}");
|
||||
assert_eq!(data, decrypted);
|
||||
assert_eq!(version.as_bytes(), &encrypted[..2]);
|
||||
assert!(!store);
|
||||
assert!(succ);
|
||||
let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
|
||||
assert!(store);
|
||||
assert!(!decrypt_vec_or_original(&decrypted, version).1);
|
||||
assert_eq!(
|
||||
encrypt_vec_or_original(&encrypted, version, max_len),
|
||||
encrypted
|
||||
);
|
||||
|
||||
println!("test original");
|
||||
let data = version.to_string() + "Hello World";
|
||||
let (decrypted, succ, store) = decrypt_str_or_original(&data, version);
|
||||
assert_eq!(data, decrypted);
|
||||
assert!(store);
|
||||
assert!(!succ);
|
||||
let verbytes = version.as_bytes();
|
||||
let data: Vec<u8> = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6];
|
||||
let (decrypted, succ, store) = decrypt_vec_or_original(&data, version);
|
||||
assert_eq!(data, decrypted);
|
||||
assert!(store);
|
||||
assert!(!succ);
|
||||
let (_, succ, store) = decrypt_str_or_original("", version);
|
||||
assert!(!store);
|
||||
assert!(!succ);
|
||||
let (_, succ, store) = decrypt_vec_or_original(&[], version);
|
||||
assert!(!store);
|
||||
assert!(!succ);
|
||||
let data = "1ü1111";
|
||||
assert_eq!(decrypt_str_or_original(data, version).0, data);
|
||||
let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
|
||||
assert_eq!(decrypt_vec_or_original(&data, version).0, data);
|
||||
|
||||
println!("test speed");
|
||||
let test_speed = |len: usize, name: &str| {
|
||||
let mut data: Vec<u8> = vec![];
|
||||
let mut rng = thread_rng();
|
||||
for _ in 0..len {
|
||||
data.push(rng.gen_range(0..255));
|
||||
}
|
||||
let start: Instant = Instant::now();
|
||||
let encrypted = encrypt_vec_or_original(&data, version, len);
|
||||
assert_ne!(data, decrypted);
|
||||
let t1 = start.elapsed();
|
||||
let start = Instant::now();
|
||||
let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version);
|
||||
let t2 = start.elapsed();
|
||||
assert_eq!(data, decrypted);
|
||||
println!("{name}");
|
||||
println!("encrypt:{:?}, decrypt:{:?}", t1, t2);
|
||||
|
||||
let start: Instant = Instant::now();
|
||||
let encrypted = base64::encode(&data, base64::Variant::Original);
|
||||
let t1 = start.elapsed();
|
||||
let start = Instant::now();
|
||||
let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap();
|
||||
let t2 = start.elapsed();
|
||||
assert_eq!(data, decrypted);
|
||||
println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,);
|
||||
};
|
||||
test_speed(128, "128");
|
||||
test_speed(1024, "1k");
|
||||
test_speed(1024 * 1024, "1M");
|
||||
test_speed(10 * 1024 * 1024, "10M");
|
||||
test_speed(100 * 1024 * 1024, "100M");
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
use crate::ResultType;
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref DISTRO: Distro = Distro::new();
|
||||
}
|
||||
|
||||
pub const DISPLAY_SERVER_WAYLAND: &str = "wayland";
|
||||
pub const DISPLAY_SERVER_X11: &str = "x11";
|
||||
pub const DISPLAY_DESKTOP_KDE: &str = "KDE";
|
||||
|
||||
pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
|
||||
|
||||
pub struct Distro {
|
||||
pub name: String,
|
||||
pub version_id: String,
|
||||
}
|
||||
|
||||
impl Distro {
|
||||
fn new() -> Self {
|
||||
let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
Self { name, version_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_kde() -> bool {
|
||||
if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) {
|
||||
env == DISPLAY_DESKTOP_KDE
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_gdm_user(username: &str) -> bool {
|
||||
username == "gdm"
|
||||
// || username == "lightgdm"
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_desktop_wayland() -> bool {
|
||||
get_display_server() == DISPLAY_SERVER_WAYLAND
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_x11_or_headless() -> bool {
|
||||
!is_desktop_wayland()
|
||||
}
|
||||
|
||||
// -1
|
||||
const INVALID_SESSION: &str = "4294967295";
|
||||
|
||||
pub fn get_display_server() -> String {
|
||||
// Check for forced display server environment variable first
|
||||
if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") {
|
||||
return forced_display;
|
||||
}
|
||||
|
||||
// Check if `loginctl` can be called successfully
|
||||
if run_loginctl(None).is_err() {
|
||||
return DISPLAY_SERVER_X11.to_owned();
|
||||
}
|
||||
|
||||
let mut session = get_values_of_seat0(&[0])[0].clone();
|
||||
if session.is_empty() {
|
||||
// loginctl has not given the expected output. try something else.
|
||||
if let Ok(sid) = std::env::var("XDG_SESSION_ID") {
|
||||
// could also execute "cat /proc/self/sessionid"
|
||||
session = sid;
|
||||
}
|
||||
if session.is_empty() {
|
||||
session = run_cmds("cat /proc/self/sessionid").unwrap_or_default();
|
||||
if session == INVALID_SESSION {
|
||||
session = "".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
if session.is_empty() {
|
||||
std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned())
|
||||
} else {
|
||||
get_display_server_of_session(&session)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_display_server_of_session(session: &str) -> String {
|
||||
let mut display_server = if let Ok(output) =
|
||||
run_loginctl(Some(vec!["show-session", "-p", "Type", session]))
|
||||
// Check session type of the session
|
||||
{
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.replace("Type=", "")
|
||||
.trim_end()
|
||||
.into()
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
if display_server.is_empty() || display_server == "tty" {
|
||||
if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") {
|
||||
if !sestype.is_empty() {
|
||||
return sestype.to_lowercase();
|
||||
}
|
||||
}
|
||||
display_server = "x11".to_owned();
|
||||
}
|
||||
display_server.to_lowercase()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn line_values(indices: &[usize], line: &str) -> Vec<String> {
|
||||
indices
|
||||
.into_iter()
|
||||
.map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, true)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
|
||||
_get_values_of_seat0(indices, false)
|
||||
}
|
||||
|
||||
// Ignore "3 sessions listed."
|
||||
fn ignore_loginctl_line(line: &str) -> bool {
|
||||
line.contains("sessions") || line.split(" ").count() < 4
|
||||
}
|
||||
|
||||
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
||||
if let Ok(output) = run_loginctl(None) {
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if line.contains("seat0") {
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if ignore_loginctl_line(line) {
|
||||
continue;
|
||||
}
|
||||
if let Some(sid) = line.split_whitespace().next() {
|
||||
if is_active(sid) {
|
||||
let d = get_display_server_of_session(sid);
|
||||
if ignore_gdm_wayland {
|
||||
if is_gdm_user(line.split_whitespace().nth(2).unwrap_or(""))
|
||||
&& d == DISPLAY_SERVER_WAYLAND
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if d == "tty" {
|
||||
continue;
|
||||
}
|
||||
return line_values(indices, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line_values(indices, "")
|
||||
}
|
||||
|
||||
pub fn is_active(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("active")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active_and_seat0(sid: &str) -> bool {
|
||||
if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) {
|
||||
String::from_utf8_lossy(&output.stdout).contains("State=active")
|
||||
&& String::from_utf8_lossy(&output.stdout).contains("Seat=seat0")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// **Note** that the return value here, the last character is '\n'.
|
||||
// Use `run_cmds_trim_newline()` if you want to remove '\n' at the end.
|
||||
pub fn run_cmds(cmds: &str) -> ResultType<String> {
|
||||
let output = std::process::Command::new("sh")
|
||||
.args(vec!["-c", cmds])
|
||||
.output()?;
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
pub fn run_cmds_trim_newline(cmds: &str) -> ResultType<String> {
|
||||
let output = std::process::Command::new("sh")
|
||||
.args(vec!["-c", cmds])
|
||||
.output()?;
|
||||
let out = String::from_utf8_lossy(&output.stdout);
|
||||
Ok(if out.ends_with('\n') {
|
||||
out[..out.len() - 1].to_string()
|
||||
} else {
|
||||
out.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
||||
if std::env::var("FLATPAK_ID").is_ok() {
|
||||
let mut l_args = String::from("loginctl");
|
||||
if let Some(a) = args.as_ref() {
|
||||
l_args = format!("{} {}", l_args, a.join(" "));
|
||||
}
|
||||
let res = std::process::Command::new("flatpak-spawn")
|
||||
.args(vec![String::from("--host"), l_args])
|
||||
.output();
|
||||
if res.is_ok() {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
let mut cmd = std::process::Command::new("loginctl");
|
||||
if let Some(a) = args {
|
||||
return cmd.args(a).output();
|
||||
}
|
||||
cmd.output()
|
||||
}
|
||||
|
||||
/// forever: may not work
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
|
||||
let cmds: HashMap<&str, Vec<&str>> = HashMap::from([
|
||||
("notify-send", [title, msg].to_vec()),
|
||||
(
|
||||
"zenity",
|
||||
[
|
||||
"--info",
|
||||
"--timeout",
|
||||
if forever { "0" } else { "3" },
|
||||
"--title",
|
||||
title,
|
||||
"--text",
|
||||
msg,
|
||||
]
|
||||
.to_vec(),
|
||||
),
|
||||
("kdialog", ["--title", title, "--msgbox", msg].to_vec()),
|
||||
(
|
||||
"xmessage",
|
||||
[
|
||||
"-center",
|
||||
"-timeout",
|
||||
if forever { "0" } else { "3" },
|
||||
title,
|
||||
msg,
|
||||
]
|
||||
.to_vec(),
|
||||
),
|
||||
]);
|
||||
for (k, v) in cmds {
|
||||
if Command::new(k).args(v).spawn().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
crate::bail!("failed to post system message");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_run_cmds_trim_newline() {
|
||||
assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123");
|
||||
assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123");
|
||||
assert_eq!(
|
||||
run_cmds_trim_newline("whoami").unwrap() + "\n",
|
||||
run_cmds("whoami").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user