mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-22 09:38:32 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
2
.github/workflows/bridge.yml
vendored
2
.github/workflows/bridge.yml
vendored
@@ -25,6 +25,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
|
||||
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
|
||||
|
||||
37
.github/workflows/flutter-build.yml
vendored
37
.github/workflows/flutter-build.yml
vendored
@@ -33,7 +33,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.7"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
@@ -87,6 +87,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
@@ -276,6 +278,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 +408,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Restore bridge files
|
||||
uses: actions/download-artifact@master
|
||||
@@ -489,6 +495,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 +555,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 +603,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 +677,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Import the codesign cert
|
||||
if: env.MACOS_P12_BASE64 != null
|
||||
@@ -952,6 +971,9 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -1228,6 +1250,9 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@@ -1396,6 +1421,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set Swap Space
|
||||
if: ${{ matrix.job.arch == 'x86_64' }}
|
||||
@@ -1724,6 +1751,8 @@ jobs:
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Free Space
|
||||
run: |
|
||||
@@ -1914,6 +1943,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download Binary
|
||||
uses: actions/download-artifact@master
|
||||
@@ -1986,6 +2017,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download Binary
|
||||
uses: actions/download-artifact@master
|
||||
@@ -2043,6 +2076,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.7"
|
||||
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
|
||||
62
Cargo.lock
generated
62
Cargo.lock
generated
@@ -723,9 +723,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 +735,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 +1581,16 @@ dependencies = [
|
||||
"windows 0.32.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "default_net"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/rustdesk-org/default_net#a831d47bcacb4615b394968287697924a8f62be1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"regex",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@@ -2251,9 +2261,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 +2271,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 +2288,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 +2322,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 +2333,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 +2911,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"confy",
|
||||
"default_net",
|
||||
"directories-next",
|
||||
"dirs-next",
|
||||
"dlopen",
|
||||
@@ -2925,6 +2936,7 @@ dependencies = [
|
||||
"serde 1.0.203",
|
||||
"serde_derive",
|
||||
"serde_json 1.0.118",
|
||||
"sha2",
|
||||
"socket2 0.3.19",
|
||||
"sodiumoxide",
|
||||
"sysinfo",
|
||||
@@ -3065,7 +3077,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#c4d6b1c5c4ddc7548868306004cf5d4eb614a36f"
|
||||
dependencies = [
|
||||
"bindgen 0.59.2",
|
||||
"cc",
|
||||
@@ -4382,9 +4394,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 +4426,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",
|
||||
@@ -5494,7 +5506,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.3.6"
|
||||
version = "1.3.7"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -5594,7 +5606,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.6"
|
||||
version = "1.3.7"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.3.6"
|
||||
version = "1.3.7"
|
||||
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 = []
|
||||
|
||||
@@ -175,6 +175,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.7
|
||||
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.7
|
||||
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__":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -172,6 +172,3 @@ target/release/rustdesk
|
||||
|
||||

|
||||
|
||||
## [Публічні сервери](#публічні-сервери)
|
||||
|
||||
RustDesk підтримується безкоштовним європейським сервером, любʼязно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github)
|
||||
|
||||
@@ -237,7 +237,9 @@ prebuild)
|
||||
|
||||
# Install rust bridge generator
|
||||
|
||||
cargo install cargo-expand
|
||||
cargo install \
|
||||
cargo-expand \
|
||||
--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 -
|
||||
@@ -3610,7 +3610,7 @@ void earlyAssert() {
|
||||
}
|
||||
|
||||
void checkUpdate() {
|
||||
if (isDesktop || isAndroid) {
|
||||
if (!isWeb) {
|
||||
if (!bind.isCustomClient()) {
|
||||
platformFFI.registerEventHandler(
|
||||
kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -80,7 +80,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
slivers: [
|
||||
SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
if (!bind.isCustomClient())
|
||||
if (!bind.isCustomClient() && !isIOS)
|
||||
Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)),
|
||||
_buildRemoteIDTextField(),
|
||||
])),
|
||||
@@ -107,7 +107,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 +115,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,
|
||||
@@ -370,10 +368,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
||||
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),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.7+56
|
||||
|
||||
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:
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read, Seek},
|
||||
os::unix::prelude::PermissionsExt,
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::SystemTime,
|
||||
};
|
||||
@@ -51,7 +51,7 @@ pub(super) struct LocalFile {
|
||||
}
|
||||
|
||||
impl LocalFile {
|
||||
pub fn try_open(path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
pub fn try_open(path: &Path) -> Result<Self, CliprdrError> {
|
||||
let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError {
|
||||
path: path.clone(),
|
||||
err: e,
|
||||
@@ -219,7 +219,7 @@ impl LocalFile {
|
||||
|
||||
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
|
||||
fn constr_file_lst(
|
||||
path: &PathBuf,
|
||||
path: &Path,
|
||||
file_list: &mut Vec<LocalFile>,
|
||||
visited: &mut HashSet<PathBuf>,
|
||||
) -> Result<(), CliprdrError> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
sync::{mpsc::Sender, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -74,7 +74,7 @@ trait SysClipboard: Send + Sync {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
fn get_sys_clipboard(ignore_path: &Path) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
#[cfg(feature = "wayland")]
|
||||
{
|
||||
unimplemented!()
|
||||
@@ -88,7 +88,7 @@ fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, Cli
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_sys_clipboard(ignore_path: &PathBuf) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
fn get_sys_clipboard(ignore_path: &Path) -> Result<Box<dyn SysClipboard>, CliprdrError> {
|
||||
use ns_clipboard::*;
|
||||
let ns_pb = NsPasteboard::new(ignore_path)?;
|
||||
Ok(Box::new(ns_pb) as Box<_>)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::{collections::BTreeSet, path::PathBuf};
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use cacao::pasteboard::{Pasteboard, PasteboardName};
|
||||
use hbb_common::log;
|
||||
@@ -30,7 +33,7 @@ pub struct NsPasteboard {
|
||||
}
|
||||
|
||||
impl NsPasteboard {
|
||||
pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
pub fn new(ignore_path: &Path) -> Result<Self, CliprdrError> {
|
||||
Ok(Self {
|
||||
ignore_path: ignore_path.to_owned(),
|
||||
former_file_list: Mutex::new(vec![]),
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::CliprdrError;
|
||||
// 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> {
|
||||
pub(super) fn encode_path_to_uri(path: &Path) -> io::Result<String> {
|
||||
let encoded =
|
||||
percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string();
|
||||
format!("file://{}", encoded)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::{collections::BTreeSet, path::PathBuf};
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use hbb_common::log;
|
||||
use once_cell::sync::OnceCell;
|
||||
@@ -26,7 +29,7 @@ pub struct X11Clipboard {
|
||||
}
|
||||
|
||||
impl X11Clipboard {
|
||||
pub fn new(ignore_path: &PathBuf) -> Result<Self, CliprdrError> {
|
||||
pub fn new(ignore_path: &Path) -> Result<Self, CliprdrError> {
|
||||
let clipboard = get_clip()?;
|
||||
let text_uri_list = clipboard
|
||||
.setter
|
||||
|
||||
1
libs/hbb_common
Submodule
1
libs/hbb_common
Submodule
Submodule libs/hbb_common added at 49c6b24a7a
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
use crate::ResultType;
|
||||
use osascript;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AlertParams {
|
||||
title: String,
|
||||
message: String,
|
||||
alert_type: String,
|
||||
buttons: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AlertResult {
|
||||
#[serde(rename = "buttonReturned")]
|
||||
button: String,
|
||||
}
|
||||
|
||||
/// Firstly run the specified app, then alert a dialog. Return the clicked button value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `app` - The app to execute the script.
|
||||
/// * `alert_type` - Alert type. . informational, warning, critical
|
||||
/// * `title` - The alert title.
|
||||
/// * `message` - The alert message.
|
||||
/// * `buttons` - The buttons to show.
|
||||
pub fn alert(
|
||||
app: String,
|
||||
alert_type: String,
|
||||
title: String,
|
||||
message: String,
|
||||
buttons: Vec<String>,
|
||||
) -> ResultType<String> {
|
||||
let script = osascript::JavaScript::new(&format!(
|
||||
"
|
||||
var App = Application('{}');
|
||||
App.includeStandardAdditions = true;
|
||||
return App.displayAlert($params.title, {{
|
||||
message: $params.message,
|
||||
'as': $params.alert_type,
|
||||
buttons: $params.buttons,
|
||||
}});
|
||||
",
|
||||
app
|
||||
));
|
||||
|
||||
let result: AlertResult = script.execute_with_params(AlertParams {
|
||||
title,
|
||||
message,
|
||||
alert_type,
|
||||
buttons,
|
||||
})?;
|
||||
Ok(result.button)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use crate::{config::Config, log};
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::process::exit;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
static mut GLOBAL_CALLBACK: Option<Box<dyn Fn()>> = None;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
extern "C" fn breakdown_signal_handler(sig: i32) {
|
||||
let mut stack = vec![];
|
||||
backtrace::trace(|frame| {
|
||||
backtrace::resolve_frame(frame, |symbol| {
|
||||
if let Some(name) = symbol.name() {
|
||||
stack.push(name.to_string());
|
||||
}
|
||||
});
|
||||
true // keep going to the next frame
|
||||
});
|
||||
let mut info = String::default();
|
||||
if stack.iter().any(|s| {
|
||||
s.contains(&"nouveau_pushbuf_kick")
|
||||
|| s.to_lowercase().contains("nvidia")
|
||||
|| s.contains("gdk_window_end_draw_frame")
|
||||
|| s.contains("glGetString")
|
||||
}) {
|
||||
Config::set_option("allow-always-software-render".to_string(), "Y".to_string());
|
||||
info = "Always use software rendering will be set.".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
if stack.iter().any(|s| {
|
||||
s.to_lowercase().contains("nvidia")
|
||||
|| s.to_lowercase().contains("amf")
|
||||
|| s.to_lowercase().contains("mfx")
|
||||
|| s.contains("cuProfilerStop")
|
||||
}) {
|
||||
Config::set_option("enable-hwcodec".to_string(), "N".to_string());
|
||||
info = "Perhaps hwcodec causing the crash, disable it first".to_string();
|
||||
log::info!("{}", info);
|
||||
}
|
||||
log::error!(
|
||||
"Got signal {} and exit. stack:\n{}",
|
||||
sig,
|
||||
stack.join("\n").to_string()
|
||||
);
|
||||
if !info.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::system_message(
|
||||
"RustDesk",
|
||||
&format!("Got signal {} and exit.{}", sig, info),
|
||||
true,
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
unsafe {
|
||||
if let Some(callback) = &GLOBAL_CALLBACK {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub fn register_breakdown_handler<T>(callback: T)
|
||||
where
|
||||
T: Fn() + 'static,
|
||||
{
|
||||
unsafe {
|
||||
GLOBAL_CALLBACK = Some(Box::new(callback));
|
||||
libc::signal(libc::SIGSEGV, breakdown_signal_handler as _);
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
use winapi::{
|
||||
shared::minwindef::{DWORD, FALSE, TRUE},
|
||||
um::{
|
||||
handleapi::CloseHandle,
|
||||
pdh::{
|
||||
PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx,
|
||||
PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE,
|
||||
PDH_HCOUNTER, PDH_HQUERY,
|
||||
},
|
||||
synchapi::{CreateEventA, WaitForSingleObject},
|
||||
sysinfoapi::VerSetConditionMask,
|
||||
winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0},
|
||||
winnt::{
|
||||
HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION,
|
||||
VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CPU_USAGE_ONE_MINUTE: Arc<Mutex<Option<(f64, Instant)>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs
|
||||
#[repr(transparent)]
|
||||
pub struct RAIIHandle(pub HANDLE);
|
||||
|
||||
impl Drop for RAIIHandle {
|
||||
fn drop(&mut self) {
|
||||
// This never gives problem except when running under a debugger.
|
||||
unsafe { CloseHandle(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub(self) struct RAIIPDHQuery(pub PDH_HQUERY);
|
||||
|
||||
impl Drop for RAIIPDHQuery {
|
||||
fn drop(&mut self) {
|
||||
unsafe { PdhCloseQuery(self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_cpu_performance_monitor() {
|
||||
// Code from:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex
|
||||
// Why value lower than taskManager:
|
||||
// https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43
|
||||
// Therefore we should compare with Precess Explorer rather than taskManager
|
||||
|
||||
let f = || unsafe {
|
||||
// load avg or cpu usage, test with prime95.
|
||||
// Prefer cpu usage because we can get accurate value from Precess Explorer.
|
||||
// const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0";
|
||||
const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0";
|
||||
const SAMPLE_INTERVAL: DWORD = 2; // 2 second
|
||||
|
||||
let mut ret;
|
||||
let mut query: PDH_HQUERY = std::mem::zeroed();
|
||||
ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query);
|
||||
if ret != 0 {
|
||||
log::error!("PdhOpenQueryA failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
let _query = RAIIPDHQuery(query);
|
||||
let mut counter: PDH_HCOUNTER = std::mem::zeroed();
|
||||
ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter);
|
||||
if ret != 0 {
|
||||
log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
ret = PdhCollectQueryData(query);
|
||||
if ret != 0 {
|
||||
log::error!("PdhCollectQueryData failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
let mut _counter_type: DWORD = 0;
|
||||
let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed();
|
||||
let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _);
|
||||
if event.is_null() {
|
||||
log::error!("CreateEventA failed");
|
||||
return;
|
||||
}
|
||||
let _event: RAIIHandle = RAIIHandle(event);
|
||||
ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event);
|
||||
if ret != 0 {
|
||||
log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut queue: VecDeque<f64> = VecDeque::new();
|
||||
let mut recent_valid: VecDeque<bool> = VecDeque::new();
|
||||
loop {
|
||||
// latest one minute
|
||||
if queue.len() == 31 {
|
||||
queue.pop_front();
|
||||
}
|
||||
if recent_valid.len() == 31 {
|
||||
recent_valid.pop_front();
|
||||
}
|
||||
// allow get value within one minute
|
||||
if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 {
|
||||
let sum: f64 = queue.iter().map(|f| f.to_owned()).sum();
|
||||
let avg = sum / (queue.len() as f64);
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now()));
|
||||
} else {
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = None;
|
||||
}
|
||||
if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) {
|
||||
recent_valid.push_back(false);
|
||||
continue;
|
||||
}
|
||||
if PdhGetFormattedCounterValue(
|
||||
counter,
|
||||
PDH_FMT_DOUBLE,
|
||||
&mut _counter_type,
|
||||
&mut counter_value,
|
||||
) != 0
|
||||
|| counter_value.CStatus != 0
|
||||
{
|
||||
recent_valid.push_back(false);
|
||||
continue;
|
||||
}
|
||||
queue.push_back(counter_value.u.doubleValue().clone());
|
||||
recent_valid.push_back(true);
|
||||
}
|
||||
};
|
||||
use std::sync::Once;
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
std::thread::spawn(f);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn cpu_uage_one_minute() -> Option<f64> {
|
||||
let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone();
|
||||
if let Some((v, instant)) = v {
|
||||
if instant.elapsed().as_secs() < 30 {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn sync_cpu_usage(cpu_usage: Option<f64>) {
|
||||
let v = match cpu_usage {
|
||||
Some(cpu_usage) => Some((cpu_usage, Instant::now())),
|
||||
None => None,
|
||||
};
|
||||
*CPU_USAGE_ONE_MINUTE.lock().unwrap() = v;
|
||||
log::info!("cpu usage synced: {:?}", cpu_usage);
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1
|
||||
// https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780
|
||||
pub fn is_windows_version_or_greater(
|
||||
os_major: u32,
|
||||
os_minor: u32,
|
||||
build_number: u32,
|
||||
service_pack_major: u32,
|
||||
service_pack_minor: u32,
|
||||
) -> bool {
|
||||
let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() };
|
||||
osvi.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOEXW>() as DWORD;
|
||||
osvi.dwMajorVersion = os_major as _;
|
||||
osvi.dwMinorVersion = os_minor as _;
|
||||
osvi.dwBuildNumber = build_number as _;
|
||||
osvi.wServicePackMajor = service_pack_major as _;
|
||||
osvi.wServicePackMinor = service_pack_minor as _;
|
||||
|
||||
let result = unsafe {
|
||||
let mut condition_mask = 0;
|
||||
let op = VER_GREATER_EQUAL;
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op);
|
||||
condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op);
|
||||
|
||||
VerifyVersionInfoW(
|
||||
&mut osvi as *mut OSVERSIONINFOEXW,
|
||||
VER_MAJORVERSION
|
||||
| VER_MINORVERSION
|
||||
| VER_BUILDNUMBER
|
||||
| VER_SERVICEPACKMAJOR
|
||||
| VER_SERVICEPACKMINOR,
|
||||
condition_mask,
|
||||
)
|
||||
};
|
||||
|
||||
result == TRUE
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
|
||||
@@ -1,561 +0,0 @@
|
||||
use std::{
|
||||
io::Error as IoError,
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose, Engine};
|
||||
use httparse::{Error as HttpParseError, Response, EMPTY_HEADER};
|
||||
use log::info;
|
||||
use thiserror::Error as ThisError;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream};
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use tokio_native_tls::{native_tls, TlsConnector, TlsStream};
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
use tokio_rustls::{client::TlsStream, TlsConnector};
|
||||
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr};
|
||||
use tokio_util::codec::Framed;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
bytes_codec::BytesCodec,
|
||||
config::Socks5Server,
|
||||
tcp::{DynTcpStream, FramedStream},
|
||||
ResultType,
|
||||
};
|
||||
|
||||
#[derive(Debug, ThisError)]
|
||||
pub enum ProxyError {
|
||||
#[error("IO Error: {0}")]
|
||||
IoError(#[from] IoError),
|
||||
#[error("Target parse error: {0}")]
|
||||
TargetParseError(String),
|
||||
#[error("HTTP parse error: {0}")]
|
||||
HttpParseError(#[from] HttpParseError),
|
||||
#[error("The maximum response header length is exceeded: {0}")]
|
||||
MaximumResponseHeaderLengthExceeded(usize),
|
||||
#[error("The end of file is reached")]
|
||||
EndOfFile,
|
||||
#[error("The url is error: {0}")]
|
||||
UrlBadScheme(String),
|
||||
#[error("The url parse error: {0}")]
|
||||
UrlParseScheme(#[from] url::ParseError),
|
||||
#[error("No HTTP code was found in the response")]
|
||||
NoHttpCode,
|
||||
#[error("The HTTP code is not equal 200: {0}")]
|
||||
HttpCode200(u16),
|
||||
#[error("The proxy address resolution failed: {0}")]
|
||||
AddressResolutionFailed(String),
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[error("The native tls error: {0}")]
|
||||
NativeTlsError(#[from] tokio_native_tls::native_tls::Error),
|
||||
}
|
||||
|
||||
const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096;
|
||||
/// The maximum HTTP Headers, which can be parsed.
|
||||
const MAXIMUM_RESPONSE_HEADERS: usize = 16;
|
||||
const DEFINE_TIME_OUT: u64 = 600;
|
||||
|
||||
pub trait IntoUrl {
|
||||
|
||||
// Besides parsing as a valid `Url`, the `Url` must be a valid
|
||||
// `http::Uri`, in that it makes sense to use in a network request.
|
||||
fn into_url(self) -> Result<Url, ProxyError>;
|
||||
|
||||
fn as_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl IntoUrl for Url {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
if self.has_host() {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(ProxyError::UrlBadScheme(self.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for &'a str {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
Url::parse(self)
|
||||
.map_err(ProxyError::UrlParseScheme)?
|
||||
.into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for &'a String {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
(&**self).into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoUrl for String {
|
||||
fn into_url(self) -> Result<Url, ProxyError> {
|
||||
(&*self).into_url()
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Auth {
|
||||
user_name: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
fn get_proxy_authorization(&self) -> String {
|
||||
format!(
|
||||
"Proxy-Authorization: Basic {}\r\n",
|
||||
self.get_basic_authorization()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_basic_authorization(&self) -> String {
|
||||
let authorization = format!("{}:{}", &self.user_name, &self.password);
|
||||
general_purpose::STANDARD.encode(authorization.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ProxyScheme {
|
||||
Http {
|
||||
auth: Option<Auth>,
|
||||
host: String,
|
||||
},
|
||||
Https {
|
||||
auth: Option<Auth>,
|
||||
host: String,
|
||||
},
|
||||
Socks5 {
|
||||
addr: SocketAddr,
|
||||
auth: Option<Auth>,
|
||||
remote_dns: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProxyScheme {
|
||||
pub fn maybe_auth(&self) -> Option<&Auth> {
|
||||
match self {
|
||||
ProxyScheme::Http { auth, .. }
|
||||
| ProxyScheme::Https { auth, .. }
|
||||
| ProxyScheme::Socks5 { auth, .. } => auth.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
fn socks5(addr: SocketAddr) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Socks5 {
|
||||
addr,
|
||||
auth: None,
|
||||
remote_dns: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn http(host: &str) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Http {
|
||||
auth: None,
|
||||
host: host.to_string(),
|
||||
})
|
||||
}
|
||||
fn https(host: &str) -> Result<Self, ProxyError> {
|
||||
Ok(ProxyScheme::Https {
|
||||
auth: None,
|
||||
host: host.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
|
||||
let auth = Auth {
|
||||
user_name: username.into(),
|
||||
password: password.into(),
|
||||
};
|
||||
match self {
|
||||
ProxyScheme::Http { auth: a, .. } => *a = Some(auth),
|
||||
ProxyScheme::Https { auth: a, .. } => *a = Some(auth),
|
||||
ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(url: Url) -> Result<Self, ProxyError> {
|
||||
use url::Position;
|
||||
|
||||
// Resolve URL to a host and port
|
||||
let to_addr = || {
|
||||
let addrs = url.socket_addrs(|| match url.scheme() {
|
||||
"socks5" => Some(1080),
|
||||
_ => None,
|
||||
})?;
|
||||
addrs
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost))
|
||||
};
|
||||
|
||||
let mut scheme: Self = match url.scheme() {
|
||||
"http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||
"https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||
"socks5" => Self::socks5(to_addr()?)?,
|
||||
e => return Err(ProxyError::UrlBadScheme(e.to_string())),
|
||||
};
|
||||
|
||||
if let Some(pwd) = url.password() {
|
||||
let username = url.username();
|
||||
scheme.set_basic_auth(username, pwd);
|
||||
}
|
||||
|
||||
Ok(scheme)
|
||||
}
|
||||
pub async fn socket_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||
info!("Resolving socket address");
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await,
|
||||
ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await,
|
||||
ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_host(&self, host: &str, default_port: u16) -> Result<SocketAddr, ProxyError> {
|
||||
let (host_str, port) = match host.split_once(':') {
|
||||
Some((h, p)) => (h, p.parse::<u16>().ok()),
|
||||
None => (host, None),
|
||||
};
|
||||
let addr = (host_str, port.unwrap_or(default_port))
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?;
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
pub fn get_domain(&self) -> Result<String, ProxyError> {
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => {
|
||||
let domain = host
|
||||
.split(':')
|
||||
.next()
|
||||
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?;
|
||||
Ok(domain.to_string())
|
||||
}
|
||||
ProxyScheme::Socks5 { addr, .. } => match addr {
|
||||
SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()),
|
||||
SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn get_host_and_port(&self) -> Result<String, ProxyError> {
|
||||
match self {
|
||||
ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)),
|
||||
ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)),
|
||||
ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)),
|
||||
}
|
||||
}
|
||||
fn append_default_port(&self, host: &str, default_port: u16) -> String {
|
||||
if host.contains(':') {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{}:{}", host, default_port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoProxyScheme {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError>;
|
||||
}
|
||||
|
||||
impl<S: IntoUrl> IntoProxyScheme for S {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||
// validate the URL
|
||||
let url = match self.as_str().into_url() {
|
||||
Ok(ok) => ok,
|
||||
Err(e) => {
|
||||
match e {
|
||||
// If the string does not contain protocol headers, try to parse it using the socks5 protocol
|
||||
ProxyError::UrlParseScheme(_source) => {
|
||||
let try_this = format!("socks5://{}", self.as_str());
|
||||
try_this.into_url()?
|
||||
}
|
||||
_ => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ProxyScheme::parse(url)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProxyScheme for ProxyScheme {
|
||||
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Proxy {
|
||||
pub intercept: ProxyScheme,
|
||||
ms_timeout: u64,
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
pub fn new<U: IntoProxyScheme>(proxy_scheme: U, ms_timeout: u64) -> Result<Self, ProxyError> {
|
||||
Ok(Self {
|
||||
intercept: proxy_scheme.into_proxy_scheme()?,
|
||||
ms_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_http_or_https(&self) -> bool {
|
||||
return match self.intercept {
|
||||
ProxyScheme::Socks5 { .. } => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn from_conf(conf: &Socks5Server, ms_timeout: Option<u64>) -> Result<Self, ProxyError> {
|
||||
let mut proxy;
|
||||
match ms_timeout {
|
||||
None => {
|
||||
proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?;
|
||||
}
|
||||
Some(time_out) => {
|
||||
proxy = Self::new(&conf.proxy, time_out)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !conf.password.is_empty() && !conf.username.is_empty() {
|
||||
proxy = proxy.basic_auth(&conf.username, &conf.password);
|
||||
}
|
||||
Ok(proxy)
|
||||
}
|
||||
|
||||
pub async fn proxy_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||
self.intercept.socket_addrs().await
|
||||
}
|
||||
|
||||
fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
|
||||
self.intercept.set_basic_auth(username, password);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn connect<'t, T>(
|
||||
self,
|
||||
target: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
) -> ResultType<FramedStream>
|
||||
where
|
||||
T: IntoTargetAddr<'t>,
|
||||
{
|
||||
info!("Connect to proxy server");
|
||||
let proxy = self.proxy_addrs().await?;
|
||||
|
||||
let local = if let Some(addr) = local_addr {
|
||||
addr
|
||||
} else {
|
||||
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
|
||||
};
|
||||
|
||||
let stream = super::timeout(
|
||||
self.ms_timeout,
|
||||
crate::tcp::new_socket(local, true)?.connect(proxy),
|
||||
)
|
||||
.await??;
|
||||
stream.set_nodelay(true).ok();
|
||||
|
||||
let addr = stream.local_addr()?;
|
||||
|
||||
return match self.intercept {
|
||||
ProxyScheme::Http { .. } => {
|
||||
info!("Connect to remote http proxy server: {}", proxy);
|
||||
let stream =
|
||||
super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??;
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
ProxyScheme::Https { .. } => {
|
||||
info!("Connect to remote https proxy server: {}", proxy);
|
||||
let stream =
|
||||
super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??;
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
ProxyScheme::Socks5 { .. } => {
|
||||
info!("Connect to remote socket5 proxy server: {}", proxy);
|
||||
let stream = if let Some(auth) = self.intercept.maybe_auth() {
|
||||
super::timeout(
|
||||
self.ms_timeout,
|
||||
Socks5Stream::connect_with_password_and_socket(
|
||||
stream,
|
||||
target,
|
||||
&auth.user_name,
|
||||
&auth.password,
|
||||
),
|
||||
)
|
||||
.await??
|
||||
} else {
|
||||
super::timeout(
|
||||
self.ms_timeout,
|
||||
Socks5Stream::connect_with_socket(stream, target),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
Ok(FramedStream(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
pub async fn https_connect<'a, Input, T>(
|
||||
self,
|
||||
io: Input,
|
||||
target: T,
|
||||
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
T: IntoTargetAddr<'a>,
|
||||
{
|
||||
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
|
||||
let stream = tls_connector
|
||||
.connect(&self.intercept.get_domain()?, io)
|
||||
.await?;
|
||||
self.http_connect(stream, target).await
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
pub async fn https_connect<'a, Input, T>(
|
||||
self,
|
||||
io: Input,
|
||||
target: T,
|
||||
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
T: IntoTargetAddr<'a>,
|
||||
{
|
||||
use std::convert::TryFrom;
|
||||
let verifier = rustls_platform_verifier::tls_config();
|
||||
let url_domain = self.intercept.get_domain()?;
|
||||
|
||||
let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str())
|
||||
.map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))?
|
||||
.to_owned();
|
||||
|
||||
let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier));
|
||||
let stream = tls_connector.connect(domain, io).await?;
|
||||
self.http_connect(stream, target).await
|
||||
}
|
||||
|
||||
pub async fn http_connect<'a, Input, T>(
|
||||
self,
|
||||
io: Input,
|
||||
target: T,
|
||||
) -> Result<BufStream<Input>, ProxyError>
|
||||
where
|
||||
Input: AsyncRead + AsyncWrite + Unpin,
|
||||
T: IntoTargetAddr<'a>,
|
||||
{
|
||||
let mut stream = BufStream::new(io);
|
||||
let (domain, port) = get_domain_and_port(target)?;
|
||||
|
||||
let request = self.make_request(&domain, port);
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
stream.flush().await?;
|
||||
recv_and_check_response(&mut stream).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn make_request(&self, host: &str, port: u16) -> String {
|
||||
let mut request = format!(
|
||||
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n",
|
||||
host = host,
|
||||
port = port
|
||||
);
|
||||
|
||||
if let Some(auth) = self.intercept.maybe_auth() {
|
||||
request = format!("{}{}", request, auth.get_proxy_authorization());
|
||||
}
|
||||
|
||||
request.push_str("\r\n");
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> {
|
||||
let target_addr = target
|
||||
.into_target_addr()
|
||||
.map_err(|e| ProxyError::TargetParseError(e.to_string()))?;
|
||||
match target_addr {
|
||||
tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())),
|
||||
tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_response<IO>(stream: &mut BufStream<IO>) -> Result<String, ProxyError>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
let mut response = String::new();
|
||||
|
||||
loop {
|
||||
if stream.read_line(&mut response).await? == 0 {
|
||||
return Err(ProxyError::EndOfFile);
|
||||
}
|
||||
|
||||
if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() {
|
||||
return Err(ProxyError::MaximumResponseHeaderLengthExceeded(
|
||||
response.len(),
|
||||
));
|
||||
}
|
||||
|
||||
if response.ends_with("\r\n\r\n") {
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_and_check_response<IO>(stream: &mut BufStream<IO>) -> Result<(), ProxyError>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let response_string = get_response(stream).await?;
|
||||
|
||||
let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS];
|
||||
let mut response = Response::new(&mut response_headers);
|
||||
let response_bytes = response_string.into_bytes();
|
||||
response.parse(&response_bytes)?;
|
||||
|
||||
return match response.code {
|
||||
Some(code) => {
|
||||
if code == 200 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ProxyError::HttpCode200(code))
|
||||
}
|
||||
}
|
||||
None => Err(ProxyError::NoHttpCode),
|
||||
};
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
use crate::{
|
||||
config::{Config, NetworkType},
|
||||
tcp::FramedStream,
|
||||
udp::FramedSocket,
|
||||
ResultType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::ToSocketAddrs;
|
||||
use tokio_socks::{IntoTargetAddr, TargetAddr};
|
||||
|
||||
#[inline]
|
||||
pub fn check_port<T: std::string::ToString>(host: T, port: i32) -> String {
|
||||
let host = host.to_string();
|
||||
if crate::is_ipv6_str(&host) {
|
||||
if host.starts_with('[') {
|
||||
return host;
|
||||
}
|
||||
return format!("[{host}]:{port}");
|
||||
}
|
||||
if !host.contains(':') {
|
||||
return format!("{host}:{port}");
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
|
||||
let host = host.to_string();
|
||||
if crate::is_ipv6_str(&host) {
|
||||
if host.starts_with('[') {
|
||||
let tmp: Vec<&str> = host.split("]:").collect();
|
||||
if tmp.len() == 2 {
|
||||
let port: i32 = tmp[1].parse().unwrap_or(0);
|
||||
if port > 0 {
|
||||
return format!("{}]:{}", tmp[0], port + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if host.contains(':') {
|
||||
let tmp: Vec<&str> = host.split(':').collect();
|
||||
if tmp.len() == 2 {
|
||||
let port: i32 = tmp[1].parse().unwrap_or(0);
|
||||
if port > 0 {
|
||||
return format!("{}:{}", tmp[0], port + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String {
|
||||
let host = check_port(host, 0);
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() {
|
||||
test_if_valid_server_for_proxy_(&host)
|
||||
} else {
|
||||
match host.to_socket_addrs() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn test_if_valid_server_for_proxy_(host: &str) -> String {
|
||||
// `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing,
|
||||
// it can be used for both `socks` and `http` proxy.
|
||||
match &host.into_target_addr() {
|
||||
Err(err) => err.to_string(),
|
||||
Ok(_) => "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IsResolvedSocketAddr {
|
||||
fn resolve(&self) -> Option<&SocketAddr>;
|
||||
}
|
||||
|
||||
impl IsResolvedSocketAddr for SocketAddr {
|
||||
fn resolve(&self) -> Option<&SocketAddr> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IsResolvedSocketAddr for String {
|
||||
fn resolve(&self) -> Option<&SocketAddr> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl IsResolvedSocketAddr for &str {
|
||||
fn resolve(&self) -> Option<&SocketAddr> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn connect_tcp<
|
||||
't,
|
||||
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
|
||||
>(
|
||||
target: T,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<FramedStream> {
|
||||
connect_tcp_local(target, None, ms_timeout).await
|
||||
}
|
||||
|
||||
pub async fn connect_tcp_local<
|
||||
't,
|
||||
T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display,
|
||||
>(
|
||||
target: T,
|
||||
local: Option<SocketAddr>,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<FramedStream> {
|
||||
if let Some(conf) = Config::get_socks() {
|
||||
return FramedStream::connect(target, local, &conf, ms_timeout).await;
|
||||
}
|
||||
if let Some(target) = target.resolve() {
|
||||
if let Some(local) = local {
|
||||
if local.is_ipv6() && target.is_ipv4() {
|
||||
let target = query_nip_io(target).await?;
|
||||
return FramedStream::new(target, Some(local), ms_timeout).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
FramedStream::new(target, local, ms_timeout).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_ipv4(target: &TargetAddr<'_>) -> bool {
|
||||
match target {
|
||||
TargetAddr::Ip(addr) => addr.is_ipv4(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn query_nip_io(addr: &SocketAddr) -> ResultType<SocketAddr> {
|
||||
tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port()))
|
||||
.await?
|
||||
.find(|x| x.is_ipv6())
|
||||
.context("Failed to get ipv6 from nip.io")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String {
|
||||
if !ipv4 && crate::is_ipv4_str(&addr) {
|
||||
if let Some(ip) = addr.split(':').next() {
|
||||
return addr.replace(ip, &format!("{ip}.nip.io"));
|
||||
}
|
||||
}
|
||||
addr
|
||||
}
|
||||
|
||||
async fn test_target(target: &str) -> ResultType<SocketAddr> {
|
||||
if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await {
|
||||
if let Ok(addr) = s.peer_addr() {
|
||||
return Ok(addr);
|
||||
}
|
||||
}
|
||||
tokio::net::lookup_host(target)
|
||||
.await?
|
||||
.next()
|
||||
.context(format!("Failed to look up host for {target}"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn new_udp_for(
|
||||
target: &str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<(FramedSocket, TargetAddr<'static>)> {
|
||||
let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() {
|
||||
let addr = test_target(target).await?;
|
||||
(addr.is_ipv4(), addr.into_target_addr()?)
|
||||
} else {
|
||||
(true, target.into_target_addr()?)
|
||||
};
|
||||
Ok((
|
||||
new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?,
|
||||
target.to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn new_udp<T: ToSocketAddrs>(local: T, ms_timeout: u64) -> ResultType<FramedSocket> {
|
||||
match Config::get_socks() {
|
||||
None => Ok(FramedSocket::new(local).await?),
|
||||
Some(conf) => {
|
||||
let socket = FramedSocket::new_proxy(
|
||||
conf.proxy.as_str(),
|
||||
local,
|
||||
conf.username.as_str(),
|
||||
conf.password.as_str(),
|
||||
ms_timeout,
|
||||
)
|
||||
.await?;
|
||||
Ok(socket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rebind_udp_for(
|
||||
target: &str,
|
||||
) -> ResultType<Option<(FramedSocket, TargetAddr<'static>)>> {
|
||||
if Config::get_network_type() != NetworkType::Direct {
|
||||
return Ok(None);
|
||||
}
|
||||
let addr = test_target(target).await?;
|
||||
let v4 = addr.is_ipv4();
|
||||
Ok(Some((
|
||||
FramedSocket::new(Config::get_any_listen_addr(v4)).await?,
|
||||
addr.into_target_addr()?.to_owned(),
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_nat64() {
|
||||
test_nat64_async();
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn test_nat64_async() {
|
||||
assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1");
|
||||
assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io");
|
||||
assert_eq!(
|
||||
ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false),
|
||||
"1.1.1.1.nip.io:8080"
|
||||
);
|
||||
assert_eq!(
|
||||
ipv4_to_ipv6("rustdesk.com".to_owned(), false),
|
||||
"rustdesk.com"
|
||||
);
|
||||
if ("rustdesk.com:80")
|
||||
.to_socket_addrs()
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.is_ipv6()
|
||||
{
|
||||
assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_ipv6());
|
||||
return;
|
||||
}
|
||||
assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_if_valid_server() {
|
||||
assert!(!test_if_valid_server("a", false).is_empty());
|
||||
// on Linux, "1" is resolved to "0.0.0.1"
|
||||
assert!(test_if_valid_server("1.1.1.1", false).is_empty());
|
||||
assert!(test_if_valid_server("1.1.1.1:1", false).is_empty());
|
||||
assert!(test_if_valid_server("microsoft.com", false).is_empty());
|
||||
assert!(test_if_valid_server("microsoft.com:1", false).is_empty());
|
||||
|
||||
// with proxy
|
||||
// `:0` indicates `let host = check_port(host, 0);` is called.
|
||||
assert!(test_if_valid_server_for_proxy_("a:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty());
|
||||
assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_port() {
|
||||
assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12");
|
||||
assert_eq!(check_port("1:2", 32), "[1:2]:32");
|
||||
assert_eq!(check_port("z1:2", 32), "z1:2");
|
||||
assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32");
|
||||
assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32");
|
||||
assert_eq!(check_port("test.com:32", 0), "test.com:32");
|
||||
assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13");
|
||||
assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13");
|
||||
assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4");
|
||||
assert_eq!(increase_port("test.com", 1), "test.com");
|
||||
assert_eq!(increase_port("test.com:13", 4), "test.com:17");
|
||||
assert_eq!(increase_port("1:13", 4), "1:13");
|
||||
assert_eq!(increase_port("22:1:13", 4), "22:1:13");
|
||||
assert_eq!(increase_port("z1:2", 1), "z1:3");
|
||||
}
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy};
|
||||
use anyhow::Context as AnyhowCtx;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use protobuf::Message;
|
||||
use sodiumoxide::crypto::{
|
||||
box_,
|
||||
secretbox::{self, Key, Nonce},
|
||||
};
|
||||
use std::{
|
||||
io::{self, Error, ErrorKind},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
|
||||
};
|
||||
use tokio_socks::IntoTargetAddr;
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
|
||||
pub struct DynTcpStream(pub(crate) Box<dyn TcpStreamTrait + Send + Sync>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Encrypt(Key, u64, u64);
|
||||
|
||||
pub struct FramedStream(
|
||||
pub(crate) Framed<DynTcpStream, BytesCodec>,
|
||||
pub(crate) SocketAddr,
|
||||
pub(crate) Option<Encrypt>,
|
||||
pub(crate) u64,
|
||||
);
|
||||
|
||||
impl Deref for FramedStream {
|
||||
type Target = Framed<DynTcpStream, BytesCodec>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for FramedStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DynTcpStream {
|
||||
type Target = Box<dyn TcpStreamTrait + Send + Sync>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DynTcpStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
||||
let socket = match addr {
|
||||
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
|
||||
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
|
||||
};
|
||||
if reuse {
|
||||
// windows has no reuse_port, but it's reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic behavior
|
||||
#[cfg(unix)]
|
||||
socket.set_reuseport(true).ok();
|
||||
socket.set_reuseaddr(true).ok();
|
||||
}
|
||||
socket.bind(addr)?;
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
impl FramedStream {
|
||||
pub async fn new<T: ToSocketAddrs + std::fmt::Display>(
|
||||
remote_addr: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
for remote_addr in lookup_host(&remote_addr).await? {
|
||||
let local = if let Some(addr) = local_addr {
|
||||
addr
|
||||
} else {
|
||||
crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4())
|
||||
};
|
||||
if let Ok(socket) = new_socket(local, true) {
|
||||
if let Ok(Ok(stream)) =
|
||||
super::timeout(ms_timeout, socket.connect(remote_addr)).await
|
||||
{
|
||||
stream.set_nodelay(true).ok();
|
||||
let addr = stream.local_addr()?;
|
||||
return Ok(Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!(format!("Failed to connect to {remote_addr}"));
|
||||
}
|
||||
|
||||
pub async fn connect<'t, T>(
|
||||
target: T,
|
||||
local_addr: Option<SocketAddr>,
|
||||
proxy_conf: &Socks5Server,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self>
|
||||
where
|
||||
T: IntoTargetAddr<'t>,
|
||||
{
|
||||
let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?;
|
||||
proxy.connect::<T>(target, local_addr).await
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.1
|
||||
}
|
||||
|
||||
pub fn set_send_timeout(&mut self, ms: u64) {
|
||||
self.3 = ms;
|
||||
}
|
||||
|
||||
pub fn from(stream: impl TcpStreamTrait + Send + Sync + 'static, addr: SocketAddr) -> Self {
|
||||
Self(
|
||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||
addr,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_raw(&mut self) {
|
||||
self.0.codec_mut().set_raw();
|
||||
self.2 = None;
|
||||
}
|
||||
|
||||
pub fn is_secured(&self) -> bool {
|
||||
self.2.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> {
|
||||
self.send_raw(msg.write_to_bytes()?).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_raw(&mut self, msg: Vec<u8>) -> ResultType<()> {
|
||||
let mut msg = msg;
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
msg = key.enc(&msg);
|
||||
}
|
||||
self.send_bytes(bytes::Bytes::from(msg)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> {
|
||||
if self.3 > 0 {
|
||||
super::timeout(self.3, self.0.send(bytes)).await??;
|
||||
} else {
|
||||
self.0.send(bytes).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
let mut res = self.0.next().await;
|
||||
if let Some(Ok(bytes)) = res.as_mut() {
|
||||
if let Some(key) = self.2.as_mut() {
|
||||
if let Err(err) = key.dec(bytes) {
|
||||
return Some(Err(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(&mut self, ms: u64) -> Option<Result<BytesMut, Error>> {
|
||||
if let Ok(res) = super::timeout(ms, self.next()).await {
|
||||
res
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, key: Key) {
|
||||
self.2 = Some(Encrypt::new(key));
|
||||
}
|
||||
|
||||
fn get_nonce(seqnum: u64) -> Nonce {
|
||||
let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]);
|
||||
nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_le_bytes());
|
||||
nonce
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_BACKLOG: u32 = 128;
|
||||
|
||||
pub async fn new_listener<T: ToSocketAddrs>(addr: T, reuse: bool) -> ResultType<TcpListener> {
|
||||
if !reuse {
|
||||
Ok(TcpListener::bind(addr).await?)
|
||||
} else {
|
||||
let addr = lookup_host(&addr)
|
||||
.await?
|
||||
.next()
|
||||
.context("could not resolve to any address")?;
|
||||
new_socket(addr, true)?
|
||||
.listen(DEFAULT_BACKLOG)
|
||||
.map_err(anyhow::Error::msg)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn listen_any(port: u16) -> ResultType<TcpListener> {
|
||||
if let Ok(mut socket) = TcpSocket::new_v6() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
socket.set_reuseport(true).ok();
|
||||
socket.set_reuseaddr(true).ok();
|
||||
use std::os::unix::io::{FromRawFd, IntoRawFd};
|
||||
let raw_fd = socket.into_raw_fd();
|
||||
let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) };
|
||||
sock2.set_only_v6(false).ok();
|
||||
socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) };
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::prelude::{FromRawSocket, IntoRawSocket};
|
||||
let raw_socket = socket.into_raw_socket();
|
||||
let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) };
|
||||
sock2.set_only_v6(false).ok();
|
||||
socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) };
|
||||
}
|
||||
if socket
|
||||
.bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port))
|
||||
.is_ok()
|
||||
{
|
||||
if let Ok(l) = socket.listen(DEFAULT_BACKLOG) {
|
||||
return Ok(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(new_socket(
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port),
|
||||
true,
|
||||
)?
|
||||
.listen(DEFAULT_BACKLOG)?)
|
||||
}
|
||||
|
||||
impl Unpin for DynTcpStream {}
|
||||
|
||||
impl AsyncRead for DynTcpStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for DynTcpStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + AsyncWrite + Unpin> TcpStreamTrait for R {}
|
||||
|
||||
impl Encrypt {
|
||||
pub fn new(key: Key) -> Self {
|
||||
Self(key, 0, 0)
|
||||
}
|
||||
|
||||
pub fn dec(&mut self, bytes: &mut BytesMut) -> Result<(), Error> {
|
||||
if bytes.len() <= 1 {
|
||||
return Ok(());
|
||||
}
|
||||
self.2 += 1;
|
||||
let nonce = FramedStream::get_nonce(self.2);
|
||||
match secretbox::open(bytes, &nonce, &self.0) {
|
||||
Ok(res) => {
|
||||
bytes.clear();
|
||||
bytes.put_slice(&res);
|
||||
Ok(())
|
||||
}
|
||||
Err(()) => Err(Error::new(ErrorKind::Other, "decryption error")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enc(&mut self, data: &[u8]) -> Vec<u8> {
|
||||
self.1 += 1;
|
||||
let nonce = FramedStream::get_nonce(self.1);
|
||||
secretbox::seal(&data, &nonce, &self.0)
|
||||
}
|
||||
|
||||
pub fn decode(
|
||||
symmetric_data: &[u8],
|
||||
their_pk_b: &[u8],
|
||||
our_sk_b: &box_::SecretKey,
|
||||
) -> ResultType<Key> {
|
||||
if their_pk_b.len() != box_::PUBLICKEYBYTES {
|
||||
anyhow::bail!("Handshake failed: pk length {}", their_pk_b.len());
|
||||
}
|
||||
let nonce = box_::Nonce([0u8; box_::NONCEBYTES]);
|
||||
let mut pk_ = [0u8; box_::PUBLICKEYBYTES];
|
||||
pk_[..].copy_from_slice(their_pk_b);
|
||||
let their_pk_b = box_::PublicKey(pk_);
|
||||
let symmetric_key = box_::open(symmetric_data, &nonce, &their_pk_b, &our_sk_b)
|
||||
.map_err(|_| anyhow::anyhow!("Handshake failed: box decryption failure"))?;
|
||||
if symmetric_key.len() != secretbox::KEYBYTES {
|
||||
anyhow::bail!("Handshake failed: invalid secret key length from peer");
|
||||
}
|
||||
let mut key = [0u8; secretbox::KEYBYTES];
|
||||
key[..].copy_from_slice(&symmetric_key);
|
||||
Ok(Key(key))
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
use crate::ResultType;
|
||||
use anyhow::{anyhow, Context};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use protobuf::Message;
|
||||
use socket2::{Domain, Socket, Type};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket};
|
||||
use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs};
|
||||
use tokio_util::{codec::BytesCodec, udp::UdpFramed};
|
||||
|
||||
pub enum FramedSocket {
|
||||
Direct(UdpFramed<BytesCodec>),
|
||||
ProxySocks(Socks5UdpFramed),
|
||||
}
|
||||
|
||||
fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result<Socket, std::io::Error> {
|
||||
let socket = match addr {
|
||||
SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None),
|
||||
SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None),
|
||||
}?;
|
||||
if reuse {
|
||||
// windows has no reuse_port, but it's reuse_address
|
||||
// almost equals to unix's reuse_port + reuse_address,
|
||||
// though may introduce nondeterministic behavior
|
||||
#[cfg(unix)]
|
||||
socket.set_reuse_port(true).ok();
|
||||
socket.set_reuse_address(true).ok();
|
||||
}
|
||||
// only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full
|
||||
socket.set_nonblocking(true)?;
|
||||
if buf_size > 0 {
|
||||
socket.set_recv_buffer_size(buf_size).ok();
|
||||
}
|
||||
log::debug!(
|
||||
"Receive buf size of udp {}: {:?}",
|
||||
addr,
|
||||
socket.recv_buffer_size()
|
||||
);
|
||||
if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 {
|
||||
socket.set_only_v6(false).ok();
|
||||
}
|
||||
socket.bind(&addr.into())?;
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
impl FramedSocket {
|
||||
pub async fn new<T: ToSocketAddrs>(addr: T) -> ResultType<Self> {
|
||||
Self::new_reuse(addr, false, 0).await
|
||||
}
|
||||
|
||||
pub async fn new_reuse<T: ToSocketAddrs>(
|
||||
addr: T,
|
||||
reuse: bool,
|
||||
buf_size: usize,
|
||||
) -> ResultType<Self> {
|
||||
let addr = lookup_host(&addr)
|
||||
.await?
|
||||
.next()
|
||||
.context("could not resolve to any address")?;
|
||||
Ok(Self::Direct(UdpFramed::new(
|
||||
UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?,
|
||||
BytesCodec::new(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>(
|
||||
proxy: P,
|
||||
local: T,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
ms_timeout: u64,
|
||||
) -> ResultType<Self> {
|
||||
let framed = if username.trim().is_empty() {
|
||||
super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await??
|
||||
} else {
|
||||
super::timeout(
|
||||
ms_timeout,
|
||||
Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
log::trace!(
|
||||
"Socks5 udp connected, local addr: {:?}, target addr: {}",
|
||||
framed.local_addr(),
|
||||
framed.socks_addr()
|
||||
);
|
||||
Ok(Self::ProxySocks(framed))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
msg: &impl Message,
|
||||
addr: impl IntoTargetAddr<'_>,
|
||||
) -> ResultType<()> {
|
||||
let addr = addr.into_target_addr()?.to_owned();
|
||||
let send_data = Bytes::from(msg.write_to_bytes()?);
|
||||
match self {
|
||||
Self::Direct(f) => {
|
||||
if let TargetAddr::Ip(addr) = addr {
|
||||
f.send((send_data, addr)).await?
|
||||
}
|
||||
}
|
||||
Self::ProxySocks(f) => f.send((send_data, addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/68733302/1926020
|
||||
#[inline]
|
||||
pub async fn send_raw(
|
||||
&mut self,
|
||||
msg: &'static [u8],
|
||||
addr: impl IntoTargetAddr<'static>,
|
||||
) -> ResultType<()> {
|
||||
let addr = addr.into_target_addr()?.to_owned();
|
||||
|
||||
match self {
|
||||
Self::Direct(f) => {
|
||||
if let TargetAddr::Ip(addr) = addr {
|
||||
f.send((Bytes::from(msg), addr)).await?
|
||||
}
|
||||
}
|
||||
Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next(&mut self) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
match self {
|
||||
Self::Direct(f) => match f.next().await {
|
||||
Some(Ok((data, addr))) => {
|
||||
Some(Ok((data, addr.into_target_addr().ok()?.to_owned())))
|
||||
}
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
Self::ProxySocks(f) => match f.next().await {
|
||||
Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))),
|
||||
Some(Err(e)) => Some(Err(anyhow!(e))),
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn next_timeout(
|
||||
&mut self,
|
||||
ms: u64,
|
||||
) -> Option<ResultType<(BytesMut, TargetAddr<'static>)>> {
|
||||
if let Ok(res) =
|
||||
tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await
|
||||
{
|
||||
res
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> Option<SocketAddr> {
|
||||
if let FramedSocket::Direct(x) = self {
|
||||
if let Ok(v) = x.get_ref().local_addr() {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.3.6"
|
||||
version = "1.3.7"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
fs::{self},
|
||||
io::{Cursor, Read},
|
||||
path::PathBuf,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -42,7 +42,7 @@ impl BinaryData {
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn write_to_file(&self, prefix: &PathBuf) {
|
||||
pub fn write_to_file(&self, prefix: &Path) {
|
||||
let p = prefix.join(&self.path);
|
||||
if let Some(parent) = p.parent() {
|
||||
if !parent.exists() {
|
||||
@@ -122,7 +122,7 @@ impl BinaryReader {
|
||||
}
|
||||
|
||||
#[cfg(linux)]
|
||||
pub fn configure_permission(&self, prefix: &PathBuf) {
|
||||
pub fn configure_permission(&self, prefix: &Path) {
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
|
||||
let exe_path = prefix.join(&self.exe);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
|
||||
#[cfg(windows)]
|
||||
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
|
||||
|
||||
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
|
||||
fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool {
|
||||
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
||||
return true;
|
||||
};
|
||||
@@ -50,7 +50,7 @@ fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn write_meta(dir: &PathBuf, ts: u64) {
|
||||
fn write_meta(dir: &Path, ts: u64) {
|
||||
let meta_file = dir.join(APP_METADATA_CONFIG);
|
||||
if ts != 0 {
|
||||
let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts);
|
||||
@@ -169,13 +169,13 @@ fn main() {
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows {
|
||||
use std::{fs, os::windows::process::CommandExt, path::PathBuf, process::Command};
|
||||
use std::{fs, os::windows::process::CommandExt, path::Path, process::Command};
|
||||
|
||||
// Used for privacy mode(magnifier impl).
|
||||
pub const RUNTIME_BROKER_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe";
|
||||
pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe";
|
||||
|
||||
pub(super) fn copy_runtime_broker(dir: &PathBuf) {
|
||||
pub(super) fn copy_runtime_broker(dir: &Path) {
|
||||
let src = RUNTIME_BROKER_EXE;
|
||||
let tgt = WIN_TOPMOST_INJECTED_PROCESS_EXE;
|
||||
let target_file = dir.join(tgt);
|
||||
|
||||
@@ -5,7 +5,7 @@ use hbb_common::{
|
||||
};
|
||||
use scrap::{
|
||||
aom::{AomDecoder, AomEncoder, AomEncoderConfig},
|
||||
codec::{EncoderApi, EncoderCfg, Quality as Q},
|
||||
codec::{EncoderApi, EncoderCfg},
|
||||
Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig,
|
||||
VpxVideoCodecId::{self, *},
|
||||
STRIDE_ALIGN,
|
||||
@@ -27,25 +27,17 @@ Usage:
|
||||
Options:
|
||||
-h --help Show this screen.
|
||||
--count=COUNT Capture frame count [default: 100].
|
||||
--quality=QUALITY Video quality [default: Balanced].
|
||||
Valid values: Best, Balanced, Low.
|
||||
--quality=QUALITY Video quality [default: 1.0].
|
||||
--i444 I444.
|
||||
";
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Clone, Copy)]
|
||||
struct Args {
|
||||
flag_count: usize,
|
||||
flag_quality: Quality,
|
||||
flag_quality: f32,
|
||||
flag_i444: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Clone, Copy)]
|
||||
enum Quality {
|
||||
Best,
|
||||
Balanced,
|
||||
Low,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
|
||||
let args: Args = Docopt::new(USAGE)
|
||||
@@ -70,11 +62,6 @@ fn main() {
|
||||
"benchmark {}x{} quality:{:?}, i444:{:?}",
|
||||
width, height, quality, args.flag_i444
|
||||
);
|
||||
let quality = match quality {
|
||||
Quality::Best => Q::Best,
|
||||
Quality::Balanced => Q::Balanced,
|
||||
Quality::Low => Q::Low,
|
||||
};
|
||||
[VP8, VP9].map(|codec| {
|
||||
test_vpx(
|
||||
&mut c,
|
||||
@@ -98,7 +85,7 @@ fn test_vpx(
|
||||
codec_id: VpxVideoCodecId,
|
||||
width: usize,
|
||||
height: usize,
|
||||
quality: Q,
|
||||
quality: f32,
|
||||
yuv_count: usize,
|
||||
i444: bool,
|
||||
) {
|
||||
@@ -177,7 +164,7 @@ fn test_av1(
|
||||
c: &mut Capturer,
|
||||
width: usize,
|
||||
height: usize,
|
||||
quality: Q,
|
||||
quality: f32,
|
||||
yuv_count: usize,
|
||||
i444: bool,
|
||||
) {
|
||||
@@ -247,7 +234,7 @@ mod hw {
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn test(c: &mut Capturer, width: usize, height: usize, quality: Q, yuv_count: usize) {
|
||||
pub fn test(c: &mut Capturer, width: usize, height: usize, quality: f32, yuv_count: usize) {
|
||||
let mut h264s = Vec::new();
|
||||
let mut h265s = Vec::new();
|
||||
if let Some(info) = HwRamEncoder::try_get(CodecFormat::H264) {
|
||||
@@ -263,7 +250,7 @@ mod hw {
|
||||
fn test_encoder(
|
||||
width: usize,
|
||||
height: usize,
|
||||
quality: Q,
|
||||
quality: f32,
|
||||
info: CodecInfo,
|
||||
c: &mut Capturer,
|
||||
yuv_count: usize,
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::time::{Duration, Instant};
|
||||
use std::{io, thread};
|
||||
|
||||
use docopt::Docopt;
|
||||
use scrap::codec::{EncoderApi, EncoderCfg, Quality as Q};
|
||||
use scrap::codec::{EncoderApi, EncoderCfg};
|
||||
use webm::mux;
|
||||
use webm::mux::Track;
|
||||
|
||||
@@ -31,8 +31,7 @@ Options:
|
||||
-h --help Show this screen.
|
||||
--time=<s> Recording duration in seconds.
|
||||
--fps=<fps> Frames per second [default: 30].
|
||||
--quality=<quality> Video quality [default: Balanced].
|
||||
Valid values: Best, Balanced, Low.
|
||||
--quality=<quality> Video quality [default: 1.0].
|
||||
--ba=<kbps> Audio bitrate in kilobits per second [default: 96].
|
||||
--codec CODEC Configure the codec used. [default: vp9]
|
||||
Valid values: vp8, vp9.
|
||||
@@ -44,14 +43,7 @@ struct Args {
|
||||
flag_codec: Codec,
|
||||
flag_time: Option<u64>,
|
||||
flag_fps: u64,
|
||||
flag_quality: Quality,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
enum Quality {
|
||||
Best,
|
||||
Balanced,
|
||||
Low,
|
||||
flag_quality: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
@@ -105,11 +97,7 @@ fn main() -> io::Result<()> {
|
||||
let mut vt = webm.add_video_track(width, height, None, mux_codec);
|
||||
|
||||
// Setup the encoder.
|
||||
let quality = match args.flag_quality {
|
||||
Quality::Best => Q::Best,
|
||||
Quality::Balanced => Q::Balanced,
|
||||
Quality::Low => Q::Low,
|
||||
};
|
||||
let quality = args.flag_quality;
|
||||
let mut vpx = vpx_encode::VpxEncoder::new(
|
||||
EncoderCfg::VPX(vpx_encode::VpxEncoderConfig {
|
||||
width,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/aom_ffi.rs"));
|
||||
|
||||
use crate::codec::{base_bitrate, codec_thread_num, Quality};
|
||||
use crate::codec::{base_bitrate, codec_thread_num};
|
||||
use crate::{codec::EncoderApi, EncodeFrame, STRIDE_ALIGN};
|
||||
use crate::{common::GoogleImage, generate_call_macro, generate_call_ptr_macro, Error, Result};
|
||||
use crate::{EncodeInput, EncodeYuvFormat, Pixfmt};
|
||||
@@ -45,7 +45,7 @@ impl Default for aom_image_t {
|
||||
pub struct AomEncoderConfig {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub quality: Quality,
|
||||
pub quality: f32,
|
||||
pub keyframe_interval: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -62,15 +62,9 @@ mod webrtc {
|
||||
use super::*;
|
||||
|
||||
const kUsageProfile: u32 = AOM_USAGE_REALTIME;
|
||||
const kMinQindex: u32 = 145; // Min qindex threshold for QP scaling.
|
||||
const kMaxQindex: u32 = 205; // Max qindex threshold for QP scaling.
|
||||
const kBitDepth: u32 = 8;
|
||||
const kLagInFrames: u32 = 0; // No look ahead.
|
||||
pub(super) const kTimeBaseDen: i64 = 1000;
|
||||
const kMinimumFrameRate: f64 = 1.0;
|
||||
|
||||
pub const DEFAULT_Q_MAX: u32 = 56; // no more than 63
|
||||
pub const DEFAULT_Q_MIN: u32 = 12; // no more than 63, litter than q_max
|
||||
|
||||
// Only positive speeds, range for real-time coding currently is: 6 - 8.
|
||||
// Lower means slower/better quality, higher means fastest/lower quality.
|
||||
@@ -116,21 +110,10 @@ mod webrtc {
|
||||
} else {
|
||||
c.kf_mode = aom_kf_mode::AOM_KF_DISABLED;
|
||||
}
|
||||
let (q_min, q_max, b) = AomEncoder::convert_quality(cfg.quality);
|
||||
if q_min > 0 && q_min < q_max && q_max < 64 {
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
} else {
|
||||
c.rc_min_quantizer = DEFAULT_Q_MIN;
|
||||
c.rc_max_quantizer = DEFAULT_Q_MAX;
|
||||
}
|
||||
let base_bitrate = base_bitrate(cfg.width as _, cfg.height as _);
|
||||
let bitrate = base_bitrate * b / 100;
|
||||
if bitrate > 0 {
|
||||
c.rc_target_bitrate = bitrate;
|
||||
} else {
|
||||
c.rc_target_bitrate = base_bitrate;
|
||||
}
|
||||
let (q_min, q_max) = AomEncoder::calc_q_values(cfg.quality);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate = AomEncoder::bitrate(cfg.width as _, cfg.height as _, cfg.quality);
|
||||
c.rc_undershoot_pct = 50;
|
||||
c.rc_overshoot_pct = 50;
|
||||
c.rc_buf_initial_sz = 600;
|
||||
@@ -273,17 +256,12 @@ impl EncoderApi for AomEncoder {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, quality: Quality) -> ResultType<()> {
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let mut c = unsafe { *self.ctx.config.enc.to_owned() };
|
||||
let (q_min, q_max, b) = Self::convert_quality(quality);
|
||||
if q_min > 0 && q_min < q_max && q_max < 64 {
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
}
|
||||
let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100;
|
||||
if bitrate > 0 {
|
||||
c.rc_target_bitrate = bitrate;
|
||||
}
|
||||
let (q_min, q_max) = Self::calc_q_values(ratio);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio);
|
||||
call_aom!(aom_codec_enc_config_set(&mut self.ctx, &c));
|
||||
Ok(())
|
||||
}
|
||||
@@ -293,10 +271,6 @@ impl EncoderApi for AomEncoder {
|
||||
c.rc_target_bitrate
|
||||
}
|
||||
|
||||
fn support_abr(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -370,31 +344,27 @@ impl AomEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_quality(quality: Quality) -> (u32, u32, u32) {
|
||||
// we can use lower bitrate for av1
|
||||
match quality {
|
||||
Quality::Best => (12, 25, 100),
|
||||
Quality::Balanced => (12, 35, 100 * 2 / 3),
|
||||
Quality::Low => (18, 45, 50),
|
||||
Quality::Custom(b) => {
|
||||
let (q_min, q_max) = Self::calc_q_values(b);
|
||||
(q_min, q_max, b)
|
||||
}
|
||||
}
|
||||
fn bitrate(width: u32, height: u32, ratio: f32) -> u32 {
|
||||
let bitrate = base_bitrate(width, height) as f32;
|
||||
(bitrate * ratio) as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn calc_q_values(b: u32) -> (u32, u32) {
|
||||
fn calc_q_values(ratio: f32) -> (u32, u32) {
|
||||
let b = (ratio * 100.0) as u32;
|
||||
let b = std::cmp::min(b, 200);
|
||||
let q_min1: i32 = 24;
|
||||
let q_min1 = 24;
|
||||
let q_min2 = 5;
|
||||
let q_max1 = 45;
|
||||
let q_max2 = 25;
|
||||
|
||||
let t = b as f32 / 200.0;
|
||||
|
||||
let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
|
||||
let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
|
||||
let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
|
||||
let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
|
||||
|
||||
q_min = q_min.clamp(q_min2, q_min1);
|
||||
q_max = q_max.clamp(q_max2, q_max1);
|
||||
|
||||
(q_min, q_max)
|
||||
}
|
||||
|
||||
@@ -62,12 +62,10 @@ pub trait EncoderApi {
|
||||
#[cfg(feature = "vram")]
|
||||
fn input_texture(&self) -> bool;
|
||||
|
||||
fn set_quality(&mut self, quality: Quality) -> ResultType<()>;
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()>;
|
||||
|
||||
fn bitrate(&self) -> u32;
|
||||
|
||||
fn support_abr(&self) -> bool;
|
||||
|
||||
fn support_changing_quality(&self) -> bool;
|
||||
|
||||
fn latency_free(&self) -> bool;
|
||||
@@ -882,12 +880,16 @@ pub fn enable_directx_capture() -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub const BR_BEST: f32 = 1.5;
|
||||
pub const BR_BALANCED: f32 = 0.67;
|
||||
pub const BR_SPEED: f32 = 0.5;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Quality {
|
||||
Best,
|
||||
Balanced,
|
||||
Low,
|
||||
Custom(u32),
|
||||
Custom(f32),
|
||||
}
|
||||
|
||||
impl Default for Quality {
|
||||
@@ -903,22 +905,59 @@ impl Quality {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ratio(&self) -> f32 {
|
||||
match self {
|
||||
Quality::Best => BR_BEST,
|
||||
Quality::Balanced => BR_BALANCED,
|
||||
Quality::Low => BR_SPEED,
|
||||
Quality::Custom(v) => *v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_bitrate(width: u32, height: u32) -> u32 {
|
||||
#[allow(unused_mut)]
|
||||
let mut base_bitrate = ((width * height) / 1000) as u32; // same as 1.1.9
|
||||
if base_bitrate == 0 {
|
||||
base_bitrate = 1920 * 1080 / 1000;
|
||||
}
|
||||
const RESOLUTION_PRESETS: &[(u32, u32, u32)] = &[
|
||||
(640, 480, 400), // VGA, 307k pixels
|
||||
(800, 600, 500), // SVGA, 480k pixels
|
||||
(1024, 768, 800), // XGA, 786k pixels
|
||||
(1280, 720, 1000), // 720p, 921k pixels
|
||||
(1366, 768, 1100), // HD, 1049k pixels
|
||||
(1440, 900, 1300), // WXGA+, 1296k pixels
|
||||
(1600, 900, 1500), // HD+, 1440k pixels
|
||||
(1920, 1080, 2073), // 1080p, 2073k pixels
|
||||
(2048, 1080, 2200), // 2K DCI, 2211k pixels
|
||||
(2560, 1440, 3000), // 2K QHD, 3686k pixels
|
||||
(3440, 1440, 4000), // UWQHD, 4953k pixels
|
||||
(3840, 2160, 5000), // 4K UHD, 8294k pixels
|
||||
(7680, 4320, 12000), // 8K UHD, 33177k pixels
|
||||
];
|
||||
let pixels = width * height;
|
||||
|
||||
let (preset_pixels, preset_bitrate) = RESOLUTION_PRESETS
|
||||
.iter()
|
||||
.map(|(w, h, bitrate)| (w * h, bitrate))
|
||||
.min_by_key(|(preset_pixels, _)| {
|
||||
if *preset_pixels >= pixels {
|
||||
preset_pixels - pixels
|
||||
} else {
|
||||
pixels - preset_pixels
|
||||
}
|
||||
})
|
||||
.unwrap_or(((1920 * 1080) as u32, &2073)); // default 1080p
|
||||
|
||||
let bitrate = (*preset_bitrate as f32 * (pixels as f32 / preset_pixels as f32)).round() as u32;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
// fix when android screen shrinks
|
||||
let fix = crate::Display::fix_quality() as u32;
|
||||
log::debug!("Android screen, fix quality:{}", fix);
|
||||
base_bitrate = base_bitrate * fix;
|
||||
bitrate * fix
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
bitrate
|
||||
}
|
||||
base_bitrate
|
||||
}
|
||||
|
||||
pub fn codec_thread_num(limit: usize) -> usize {
|
||||
@@ -1001,8 +1040,7 @@ pub fn test_av1() {
|
||||
static ONCE: Once = Once::new();
|
||||
ONCE.call_once(|| {
|
||||
let f = || {
|
||||
let (width, height, quality, keyframe_interval, i444) =
|
||||
(1920, 1080, Quality::Balanced, None, false);
|
||||
let (width, height, quality, keyframe_interval, i444) = (1920, 1080, 1.0, None, false);
|
||||
let frame_count = 10;
|
||||
let block_size = 300;
|
||||
let move_step = 50;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::{
|
||||
codec::{
|
||||
base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg, Quality as Q,
|
||||
},
|
||||
codec::{base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg},
|
||||
convert::*,
|
||||
CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN,
|
||||
};
|
||||
@@ -47,7 +45,7 @@ pub struct HwRamEncoderConfig {
|
||||
pub mc_name: Option<String>,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub quality: Q,
|
||||
pub quality: f32,
|
||||
pub keyframe_interval: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -67,12 +65,8 @@ impl EncoderApi for HwRamEncoder {
|
||||
match cfg {
|
||||
EncoderCfg::HWRAM(config) => {
|
||||
let rc = Self::rate_control(&config);
|
||||
let b = Self::convert_quality(&config.name, config.quality);
|
||||
let base_bitrate = base_bitrate(config.width as _, config.height as _);
|
||||
let mut bitrate = base_bitrate * b / 100;
|
||||
if bitrate <= 0 {
|
||||
bitrate = base_bitrate;
|
||||
}
|
||||
let mut bitrate =
|
||||
Self::bitrate(&config.name, config.width, config.height, config.quality);
|
||||
bitrate = Self::check_bitrate_range(&config, bitrate);
|
||||
let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32;
|
||||
let ctx = EncodeContext {
|
||||
@@ -176,15 +170,19 @@ impl EncoderApi for HwRamEncoder {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, quality: crate::codec::Quality) -> ResultType<()> {
|
||||
let b = Self::convert_quality(&self.config.name, quality);
|
||||
let mut bitrate = base_bitrate(self.config.width as _, self.config.height as _) * b / 100;
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let mut bitrate = Self::bitrate(
|
||||
&self.config.name,
|
||||
self.config.width,
|
||||
self.config.height,
|
||||
ratio,
|
||||
);
|
||||
if bitrate > 0 {
|
||||
bitrate = Self::check_bitrate_range(&self.config, bitrate);
|
||||
self.encoder.set_bitrate(bitrate as _).ok();
|
||||
self.bitrate = bitrate;
|
||||
}
|
||||
self.config.quality = quality;
|
||||
self.config.quality = ratio;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -192,10 +190,6 @@ impl EncoderApi for HwRamEncoder {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
fn support_abr(&self) -> bool {
|
||||
["qsv", "vaapi"].iter().all(|&x| !self.config.name.contains(x))
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
["vaapi"].iter().all(|&x| !self.config.name.contains(x))
|
||||
}
|
||||
@@ -254,21 +248,35 @@ impl HwRamEncoder {
|
||||
RC_CBR
|
||||
}
|
||||
|
||||
pub fn convert_quality(name: &str, quality: crate::codec::Quality) -> u32 {
|
||||
use crate::codec::Quality;
|
||||
let quality = match quality {
|
||||
Quality::Best => 150,
|
||||
Quality::Balanced => 100,
|
||||
Quality::Low => 50,
|
||||
Quality::Custom(b) => b,
|
||||
};
|
||||
let factor = if name.contains("mediacodec") {
|
||||
pub fn bitrate(name: &str, width: usize, height: usize, ratio: f32) -> u32 {
|
||||
Self::calc_bitrate(width, height, ratio, name.contains("h264"))
|
||||
}
|
||||
|
||||
pub fn calc_bitrate(width: usize, height: usize, ratio: f32, h264: bool) -> u32 {
|
||||
let base = base_bitrate(width as _, height as _) as f32 * ratio;
|
||||
let threshold = 2000.0;
|
||||
let decay_rate = 0.001; // 1000 * 0.001 = 1
|
||||
let factor: f32 = if cfg!(target_os = "android") {
|
||||
// https://stackoverflow.com/questions/26110337/what-are-valid-bit-rates-to-set-for-mediacodec?rq=3
|
||||
5
|
||||
if base > threshold {
|
||||
1.0 + 4.0 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
5.0
|
||||
}
|
||||
} else if h264 {
|
||||
if base > threshold {
|
||||
1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
2.0
|
||||
}
|
||||
} else {
|
||||
1
|
||||
if base > threshold {
|
||||
1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate)
|
||||
} else {
|
||||
1.5
|
||||
}
|
||||
};
|
||||
quality * factor
|
||||
(base * factor) as u32
|
||||
}
|
||||
|
||||
pub fn check_bitrate_range(_config: &HwRamEncoderConfig, bitrate: u32) -> u32 {
|
||||
|
||||
@@ -13,12 +13,12 @@ cfg_if! {
|
||||
} else if #[cfg(x11)] {
|
||||
cfg_if! {
|
||||
if #[cfg(feature="wayland")] {
|
||||
mod linux;
|
||||
mod wayland;
|
||||
mod x11;
|
||||
pub use self::linux::*;
|
||||
pub use self::wayland::set_map_err;
|
||||
pub use self::x11::PixelBuffer;
|
||||
mod linux;
|
||||
mod wayland;
|
||||
mod x11;
|
||||
pub use self::linux::*;
|
||||
pub use self::wayland::set_map_err;
|
||||
pub use self::x11::PixelBuffer;
|
||||
} else {
|
||||
mod x11;
|
||||
pub use self::x11::*;
|
||||
@@ -189,7 +189,7 @@ impl Frame<'_> {
|
||||
yuvfmt: EncodeYuvFormat,
|
||||
yuv: &'a mut Vec<u8>,
|
||||
mid_data: &mut Vec<u8>,
|
||||
) -> ResultType<EncodeInput> {
|
||||
) -> ResultType<EncodeInput<'a>> {
|
||||
match self {
|
||||
Frame::PixelBuffer(pixelbuffer) => {
|
||||
convert_to_yuv(&pixelbuffer, yuvfmt, yuv, mid_data)?;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// https://github.com/astraw/vpx-encode
|
||||
// https://github.com/astraw/env-libvpx-sys
|
||||
// https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs
|
||||
// https://github.com/chromium/chromium/blob/e7b24573bc2e06fed4749dd6b6abfce67f29052f/media/video/vpx_video_encoder.cc#L522
|
||||
|
||||
use hbb_common::anyhow::{anyhow, Context};
|
||||
use hbb_common::log;
|
||||
use hbb_common::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame};
|
||||
use hbb_common::ResultType;
|
||||
|
||||
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality};
|
||||
use crate::codec::{base_bitrate, codec_thread_num, EncoderApi};
|
||||
use crate::{EncodeInput, EncodeYuvFormat, GoogleImage, Pixfmt, STRIDE_ALIGN};
|
||||
|
||||
use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
|
||||
@@ -19,9 +20,6 @@ use std::{ptr, slice};
|
||||
generate_call_macro!(call_vpx, false);
|
||||
generate_call_ptr_macro!(call_vpx_ptr);
|
||||
|
||||
const DEFAULT_QP_MAX: u32 = 56; // no more than 63
|
||||
const DEFAULT_QP_MIN: u32 = 12; // no more than 63
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum VpxVideoCodecId {
|
||||
VP8,
|
||||
@@ -85,21 +83,11 @@ impl EncoderApi for VpxEncoder {
|
||||
c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot
|
||||
}
|
||||
|
||||
let (q_min, q_max, b) = Self::convert_quality(config.quality);
|
||||
if q_min > 0 && q_min < q_max && q_max < 64 {
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
} else {
|
||||
c.rc_min_quantizer = DEFAULT_QP_MIN;
|
||||
c.rc_max_quantizer = DEFAULT_QP_MAX;
|
||||
}
|
||||
let base_bitrate = base_bitrate(config.width as _, config.height as _);
|
||||
let bitrate = base_bitrate * b / 100;
|
||||
if bitrate > 0 {
|
||||
c.rc_target_bitrate = bitrate;
|
||||
} else {
|
||||
c.rc_target_bitrate = base_bitrate;
|
||||
}
|
||||
let (q_min, q_max) = Self::calc_q_values(config.quality);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate =
|
||||
Self::bitrate(config.width as _, config.height as _, config.quality);
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp9/common/vp9_enums.h#29
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp8/vp8_cx_iface.c#282
|
||||
c.g_profile = if i444 && config.codec == VpxVideoCodecId::VP9 {
|
||||
@@ -212,17 +200,12 @@ impl EncoderApi for VpxEncoder {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, quality: Quality) -> ResultType<()> {
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let mut c = unsafe { *self.ctx.config.enc.to_owned() };
|
||||
let (q_min, q_max, b) = Self::convert_quality(quality);
|
||||
if q_min > 0 && q_min < q_max && q_max < 64 {
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
}
|
||||
let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100;
|
||||
if bitrate > 0 {
|
||||
c.rc_target_bitrate = bitrate;
|
||||
}
|
||||
let (q_min, q_max) = Self::calc_q_values(ratio);
|
||||
c.rc_min_quantizer = q_min;
|
||||
c.rc_max_quantizer = q_max;
|
||||
c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio);
|
||||
call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &c));
|
||||
Ok(())
|
||||
}
|
||||
@@ -232,9 +215,6 @@ impl EncoderApi for VpxEncoder {
|
||||
c.rc_target_bitrate
|
||||
}
|
||||
|
||||
fn support_abr(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -331,30 +311,27 @@ impl VpxEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_quality(quality: Quality) -> (u32, u32, u32) {
|
||||
match quality {
|
||||
Quality::Best => (6, 45, 150),
|
||||
Quality::Balanced => (12, 56, 100 * 2 / 3),
|
||||
Quality::Low => (18, 56, 50),
|
||||
Quality::Custom(b) => {
|
||||
let (q_min, q_max) = Self::calc_q_values(b);
|
||||
(q_min, q_max, b)
|
||||
}
|
||||
}
|
||||
fn bitrate(width: u32, height: u32, ratio: f32) -> u32 {
|
||||
let bitrate = base_bitrate(width, height) as f32;
|
||||
(bitrate * ratio) as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn calc_q_values(b: u32) -> (u32, u32) {
|
||||
fn calc_q_values(ratio: f32) -> (u32, u32) {
|
||||
let b = (ratio * 100.0) as u32;
|
||||
let b = std::cmp::min(b, 200);
|
||||
let q_min1: i32 = 36;
|
||||
let q_min1 = 36;
|
||||
let q_min2 = 0;
|
||||
let q_max1 = 56;
|
||||
let q_max2 = 37;
|
||||
|
||||
let t = b as f32 / 200.0;
|
||||
|
||||
let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
|
||||
let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
|
||||
let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
|
||||
let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
|
||||
|
||||
q_min = q_min.clamp(q_min2, q_min1);
|
||||
q_max = q_max.clamp(q_max2, q_max1);
|
||||
|
||||
(q_min, q_max)
|
||||
}
|
||||
@@ -415,8 +392,8 @@ pub struct VpxEncoderConfig {
|
||||
pub width: c_uint,
|
||||
/// The height (in pixels).
|
||||
pub height: c_uint,
|
||||
/// The image quality
|
||||
pub quality: Quality,
|
||||
/// The bitrate ratio
|
||||
pub quality: f32,
|
||||
/// The codec
|
||||
pub codec: VpxVideoCodecId,
|
||||
/// keyframe interval
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg, Quality},
|
||||
codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg},
|
||||
hwcodec::HwCodecConfig,
|
||||
AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt,
|
||||
};
|
||||
@@ -17,7 +17,7 @@ use hbb_common::{
|
||||
ResultType,
|
||||
};
|
||||
use hwcodec::{
|
||||
common::{AdapterVendor::*, DataFormat, Driver, MAX_GOP},
|
||||
common::{DataFormat, Driver, MAX_GOP},
|
||||
vram::{
|
||||
decode::{self, DecodeFrame, Decoder},
|
||||
encode::{self, EncodeFrame, Encoder},
|
||||
@@ -39,7 +39,7 @@ pub struct VRamEncoderConfig {
|
||||
pub device: AdapterDevice,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub quality: Quality,
|
||||
pub quality: f32,
|
||||
pub feature: FeatureContext,
|
||||
pub keyframe_interval: Option<usize>,
|
||||
}
|
||||
@@ -51,7 +51,6 @@ pub struct VRamEncoder {
|
||||
bitrate: u32,
|
||||
last_frame_len: usize,
|
||||
same_bad_len_counter: usize,
|
||||
config: VRamEncoderConfig,
|
||||
}
|
||||
|
||||
impl EncoderApi for VRamEncoder {
|
||||
@@ -61,12 +60,12 @@ impl EncoderApi for VRamEncoder {
|
||||
{
|
||||
match cfg {
|
||||
EncoderCfg::VRAM(config) => {
|
||||
let b = Self::convert_quality(config.quality, &config.feature);
|
||||
let base_bitrate = base_bitrate(config.width as _, config.height as _);
|
||||
let mut bitrate = base_bitrate * b / 100;
|
||||
if bitrate <= 0 {
|
||||
bitrate = base_bitrate;
|
||||
}
|
||||
let bitrate = Self::bitrate(
|
||||
config.feature.data_format,
|
||||
config.width,
|
||||
config.height,
|
||||
config.quality,
|
||||
);
|
||||
let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32;
|
||||
let ctx = EncodeContext {
|
||||
f: config.feature.clone(),
|
||||
@@ -87,7 +86,6 @@ impl EncoderApi for VRamEncoder {
|
||||
bitrate,
|
||||
last_frame_len: 0,
|
||||
same_bad_len_counter: 0,
|
||||
config,
|
||||
}),
|
||||
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
|
||||
}
|
||||
@@ -172,9 +170,13 @@ impl EncoderApi for VRamEncoder {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_quality(&mut self, quality: Quality) -> ResultType<()> {
|
||||
let b = Self::convert_quality(quality, &self.ctx.f);
|
||||
let bitrate = base_bitrate(self.ctx.d.width as _, self.ctx.d.height as _) * b / 100;
|
||||
fn set_quality(&mut self, ratio: f32) -> ResultType<()> {
|
||||
let bitrate = Self::bitrate(
|
||||
self.ctx.f.data_format,
|
||||
self.ctx.d.width as _,
|
||||
self.ctx.d.height as _,
|
||||
ratio,
|
||||
);
|
||||
if bitrate > 0 {
|
||||
if self.encoder.set_bitrate((bitrate) as _).is_ok() {
|
||||
self.bitrate = bitrate;
|
||||
@@ -187,10 +189,6 @@ impl EncoderApi for VRamEncoder {
|
||||
self.bitrate
|
||||
}
|
||||
|
||||
fn support_abr(&self) -> bool {
|
||||
self.config.device.vendor_id != ADAPTER_VENDOR_INTEL as u32
|
||||
}
|
||||
|
||||
fn support_changing_quality(&self) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -285,31 +283,8 @@ impl VRamEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_quality(quality: Quality, f: &FeatureContext) -> u32 {
|
||||
match quality {
|
||||
Quality::Best => {
|
||||
if f.driver == Driver::MFX && f.data_format == DataFormat::H264 {
|
||||
200
|
||||
} else {
|
||||
150
|
||||
}
|
||||
}
|
||||
Quality::Balanced => {
|
||||
if f.driver == Driver::MFX && f.data_format == DataFormat::H264 {
|
||||
150
|
||||
} else {
|
||||
100
|
||||
}
|
||||
}
|
||||
Quality::Low => {
|
||||
if f.driver == Driver::MFX && f.data_format == DataFormat::H264 {
|
||||
75
|
||||
} else {
|
||||
50
|
||||
}
|
||||
}
|
||||
Quality::Custom(b) => b,
|
||||
}
|
||||
pub fn bitrate(fmt: DataFormat, width: usize, height: usize, ratio: f32) -> u32 {
|
||||
crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264)
|
||||
}
|
||||
|
||||
pub fn set_not_use(display: usize, not_use: bool) {
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
if [ "$1" = configure ]; then
|
||||
|
||||
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
|
||||
ln -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
|
||||
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
|
||||
|
||||
if [ "systemd" == "$INITSYS" ]; then
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
case $1 in
|
||||
remove|upgrade)
|
||||
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
|
||||
rm /usr/bin/rustdesk
|
||||
rm -f /usr/bin/rustdesk
|
||||
|
||||
if [ "systemd" == "${INITSYS}" ]; then
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.3.6
|
||||
pkgver=1.3.7
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.6
|
||||
Version: 1.3.7
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.6
|
||||
Version: 1.3.7
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.3.6
|
||||
Version: 1.3.7
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -58,7 +58,7 @@ index da7b291b03..3c866177f5 100644
|
||||
+ int status = VTSessionSetProperty(vtctx->session,
|
||||
+ kVTCompressionPropertyKey_AverageBitRate,
|
||||
+ bit_rate_num);
|
||||
+ if (!status) {
|
||||
+ if (status) {
|
||||
+ av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status);
|
||||
+ }
|
||||
+ }
|
||||
|
||||
28
res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch
Normal file
28
res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch
Normal file
@@ -0,0 +1,28 @@
|
||||
From bec8d49e75b37806e1cff39c75027860fde0bfa2 Mon Sep 17 00:00:00 2001
|
||||
From: 21pages <sunboeasy@gmail.com>
|
||||
Date: Fri, 27 Dec 2024 08:43:12 +0800
|
||||
Subject: [PATCH] fix nvenc reconfigure blur
|
||||
|
||||
Signed-off-by: 21pages <sunboeasy@gmail.com>
|
||||
---
|
||||
libavcodec/nvenc.c | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c
|
||||
index 2cce478be0..f4c559b7ce 100644
|
||||
--- a/libavcodec/nvenc.c
|
||||
+++ b/libavcodec/nvenc.c
|
||||
@@ -2741,8 +2741,8 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame)
|
||||
}
|
||||
|
||||
if (reconfig_bitrate) {
|
||||
- params.resetEncoder = 1;
|
||||
- params.forceIDR = 1;
|
||||
+ params.resetEncoder = 0;
|
||||
+ params.forceIDR = 0;
|
||||
|
||||
needs_encode_config = 1;
|
||||
needs_reconfig = 1;
|
||||
--
|
||||
2.43.0.windows.1
|
||||
|
||||
@@ -24,6 +24,7 @@ vcpkg_from_github(
|
||||
patch/0006-dlopen-libva.patch
|
||||
patch/0007-fix-linux-configure.patch
|
||||
patch/0008-remove-amf-loop-query.patch
|
||||
patch/0009-fix-nvenc-reconfigure-blur.patch
|
||||
)
|
||||
|
||||
if(SOURCE_PATH MATCHES " ")
|
||||
|
||||
@@ -12,7 +12,6 @@ use magnum_opus::{Channels::*, Decoder as AudioDecoder};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use ringbuf::{ring_buffer::RbBase, Rb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::c_void,
|
||||
@@ -31,6 +30,7 @@ pub use file_trait::FileManager;
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::tokio::sync::mpsc::UnboundedSender;
|
||||
use hbb_common::tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
|
||||
use hbb_common::{
|
||||
allow_err,
|
||||
anyhow::{anyhow, Context},
|
||||
@@ -44,6 +44,7 @@ use hbb_common::{
|
||||
protobuf::{Message as _, MessageField},
|
||||
rand,
|
||||
rendezvous_proto::*,
|
||||
sha2::{Digest, Sha256},
|
||||
socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6},
|
||||
sodiumoxide::{base64, crypto::sign},
|
||||
tcp::FramedStream,
|
||||
@@ -54,10 +55,6 @@ use hbb_common::{
|
||||
},
|
||||
AddrMangle, ResultType, Stream,
|
||||
};
|
||||
use hbb_common::{
|
||||
config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING,
|
||||
tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver},
|
||||
};
|
||||
pub use helper::*;
|
||||
use scrap::{
|
||||
codec::Decoder,
|
||||
|
||||
@@ -122,6 +122,28 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
|
||||
pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let _file_clip_context_holder = {
|
||||
// `is_port_forward()` will not reach here, but we still check it for clarity.
|
||||
if !self.handler.is_file_transfer() && !self.handler.is_port_forward() {
|
||||
// It is ok to call this function multiple times.
|
||||
ContextSend::enable(true);
|
||||
Some(crate::SimpleCallOnReturn {
|
||||
b: true,
|
||||
f: Box::new(|| {
|
||||
// No need to call `enable(false)` for sciter version, because each client of sciter version is a new process.
|
||||
// It's better to check if the peers are windows(support file copy&paste), but it's not necessary.
|
||||
#[cfg(feature = "flutter")]
|
||||
if !crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
|
||||
ContextSend::enable(false);
|
||||
};
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut last_recv_time = Instant::now();
|
||||
let mut received = false;
|
||||
let conn_type = if self.handler.is_file_transfer() {
|
||||
@@ -1199,6 +1221,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
let peer_platform = pi.platform.clone();
|
||||
self.set_peer_info(&pi);
|
||||
self.handler.handle_peer_info(pi);
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
self.check_clipboard_file_context();
|
||||
if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) {
|
||||
#[cfg(feature = "flutter")]
|
||||
@@ -1237,7 +1260,8 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
// to-do: Android, is `sync_init_clipboard` really needed?
|
||||
// https://github.com/rustdesk/rustdesk/discussions/9010
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[cfg(feature = "flutter")]
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
crate::flutter::update_text_clipboard_required();
|
||||
|
||||
// on connection established client
|
||||
@@ -1898,6 +1922,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
fn check_clipboard_file_context(&self) {
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
|
||||
@@ -816,16 +816,17 @@ pub fn check_software_update() {
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn check_software_update_() -> hbb_common::ResultType<()> {
|
||||
let url = "https://github.com/rustdesk/rustdesk/releases/latest";
|
||||
let latest_release_response = create_http_client_async().get(url).send().await?;
|
||||
let latest_release_version = latest_release_response
|
||||
.url()
|
||||
.path()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or_default();
|
||||
|
||||
let response_url = latest_release_response.url().to_string();
|
||||
let (request, url) =
|
||||
hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_CLIENT.to_string());
|
||||
let latest_release_response = create_http_client_async()
|
||||
.post(url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
let bytes = latest_release_response.bytes().await?;
|
||||
let resp: hbb_common::VersionCheckResponse = serde_json::from_slice(&bytes)?;
|
||||
let response_url = resp.url;
|
||||
let latest_release_version = response_url.rsplit('/').next().unwrap_or_default();
|
||||
|
||||
if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) {
|
||||
#[cfg(feature = "flutter")]
|
||||
@@ -1541,7 +1542,7 @@ pub fn is_empty_uni_link(arg: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn get_hwid() -> Bytes {
|
||||
use sha2::{Digest, Sha256};
|
||||
use hbb_common::sha2::{Digest, Sha256};
|
||||
|
||||
let uuid = hbb_common::get_uuid();
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
@@ -346,7 +346,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Dark", "黑暗"),
|
||||
("Light", "明亮"),
|
||||
("Follow System", "跟随系统"),
|
||||
("Enable hardware codec", "使能硬件编解码"),
|
||||
("Enable hardware codec", "启用硬件编解码"),
|
||||
("Unlock Security Settings", "解锁安全设置"),
|
||||
("Enable audio", "允许传输音频"),
|
||||
("Unlock Network Settings", "解锁网络设置"),
|
||||
|
||||
@@ -654,7 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Upload files", "Subir archivos"),
|
||||
("Clipboard is synchronized", "Portapapeles sincronizado"),
|
||||
("Update client clipboard", "Actualizar portapapeles del cliente"),
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Untagged", "Sin itiquetar"),
|
||||
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -655,6 +655,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Clipboard is synchronized", "Starpliktuve ir sinhronizēta"),
|
||||
("Update client clipboard", "Atjaunināt klienta starpliktuvi"),
|
||||
("Untagged", "Neatzīmēts"),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("new-version-of-{}-tip", "Ir pieejama jauna {} versija"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
[
|
||||
("Status", "Stanje"),
|
||||
("Your Desktop", "Vaše namizje"),
|
||||
("desk_tip", "Do vašega namizja lahko dostopate s spodnjim IDjem in geslom"),
|
||||
("desk_tip", "S spodnjim IDjem in geslom omogočite oddaljeni nadzor vašega računalnika"),
|
||||
("Password", "Geslo"),
|
||||
("Ready", "Pripravljen"),
|
||||
("Established", "Povezava vzpostavljena"),
|
||||
@@ -190,7 +190,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Logging in...", "Prijavljanje..."),
|
||||
("Enable RDP session sharing", "Omogoči deljenje RDP seje"),
|
||||
("Auto Login", "Samodejna prijava"),
|
||||
("Enable direct IP access", "Omogoči neposredni dostop preko IP"),
|
||||
("Enable direct IP access", "Omogoči neposredni dostop preko IP naslova"),
|
||||
("Rename", "Preimenuj"),
|
||||
("Space", "Prazno"),
|
||||
("Create desktop shortcut", "Ustvari bližnjico na namizju"),
|
||||
@@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "Snemanje"),
|
||||
("Directory", "Imenik"),
|
||||
("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Automatically record outgoing sessions", "Samodejno snemaj odhodne seje"),
|
||||
("Change", "Spremeni"),
|
||||
("Start session recording", "Začni snemanje seje"),
|
||||
("Stop session recording", "Ustavi snemanje seje"),
|
||||
@@ -412,8 +412,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"),
|
||||
("software_render_tip", "Če na Linuxu uporabljate Nvidino grafično kartico in se oddaljeno okno zapre takoj po vzpostavitvi povezave, lahko pomaga preklop na odprtokodni gonilnik Nouveau in uporaba programskega upodabljanja. Potreben je ponovni zagon programa."),
|
||||
("Always use software rendering", "Vedno uporabi programsko upodabljanje"),
|
||||
("config_input", "Za nadzor oddaljenega namizja s tipkovnico, rabi RustDesk pravico »Nadzor vnosa«."),
|
||||
("config_microphone", "Za zajem zvoka, rabi RustDesk pravico »Snemanje zvoka«."),
|
||||
("config_input", "RustDesk potrebuje pravico »Nadzor vnosa« za nadzor oddaljenega namizja s tipkovnico."),
|
||||
("config_microphone", "RustDesk potrebuje pravico »Snemanje zvoka« za zajemanje zvoka."),
|
||||
("request_elevation_tip", "Lahko tudi zaprosite za dvig pravic, če je kdo na oddaljeni strani."),
|
||||
("Wait", "Čakaj"),
|
||||
("Elevation Error", "Napaka pri povzdigovanju"),
|
||||
@@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Voice call", "Glasovni klic"),
|
||||
("Text chat", "Besedilni klepet"),
|
||||
("Stop voice call", "Prekini glasovni klic"),
|
||||
("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poikusite povezati preko posrednika. Če želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, če le-ta obstja."),
|
||||
("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poizkusite povezati preko posrednika. Če želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, če le-ta obstja."),
|
||||
("Reconnect", "Ponovna povezava"),
|
||||
("Codec", "Kodek"),
|
||||
("Resolution", "Ločljivost"),
|
||||
@@ -649,12 +649,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Authentication Required", "Potrebno je preverjanje pristnosti"),
|
||||
("Authenticate", "Preverjanje pristnosti"),
|
||||
("web_id_input_tip", "Vnesete lahko ID iz istega strežnika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.\nČe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (<id>@<naslov_strežnika>?key=<ključ>), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nČe želite dostopati do naprave na javnem strežniku, vnesite »<id>@public«; ključ za javni strežnik ni potreben."),
|
||||
("Download", ""),
|
||||
("Upload folder", ""),
|
||||
("Upload files", ""),
|
||||
("Clipboard is synchronized", ""),
|
||||
("Update client clipboard", ""),
|
||||
("Untagged", ""),
|
||||
("new-version-of-{}-tip", ""),
|
||||
("Download", "Prenos"),
|
||||
("Upload folder", "Naloži mapo"),
|
||||
("Upload files", "Naloži datoteke"),
|
||||
("Clipboard is synchronized", "Odložišče je usklajeno"),
|
||||
("Update client clipboard", "Osveži odjemalčevo odložišče"),
|
||||
("Untagged", "Neoznačeno"),
|
||||
("new-version-of-{}-tip", "Na voljo je nova različica {}"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Recording", "錄製"),
|
||||
("Directory", "路徑"),
|
||||
("Automatically record incoming sessions", "自動錄製連入的工作階段"),
|
||||
("Automatically record outgoing sessions", ""),
|
||||
("Automatically record outgoing sessions", "自動錄製連出的工作階段"),
|
||||
("Change", "變更"),
|
||||
("Start session recording", "開始錄影"),
|
||||
("Stop session recording", "停止錄影"),
|
||||
|
||||
@@ -27,7 +27,7 @@ use include_dir::{include_dir, Dir};
|
||||
use objc::rc::autoreleasepool;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use scrap::{libc::c_void, quartz::ffi::*};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
static PRIVILEGES_SCRIPTS_DIR: Dir =
|
||||
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
||||
@@ -515,53 +515,6 @@ pub fn lock_screen() {
|
||||
|
||||
pub fn start_os_service() {
|
||||
log::info!("Username: {}", crate::username());
|
||||
let mut sys = System::new();
|
||||
let path =
|
||||
std::fs::canonicalize(std::env::current_exe().unwrap_or_default()).unwrap_or_default();
|
||||
let mut server = get_server_start_time(&mut sys, &path);
|
||||
if server.is_none() {
|
||||
log::error!("Agent not started yet, please restart --server first to make delegate work",);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
let my_start_time = sys
|
||||
.process((std::process::id() as usize).into())
|
||||
.map(|p| p.start_time())
|
||||
.unwrap_or_default() as i64;
|
||||
log::info!("Startime: {my_start_time} vs {:?}", server);
|
||||
|
||||
std::thread::spawn(move || loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
if server.is_none() {
|
||||
server = get_server_start_time(&mut sys, &path);
|
||||
}
|
||||
let Some((start_time, pid)) = server else {
|
||||
log::error!(
|
||||
"Agent not started yet, please restart --server first to make delegate work",
|
||||
);
|
||||
std::process::exit(-1);
|
||||
};
|
||||
if my_start_time <= start_time + 3 {
|
||||
log::error!(
|
||||
"Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work, earlier more 3 seconds",
|
||||
);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
// only refresh this pid and check if valid, no need to refresh all processes since refreshing all is expensive, about 10ms on my machine
|
||||
if !sys.refresh_process_specifics(pid, ProcessRefreshKind::new()) {
|
||||
server = None;
|
||||
continue;
|
||||
}
|
||||
if let Some(p) = sys.process(pid.into()) {
|
||||
if let Some(p) = get_server_start_time_of(p, &path) {
|
||||
server = Some((p, pid));
|
||||
} else {
|
||||
server = None;
|
||||
}
|
||||
} else {
|
||||
server = None;
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(err) = crate::ipc::start("_service") {
|
||||
log::error!("Failed to start ipc_service: {}", err);
|
||||
}
|
||||
@@ -661,7 +614,7 @@ pub fn hide_dock() {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_server_start_time_of(p: &Process, path: &PathBuf) -> Option<i64> {
|
||||
fn get_server_start_time_of(p: &Process, path: &Path) -> Option<i64> {
|
||||
let cmd = p.cmd();
|
||||
if cmd.len() <= 1 {
|
||||
return None;
|
||||
@@ -679,7 +632,7 @@ fn get_server_start_time_of(p: &Process, path: &PathBuf) -> Option<i64> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_server_start_time(sys: &mut System, path: &PathBuf) -> Option<(i64, Pid)> {
|
||||
fn get_server_start_time(sys: &mut System, path: &Path) -> Option<(i64, Pid)> {
|
||||
sys.refresh_processes_specifics(ProcessRefreshKind::new());
|
||||
for (_, p) in sys.processes() {
|
||||
if let Some(t) = get_server_start_time_of(p, path) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-c</string>
|
||||
<string>sleep 3; if pgrep -f '/Applications/RustDesk.app/Contents/MacOS/RustDesk --server' > /dev/null; then /Applications/RustDesk.app/Contents/MacOS/RustDesk --service; fi</string>
|
||||
<string>/Applications/RustDesk.app/Contents/MacOS/service</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
@@ -1460,15 +1460,13 @@ fn to_le(v: &mut [u16]) -> &[u8] {
|
||||
unsafe { v.align_to().1 }
|
||||
}
|
||||
|
||||
fn get_undone_file(tmp: &PathBuf) -> ResultType<PathBuf> {
|
||||
let mut tmp1 = tmp.clone();
|
||||
tmp1.set_file_name(format!(
|
||||
fn get_undone_file(tmp: &Path) -> ResultType<PathBuf> {
|
||||
Ok(tmp.with_file_name(format!(
|
||||
"{}.undone",
|
||||
tmp.file_name()
|
||||
.ok_or(anyhow!("Failed to get filename of {:?}", tmp))?
|
||||
.to_string_lossy()
|
||||
));
|
||||
Ok(tmp1)
|
||||
)))
|
||||
}
|
||||
|
||||
fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
|
||||
@@ -1933,7 +1931,7 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) ->
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> {
|
||||
pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> {
|
||||
std::process::Command::new("icacls")
|
||||
.arg(dir.as_os_str())
|
||||
.arg("/grant")
|
||||
|
||||
@@ -452,7 +452,7 @@ pub(super) mod install {
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter, Write},
|
||||
path::PathBuf,
|
||||
path::Path,
|
||||
};
|
||||
use zip::ZipArchive;
|
||||
|
||||
@@ -488,7 +488,7 @@ pub(super) mod install {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_file(id: &str, url: &str, filename: &PathBuf) -> bool {
|
||||
fn download_file(id: &str, url: &str, filename: &Path) -> bool {
|
||||
let file = match File::create(filename) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
@@ -505,7 +505,7 @@ pub(super) mod install {
|
||||
true
|
||||
}
|
||||
|
||||
fn do_install_file(filename: &PathBuf, target_dir: &PathBuf) -> ResultType<()> {
|
||||
fn do_install_file(filename: &Path, target_dir: &Path) -> ResultType<()> {
|
||||
let mut zip = ZipArchive::new(BufReader::new(File::open(filename)?))?;
|
||||
for i in 0..zip.len() {
|
||||
let mut file = zip.by_index(i)?;
|
||||
|
||||
@@ -13,7 +13,7 @@ use serde_derive::Serialize;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{c_char, c_void},
|
||||
path::PathBuf,
|
||||
path::Path,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
@@ -299,7 +299,7 @@ pub(super) fn load_plugins(uninstalled_ids: &HashSet<String>) -> ResultType<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_plugin_dir(dir: &PathBuf) {
|
||||
fn load_plugin_dir(dir: &Path) {
|
||||
log::debug!("Begin load plugin dir: {}", dir.display());
|
||||
if let Ok(rd) = std::fs::read_dir(dir) {
|
||||
for entry in rd {
|
||||
|
||||
@@ -33,6 +33,7 @@ use hbb_common::{
|
||||
get_time, get_version_number,
|
||||
message_proto::{option_message::BoolOption, permission_info::Permission},
|
||||
password_security::{self as password, ApproveMode},
|
||||
sha2::{Digest, Sha256},
|
||||
sleep, timeout,
|
||||
tokio::{
|
||||
net::TcpStream,
|
||||
@@ -45,7 +46,6 @@ use hbb_common::{
|
||||
use scrap::android::{call_main_service_key_event, call_main_service_pointer_input};
|
||||
use serde_derive::Serialize;
|
||||
use serde_json::{json, value::Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::{
|
||||
@@ -228,7 +228,6 @@ pub struct Connection {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux_headless_handle: LinuxHeadlessHandle,
|
||||
closed: bool,
|
||||
delay_response_instant: Instant,
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
start_cm_ipc_para: Option<StartCmIpcPara>,
|
||||
auto_disconnect_timer: Option<(Instant, u64)>,
|
||||
@@ -376,7 +375,6 @@ impl Connection {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux_headless_handle,
|
||||
closed: false,
|
||||
delay_response_instant: Instant::now(),
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
start_cm_ipc_para: Some(StartCmIpcPara {
|
||||
rx_to_cm,
|
||||
@@ -736,7 +734,11 @@ impl Connection {
|
||||
});
|
||||
conn.send(msg_out.into()).await;
|
||||
}
|
||||
video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(conn.inner.id(), conn.delay_response_instant.elapsed().as_millis());
|
||||
if conn.is_authed_remote_conn() {
|
||||
if let Some(last_test_delay) = conn.last_test_delay {
|
||||
video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(id, last_test_delay.elapsed().as_millis());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1877,7 +1879,6 @@ impl Connection {
|
||||
.user_network_delay(self.inner.id(), new_delay);
|
||||
self.network_delay = new_delay;
|
||||
}
|
||||
self.delay_response_instant = Instant::now();
|
||||
}
|
||||
} else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union {
|
||||
#[cfg(feature = "flutter")]
|
||||
@@ -3322,6 +3323,13 @@ impl Connection {
|
||||
session_id: self.lr.session_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_authed_remote_conn(&self) -> bool {
|
||||
if let Some(id) = self.authed_conn_id.as_ref() {
|
||||
return id.conn_type() == AuthConnType::Remote;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) {
|
||||
@@ -3809,10 +3817,6 @@ mod raii {
|
||||
fn drop(&mut self) {
|
||||
let mut active_conns_lock = ALIVE_CONNS.lock().unwrap();
|
||||
active_conns_lock.retain(|&c| c != self.0);
|
||||
video_service::VIDEO_QOS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.on_connection_close(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3830,6 +3834,12 @@ mod raii {
|
||||
_ONCE.call_once(|| {
|
||||
shutdown_hooks::add_shutdown_hook(connection_shutdown_hook);
|
||||
});
|
||||
if conn_type == AuthConnType::Remote {
|
||||
video_service::VIDEO_QOS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.on_connection_open(conn_id);
|
||||
}
|
||||
Self(conn_id, conn_type)
|
||||
}
|
||||
|
||||
@@ -3927,12 +3937,20 @@ mod raii {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn conn_type(&self) -> AuthConnType {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AuthedConnID {
|
||||
fn drop(&mut self) {
|
||||
if self.1 == AuthConnType::Remote {
|
||||
scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0));
|
||||
video_service::VIDEO_QOS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.on_connection_close(self.0);
|
||||
}
|
||||
AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0);
|
||||
let remote_count = AUTHED_CONNS
|
||||
|
||||
@@ -15,7 +15,7 @@ use shared_memory::*;
|
||||
use std::{
|
||||
mem::size_of,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -92,7 +92,7 @@ impl SharedMemory {
|
||||
}
|
||||
};
|
||||
log::info!("Create shared memory, size: {}, flink: {}", size, flink);
|
||||
set_path_permission(&PathBuf::from(flink), "F").ok();
|
||||
set_path_permission(Path::new(&flink), "F").ok();
|
||||
Ok(SharedMemory { inner: shmem })
|
||||
}
|
||||
|
||||
@@ -586,8 +586,8 @@ pub mod client {
|
||||
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
|
||||
#[cfg(feature = "flutter")]
|
||||
{
|
||||
if let Some(dir) = PathBuf::from(&exe).parent() {
|
||||
if set_path_permission(&PathBuf::from(dir), "RX").is_err() {
|
||||
if let Some(dir) = Path::new(&exe).parent() {
|
||||
if set_path_permission(Path::new(dir), "RX").is_err() {
|
||||
*SHMEM.lock().unwrap() = None;
|
||||
bail!("Failed to set permission of {:?}", dir);
|
||||
}
|
||||
|
||||
@@ -1,287 +1,222 @@
|
||||
use super::*;
|
||||
use scrap::codec::Quality;
|
||||
use std::time::Duration;
|
||||
use scrap::codec::{Quality, BR_BALANCED, BR_BEST, BR_SPEED};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/*
|
||||
FPS adjust:
|
||||
a. new user connected =>set to INIT_FPS
|
||||
b. TestDelay receive => update user's fps according to network delay
|
||||
When network delay < DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and increase fps;
|
||||
When network delay >= DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and decrease fps;
|
||||
c. second timeout / TestDelay receive => update real fps to the minimum fps from all users
|
||||
|
||||
ratio adjust:
|
||||
a. user set image quality => update to the maximum ratio of the latest quality
|
||||
b. 3 seconds timeout => update ratio according to network delay
|
||||
When network delay < DELAY_THRESHOLD_150MS, increase ratio, max 150kbps;
|
||||
When network delay >= DELAY_THRESHOLD_150MS, decrease ratio;
|
||||
|
||||
adjust betwen FPS and ratio:
|
||||
When network delay < DELAY_THRESHOLD_150MS, fps is always higher than the minimum fps, and ratio is increasing;
|
||||
When network delay >= DELAY_THRESHOLD_150MS, fps is always lower than the minimum fps, and ratio is decreasing;
|
||||
|
||||
delay:
|
||||
use delay minus RTT as the actual network delay
|
||||
*/
|
||||
|
||||
// Constants
|
||||
pub const FPS: u32 = 30;
|
||||
pub const MIN_FPS: u32 = 1;
|
||||
pub const MAX_FPS: u32 = 120;
|
||||
trait Percent {
|
||||
fn as_percent(&self) -> u32;
|
||||
pub const INIT_FPS: u32 = 15;
|
||||
|
||||
// Bitrate ratio constants for different quality levels
|
||||
const BR_MAX: f32 = 40.0; // 2000 * 2 / 100
|
||||
const BR_MIN: f32 = 0.2;
|
||||
const BR_MIN_HIGH_RESOLUTION: f32 = 0.1; // For high resolution, BR_MIN is still too high, so we set a lower limit
|
||||
const MAX_BR_MULTIPLE: f32 = 1.0;
|
||||
|
||||
const HISTORY_DELAY_LEN: usize = 2;
|
||||
const ADJUST_RATIO_INTERVAL: usize = 3; // Adjust quality ratio every 3 seconds
|
||||
const DYNAMIC_SCREEN_THRESHOLD: usize = 2; // Allow increase quality ratio if encode more than 2 times in one second
|
||||
const DELAY_THRESHOLD_150MS: u32 = 150; // 150ms is the threshold for good network condition
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct UserDelay {
|
||||
response_delayed: bool,
|
||||
delay_history: VecDeque<u32>,
|
||||
fps: Option<u32>,
|
||||
rtt_calculator: RttCalculator,
|
||||
quick_increase_fps_count: usize,
|
||||
increase_fps_count: usize,
|
||||
}
|
||||
|
||||
impl Percent for ImageQuality {
|
||||
fn as_percent(&self) -> u32 {
|
||||
match self {
|
||||
ImageQuality::NotSet => 0,
|
||||
ImageQuality::Low => 50,
|
||||
ImageQuality::Balanced => 66,
|
||||
ImageQuality::Best => 100,
|
||||
impl UserDelay {
|
||||
fn add_delay(&mut self, delay: u32) {
|
||||
self.rtt_calculator.update(delay);
|
||||
if self.delay_history.len() > HISTORY_DELAY_LEN {
|
||||
self.delay_history.pop_front();
|
||||
}
|
||||
self.delay_history.push_back(delay);
|
||||
}
|
||||
|
||||
// Average delay minus RTT
|
||||
fn avg_delay(&self) -> u32 {
|
||||
let len = self.delay_history.len();
|
||||
if len > 0 {
|
||||
let avg_delay = self.delay_history.iter().sum::<u32>() / len as u32;
|
||||
|
||||
// If RTT is available, subtract it from average delay to get actual network latency
|
||||
if let Some(rtt) = self.rtt_calculator.get_rtt() {
|
||||
if avg_delay > rtt {
|
||||
avg_delay - rtt
|
||||
} else {
|
||||
avg_delay
|
||||
}
|
||||
} else {
|
||||
avg_delay
|
||||
}
|
||||
} else {
|
||||
DELAY_THRESHOLD_150MS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone)]
|
||||
struct Delay {
|
||||
state: DelayState,
|
||||
staging_state: DelayState,
|
||||
delay: u32,
|
||||
counter: u32,
|
||||
slower_than_old_state: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone)]
|
||||
// User session data structure
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct UserData {
|
||||
auto_adjust_fps: Option<u32>, // reserve for compatibility
|
||||
custom_fps: Option<u32>,
|
||||
quality: Option<(i64, Quality)>, // (time, quality)
|
||||
delay: Option<Delay>,
|
||||
response_delayed: bool,
|
||||
delay: UserDelay,
|
||||
record: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct DisplayData {
|
||||
send_counter: usize, // Number of times encode during period
|
||||
support_changing_quality: bool,
|
||||
}
|
||||
|
||||
// Main QoS controller structure
|
||||
pub struct VideoQoS {
|
||||
fps: u32,
|
||||
quality: Quality,
|
||||
ratio: f32,
|
||||
users: HashMap<i32, UserData>,
|
||||
displays: HashMap<usize, DisplayData>,
|
||||
bitrate_store: u32,
|
||||
support_abr: HashMap<usize, bool>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
enum DelayState {
|
||||
Normal = 0,
|
||||
LowDelay = 200,
|
||||
HighDelay = 500,
|
||||
Broken = 1000,
|
||||
}
|
||||
|
||||
impl Default for DelayState {
|
||||
fn default() -> Self {
|
||||
DelayState::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl DelayState {
|
||||
fn from_delay(delay: u32) -> Self {
|
||||
if delay > DelayState::Broken as u32 {
|
||||
DelayState::Broken
|
||||
} else if delay > DelayState::HighDelay as u32 {
|
||||
DelayState::HighDelay
|
||||
} else if delay > DelayState::LowDelay as u32 {
|
||||
DelayState::LowDelay
|
||||
} else {
|
||||
DelayState::Normal
|
||||
}
|
||||
}
|
||||
adjust_ratio_instant: Instant,
|
||||
abr_config: bool,
|
||||
new_user_instant: Instant,
|
||||
}
|
||||
|
||||
impl Default for VideoQoS {
|
||||
fn default() -> Self {
|
||||
VideoQoS {
|
||||
fps: FPS,
|
||||
quality: Default::default(),
|
||||
ratio: 1.0,
|
||||
users: Default::default(),
|
||||
displays: Default::default(),
|
||||
bitrate_store: 0,
|
||||
support_abr: Default::default(),
|
||||
adjust_ratio_instant: Instant::now(),
|
||||
abr_config: true,
|
||||
new_user_instant: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum RefreshType {
|
||||
SetImageQuality,
|
||||
}
|
||||
|
||||
// Basic functionality
|
||||
impl VideoQoS {
|
||||
// Calculate seconds per frame based on current FPS
|
||||
pub fn spf(&self) -> Duration {
|
||||
Duration::from_secs_f32(1. / (self.fps() as f32))
|
||||
}
|
||||
|
||||
// Get current FPS within valid range
|
||||
pub fn fps(&self) -> u32 {
|
||||
if self.fps >= MIN_FPS && self.fps <= MAX_FPS {
|
||||
self.fps
|
||||
let fps = self.fps;
|
||||
if fps >= MIN_FPS && fps <= MAX_FPS {
|
||||
fps
|
||||
} else {
|
||||
FPS
|
||||
}
|
||||
}
|
||||
|
||||
// Store bitrate for later use
|
||||
pub fn store_bitrate(&mut self, bitrate: u32) {
|
||||
self.bitrate_store = bitrate;
|
||||
}
|
||||
|
||||
// Get stored bitrate
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
self.bitrate_store
|
||||
}
|
||||
|
||||
pub fn quality(&self) -> Quality {
|
||||
self.quality
|
||||
// Get current bitrate ratio with bounds checking
|
||||
pub fn ratio(&mut self) -> f32 {
|
||||
if self.ratio < BR_MIN_HIGH_RESOLUTION || self.ratio > BR_MAX {
|
||||
self.ratio = BR_BALANCED;
|
||||
}
|
||||
self.ratio
|
||||
}
|
||||
|
||||
// Check if any user is in recording mode
|
||||
pub fn record(&self) -> bool {
|
||||
self.users.iter().any(|u| u.1.record)
|
||||
}
|
||||
|
||||
pub fn set_support_abr(&mut self, display_idx: usize, support: bool) {
|
||||
self.support_abr.insert(display_idx, support);
|
||||
pub fn set_support_changing_quality(&mut self, display_idx: usize, support: bool) {
|
||||
if let Some(display) = self.displays.get_mut(&display_idx) {
|
||||
display.support_changing_quality = support;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if variable bitrate encoding is supported and enabled
|
||||
pub fn in_vbr_state(&self) -> bool {
|
||||
Config::get_option("enable-abr") != "N" && self.support_abr.iter().all(|e| *e.1)
|
||||
self.abr_config && self.displays.iter().all(|e| e.1.support_changing_quality)
|
||||
}
|
||||
}
|
||||
|
||||
// User session management
|
||||
impl VideoQoS {
|
||||
// Initialize new user session
|
||||
pub fn on_connection_open(&mut self, id: i32) {
|
||||
self.users.insert(id, UserData::default());
|
||||
self.abr_config = Config::get_option("enable-abr") != "N";
|
||||
self.new_user_instant = Instant::now();
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, typ: Option<RefreshType>) {
|
||||
// fps
|
||||
let user_fps = |u: &UserData| {
|
||||
// custom_fps
|
||||
let mut fps = u.custom_fps.unwrap_or(FPS);
|
||||
// auto adjust fps
|
||||
if let Some(auto_adjust_fps) = u.auto_adjust_fps {
|
||||
if fps == 0 || auto_adjust_fps < fps {
|
||||
fps = auto_adjust_fps;
|
||||
}
|
||||
}
|
||||
// delay
|
||||
if let Some(delay) = u.delay {
|
||||
fps = match delay.state {
|
||||
DelayState::Normal => fps,
|
||||
DelayState::LowDelay => fps * 3 / 4,
|
||||
DelayState::HighDelay => fps / 2,
|
||||
DelayState::Broken => fps / 4,
|
||||
}
|
||||
}
|
||||
// delay response
|
||||
if u.response_delayed {
|
||||
if fps > MIN_FPS + 2 {
|
||||
fps = MIN_FPS + 2;
|
||||
}
|
||||
}
|
||||
return fps;
|
||||
};
|
||||
let mut fps = self
|
||||
.users
|
||||
.iter()
|
||||
.map(|(_, u)| user_fps(u))
|
||||
.filter(|u| *u >= MIN_FPS)
|
||||
.min()
|
||||
.unwrap_or(FPS);
|
||||
if fps > MAX_FPS {
|
||||
fps = MAX_FPS;
|
||||
// Clean up user session
|
||||
pub fn on_connection_close(&mut self, id: i32) {
|
||||
self.users.remove(&id);
|
||||
if self.users.is_empty() {
|
||||
*self = Default::default();
|
||||
}
|
||||
self.fps = fps;
|
||||
|
||||
// quality
|
||||
// latest image quality
|
||||
let latest_quality = self
|
||||
.users
|
||||
.iter()
|
||||
.map(|(_, u)| u.quality)
|
||||
.filter(|q| *q != None)
|
||||
.max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0))
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.1;
|
||||
let mut quality = latest_quality;
|
||||
|
||||
// network delay
|
||||
let abr_enabled = self.in_vbr_state();
|
||||
if abr_enabled && typ != Some(RefreshType::SetImageQuality) {
|
||||
// max delay
|
||||
let delay = self
|
||||
.users
|
||||
.iter()
|
||||
.map(|u| u.1.delay)
|
||||
.filter(|d| d.is_some())
|
||||
.max_by(|a, b| {
|
||||
(a.unwrap_or_default().state as u32).cmp(&(b.unwrap_or_default().state as u32))
|
||||
});
|
||||
let delay = delay.unwrap_or_default().unwrap_or_default().state;
|
||||
if delay != DelayState::Normal {
|
||||
match self.quality {
|
||||
Quality::Best => {
|
||||
quality = if delay == DelayState::Broken {
|
||||
Quality::Low
|
||||
} else {
|
||||
Quality::Balanced
|
||||
};
|
||||
}
|
||||
Quality::Balanced => {
|
||||
quality = Quality::Low;
|
||||
}
|
||||
Quality::Low => {
|
||||
quality = Quality::Low;
|
||||
}
|
||||
Quality::Custom(b) => match delay {
|
||||
DelayState::LowDelay => {
|
||||
quality =
|
||||
Quality::Custom(if b >= 150 { 100 } else { std::cmp::min(50, b) });
|
||||
}
|
||||
DelayState::HighDelay => {
|
||||
quality =
|
||||
Quality::Custom(if b >= 100 { 50 } else { std::cmp::min(25, b) });
|
||||
}
|
||||
DelayState::Broken => {
|
||||
quality =
|
||||
Quality::Custom(if b >= 50 { 25 } else { std::cmp::min(10, b) });
|
||||
}
|
||||
DelayState::Normal => {}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
match self.quality {
|
||||
Quality::Low => {
|
||||
if latest_quality == Quality::Best {
|
||||
quality = Quality::Balanced;
|
||||
}
|
||||
}
|
||||
Quality::Custom(current_b) => {
|
||||
if let Quality::Custom(latest_b) = latest_quality {
|
||||
if current_b < latest_b / 2 {
|
||||
quality = Quality::Custom(latest_b / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.quality = quality;
|
||||
}
|
||||
|
||||
pub fn user_custom_fps(&mut self, id: i32, fps: u32) {
|
||||
if fps < MIN_FPS {
|
||||
if fps < MIN_FPS || fps > MAX_FPS {
|
||||
return;
|
||||
}
|
||||
if let Some(user) = self.users.get_mut(&id) {
|
||||
user.custom_fps = Some(fps);
|
||||
} else {
|
||||
self.users.insert(
|
||||
id,
|
||||
UserData {
|
||||
custom_fps: Some(fps),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
self.refresh(None);
|
||||
}
|
||||
|
||||
pub fn user_auto_adjust_fps(&mut self, id: i32, fps: u32) {
|
||||
if fps < MIN_FPS || fps > MAX_FPS {
|
||||
return;
|
||||
}
|
||||
if let Some(user) = self.users.get_mut(&id) {
|
||||
user.auto_adjust_fps = Some(fps);
|
||||
} else {
|
||||
self.users.insert(
|
||||
id,
|
||||
UserData {
|
||||
auto_adjust_fps: Some(fps),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
self.refresh(None);
|
||||
}
|
||||
|
||||
pub fn user_image_quality(&mut self, id: i32, image_quality: i32) {
|
||||
// https://github.com/rustdesk/rustdesk/blob/d716e2b40c38737f1aa3f16de0dec67394a6ac68/src/server/video_service.rs#L493
|
||||
let convert_quality = |q: i32| {
|
||||
let convert_quality = |q: i32| -> Quality {
|
||||
if q == ImageQuality::Balanced.value() {
|
||||
Quality::Balanced
|
||||
} else if q == ImageQuality::Low.value() {
|
||||
@@ -289,92 +224,16 @@ impl VideoQoS {
|
||||
} else if q == ImageQuality::Best.value() {
|
||||
Quality::Best
|
||||
} else {
|
||||
let mut b = (q >> 8 & 0xFFF) * 2;
|
||||
b = std::cmp::max(b, 20);
|
||||
b = std::cmp::min(b, 8000);
|
||||
Quality::Custom(b as u32)
|
||||
let b = ((q >> 8 & 0xFFF) * 2) as f32 / 100.0;
|
||||
Quality::Custom(b.clamp(BR_MIN, BR_MAX))
|
||||
}
|
||||
};
|
||||
|
||||
let quality = Some((hbb_common::get_time(), convert_quality(image_quality)));
|
||||
if let Some(user) = self.users.get_mut(&id) {
|
||||
user.quality = quality;
|
||||
} else {
|
||||
self.users.insert(
|
||||
id,
|
||||
UserData {
|
||||
quality,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
self.refresh(Some(RefreshType::SetImageQuality));
|
||||
}
|
||||
|
||||
pub fn user_network_delay(&mut self, id: i32, delay: u32) {
|
||||
let state = DelayState::from_delay(delay);
|
||||
let debounce = 3;
|
||||
if let Some(user) = self.users.get_mut(&id) {
|
||||
if let Some(d) = &mut user.delay {
|
||||
d.delay = (delay + d.delay) / 2;
|
||||
let new_state = DelayState::from_delay(d.delay);
|
||||
let slower_than_old_state = new_state as i32 - d.staging_state as i32;
|
||||
let slower_than_old_state = if slower_than_old_state > 0 {
|
||||
Some(true)
|
||||
} else if slower_than_old_state < 0 {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if d.slower_than_old_state == slower_than_old_state {
|
||||
let old_counter = d.counter;
|
||||
d.counter += delay / 1000 + 1;
|
||||
if old_counter < debounce && d.counter >= debounce {
|
||||
d.counter = 0;
|
||||
d.state = d.staging_state;
|
||||
d.staging_state = new_state;
|
||||
}
|
||||
if d.counter % debounce == 0 {
|
||||
self.refresh(None);
|
||||
}
|
||||
} else {
|
||||
d.counter = 0;
|
||||
d.staging_state = new_state;
|
||||
d.slower_than_old_state = slower_than_old_state;
|
||||
}
|
||||
} else {
|
||||
user.delay = Some(Delay {
|
||||
state: DelayState::Normal,
|
||||
staging_state: state,
|
||||
delay,
|
||||
counter: 0,
|
||||
slower_than_old_state: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
self.users.insert(
|
||||
id,
|
||||
UserData {
|
||||
delay: Some(Delay {
|
||||
state: DelayState::Normal,
|
||||
staging_state: state,
|
||||
delay,
|
||||
counter: 0,
|
||||
slower_than_old_state: None,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) {
|
||||
if let Some(user) = self.users.get_mut(&id) {
|
||||
let old = user.response_delayed;
|
||||
user.response_delayed = elapsed > 3000;
|
||||
if old != user.response_delayed {
|
||||
self.refresh(None);
|
||||
}
|
||||
// update ratio directly
|
||||
self.ratio = self.latest_quality().ratio();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,8 +243,348 @@ impl VideoQoS {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_connection_close(&mut self, id: i32) {
|
||||
self.users.remove(&id);
|
||||
self.refresh(None);
|
||||
pub fn user_network_delay(&mut self, id: i32, delay: u32) {
|
||||
let highest_fps = self.highest_fps();
|
||||
let target_ratio = self.latest_quality().ratio();
|
||||
|
||||
// For bad network, small fps means quick reaction and high quality
|
||||
let (min_fps, normal_fps) = if target_ratio >= BR_BEST {
|
||||
(8, 16)
|
||||
} else if target_ratio >= BR_BALANCED {
|
||||
(10, 20)
|
||||
} else {
|
||||
(12, 24)
|
||||
};
|
||||
|
||||
// Calculate minimum acceptable delay-fps product
|
||||
let dividend_ms = DELAY_THRESHOLD_150MS * min_fps;
|
||||
|
||||
let mut adjust_ratio = false;
|
||||
if let Some(user) = self.users.get_mut(&id) {
|
||||
let delay = delay.max(10);
|
||||
let old_avg_delay = user.delay.avg_delay();
|
||||
user.delay.add_delay(delay);
|
||||
let mut avg_delay = user.delay.avg_delay();
|
||||
avg_delay = avg_delay.max(10);
|
||||
let mut fps = self.fps;
|
||||
|
||||
// Adaptive FPS adjustment based on network delay:
|
||||
if avg_delay < 50 {
|
||||
user.delay.quick_increase_fps_count += 1;
|
||||
let mut step = if fps < normal_fps { 1 } else { 0 };
|
||||
if user.delay.quick_increase_fps_count >= 3 {
|
||||
// After 3 consecutive good samples, increase more aggressively
|
||||
user.delay.quick_increase_fps_count = 0;
|
||||
step = 5;
|
||||
}
|
||||
fps = min_fps.max(fps + step);
|
||||
} else if avg_delay < 100 {
|
||||
let step = if avg_delay < old_avg_delay {
|
||||
if fps < normal_fps {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
fps = min_fps.max(fps + step);
|
||||
} else if avg_delay < DELAY_THRESHOLD_150MS {
|
||||
fps = min_fps.max(fps);
|
||||
} else {
|
||||
let devide_fps = ((fps as f32) / (avg_delay as f32 / DELAY_THRESHOLD_150MS as f32))
|
||||
.ceil() as u32;
|
||||
if avg_delay < 200 {
|
||||
fps = min_fps.max(devide_fps);
|
||||
} else if avg_delay < 300 {
|
||||
fps = min_fps.min(devide_fps);
|
||||
} else if avg_delay < 600 {
|
||||
fps = dividend_ms / avg_delay;
|
||||
} else {
|
||||
fps = (dividend_ms / avg_delay).min(devide_fps);
|
||||
}
|
||||
}
|
||||
|
||||
if avg_delay < DELAY_THRESHOLD_150MS {
|
||||
user.delay.increase_fps_count += 1;
|
||||
} else {
|
||||
user.delay.increase_fps_count = 0;
|
||||
}
|
||||
if user.delay.increase_fps_count >= 3 {
|
||||
// After 3 stable samples, try increasing FPS
|
||||
user.delay.increase_fps_count = 0;
|
||||
fps += 1;
|
||||
}
|
||||
|
||||
// Reset quick increase counter if network condition worsens
|
||||
if avg_delay > 50 {
|
||||
user.delay.quick_increase_fps_count = 0;
|
||||
}
|
||||
|
||||
fps = fps.clamp(MIN_FPS, highest_fps);
|
||||
// first network delay message
|
||||
adjust_ratio = user.delay.fps.is_none();
|
||||
user.delay.fps = Some(fps);
|
||||
}
|
||||
self.adjust_fps();
|
||||
if adjust_ratio {
|
||||
self.adjust_ratio(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) {
|
||||
if let Some(user) = self.users.get_mut(&id) {
|
||||
user.delay.response_delayed = elapsed > 2000;
|
||||
if user.delay.response_delayed {
|
||||
user.delay.add_delay(elapsed as u32);
|
||||
self.adjust_fps();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common adjust functions
|
||||
impl VideoQoS {
|
||||
pub fn new_display(&mut self, display_idx: usize) {
|
||||
self.displays.insert(display_idx, DisplayData::default());
|
||||
}
|
||||
|
||||
pub fn remove_display(&mut self, display_idx: usize) {
|
||||
self.displays.remove(&display_idx);
|
||||
}
|
||||
|
||||
pub fn update_display_data(&mut self, display_idx: usize, send_counter: usize) {
|
||||
if let Some(display) = self.displays.get_mut(&display_idx) {
|
||||
display.send_counter += send_counter;
|
||||
}
|
||||
self.adjust_fps();
|
||||
let abr_enabled = self.in_vbr_state();
|
||||
if abr_enabled {
|
||||
if self.adjust_ratio_instant.elapsed().as_secs() >= ADJUST_RATIO_INTERVAL as u64 {
|
||||
let dynamic_screen = self
|
||||
.displays
|
||||
.iter()
|
||||
.any(|d| d.1.send_counter >= ADJUST_RATIO_INTERVAL * DYNAMIC_SCREEN_THRESHOLD);
|
||||
self.displays.iter_mut().for_each(|d| {
|
||||
d.1.send_counter = 0;
|
||||
});
|
||||
self.adjust_ratio(dynamic_screen);
|
||||
}
|
||||
} else {
|
||||
self.ratio = self.latest_quality().ratio();
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn highest_fps(&self) -> u32 {
|
||||
let user_fps = |u: &UserData| {
|
||||
let mut fps = u.custom_fps.unwrap_or(FPS);
|
||||
if let Some(auto_adjust_fps) = u.auto_adjust_fps {
|
||||
if fps == 0 || auto_adjust_fps < fps {
|
||||
fps = auto_adjust_fps;
|
||||
}
|
||||
}
|
||||
fps
|
||||
};
|
||||
|
||||
let fps = self
|
||||
.users
|
||||
.iter()
|
||||
.map(|(_, u)| user_fps(u))
|
||||
.filter(|u| *u >= MIN_FPS)
|
||||
.min()
|
||||
.unwrap_or(FPS);
|
||||
|
||||
fps.clamp(MIN_FPS, MAX_FPS)
|
||||
}
|
||||
|
||||
// Get latest quality settings from all users
|
||||
pub fn latest_quality(&self) -> Quality {
|
||||
self.users
|
||||
.iter()
|
||||
.map(|(_, u)| u.quality)
|
||||
.filter(|q| *q != None)
|
||||
.max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0))
|
||||
.flatten()
|
||||
.unwrap_or((0, Quality::Balanced))
|
||||
.1
|
||||
}
|
||||
|
||||
// Adjust quality ratio based on network delay and screen changes
|
||||
fn adjust_ratio(&mut self, dynamic_screen: bool) {
|
||||
// Get maximum delay from all users
|
||||
let max_delay = self.users.iter().map(|u| u.1.delay.avg_delay()).max();
|
||||
let Some(max_delay) = max_delay else {
|
||||
return;
|
||||
};
|
||||
|
||||
let target_quality = self.latest_quality();
|
||||
let target_ratio = self.latest_quality().ratio();
|
||||
let current_ratio = self.ratio;
|
||||
let current_bitrate = self.bitrate();
|
||||
|
||||
// Calculate minimum ratio for high resolution (1Mbps baseline)
|
||||
let ratio_1mbps = if current_bitrate > 0 {
|
||||
Some((current_ratio * 1000.0 / current_bitrate as f32).max(BR_MIN_HIGH_RESOLUTION))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Calculate ratio for adding 150kbps bandwidth
|
||||
let ratio_add_150kbps = if current_bitrate > 0 {
|
||||
Some((current_bitrate + 150) as f32 * current_ratio / current_bitrate as f32)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Set minimum ratio based on quality mode
|
||||
let min = match target_quality {
|
||||
Quality::Best => {
|
||||
// For Best quality, ensure minimum 1Mbps for high resolution
|
||||
let mut min = BR_BEST / 2.5;
|
||||
if let Some(ratio_1mbps) = ratio_1mbps {
|
||||
if min > ratio_1mbps {
|
||||
min = ratio_1mbps;
|
||||
}
|
||||
}
|
||||
min.max(BR_MIN)
|
||||
}
|
||||
Quality::Balanced => {
|
||||
let mut min = (BR_BALANCED / 2.0).min(0.4);
|
||||
if let Some(ratio_1mbps) = ratio_1mbps {
|
||||
if min > ratio_1mbps {
|
||||
min = ratio_1mbps;
|
||||
}
|
||||
}
|
||||
min.max(BR_MIN_HIGH_RESOLUTION)
|
||||
}
|
||||
Quality::Low => BR_MIN_HIGH_RESOLUTION,
|
||||
Quality::Custom(_) => BR_MIN_HIGH_RESOLUTION,
|
||||
};
|
||||
let max = target_ratio * MAX_BR_MULTIPLE;
|
||||
|
||||
let mut v = current_ratio;
|
||||
|
||||
// Adjust ratio based on network delay thresholds
|
||||
if max_delay < 50 {
|
||||
if dynamic_screen {
|
||||
v = current_ratio * 1.15;
|
||||
}
|
||||
} else if max_delay < 100 {
|
||||
if dynamic_screen {
|
||||
v = current_ratio * 1.1;
|
||||
}
|
||||
} else if max_delay < DELAY_THRESHOLD_150MS {
|
||||
if dynamic_screen {
|
||||
v = current_ratio * 1.05;
|
||||
}
|
||||
} else if max_delay < 200 {
|
||||
v = current_ratio * 0.95;
|
||||
} else if max_delay < 300 {
|
||||
v = current_ratio * 0.9;
|
||||
} else if max_delay < 500 {
|
||||
v = current_ratio * 0.85;
|
||||
} else {
|
||||
v = current_ratio * 0.8;
|
||||
}
|
||||
|
||||
// Limit quality increase rate for better stability
|
||||
if let Some(ratio_add_150kbps) = ratio_add_150kbps {
|
||||
if v > ratio_add_150kbps
|
||||
&& ratio_add_150kbps > current_ratio
|
||||
&& current_ratio >= BR_SPEED
|
||||
{
|
||||
v = ratio_add_150kbps;
|
||||
}
|
||||
}
|
||||
|
||||
self.ratio = v.clamp(min, max);
|
||||
self.adjust_ratio_instant = Instant::now();
|
||||
}
|
||||
|
||||
// Adjust fps based on network delay and user response time
|
||||
fn adjust_fps(&mut self) {
|
||||
let highest_fps = self.highest_fps();
|
||||
// Get minimum fps from all users
|
||||
let mut fps = self
|
||||
.users
|
||||
.iter()
|
||||
.map(|u| u.1.delay.fps.unwrap_or(INIT_FPS))
|
||||
.min()
|
||||
.unwrap_or(INIT_FPS);
|
||||
|
||||
if self.users.iter().any(|u| u.1.delay.response_delayed) {
|
||||
if fps > MIN_FPS + 1 {
|
||||
fps = MIN_FPS + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// For new connections (within 1 second), cap fps to INIT_FPS to ensure stability
|
||||
if self.new_user_instant.elapsed().as_secs() < 1 {
|
||||
if fps > INIT_FPS {
|
||||
fps = INIT_FPS;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure fps stays within valid range
|
||||
self.fps = fps.clamp(MIN_FPS, highest_fps);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct RttCalculator {
|
||||
min_rtt: Option<u32>, // Historical minimum RTT ever observed
|
||||
window_min_rtt: Option<u32>, // Minimum RTT within last 60 samples
|
||||
smoothed_rtt: Option<u32>, // Smoothed RTT estimation
|
||||
samples: VecDeque<u32>, // Last 60 RTT samples
|
||||
}
|
||||
|
||||
impl RttCalculator {
|
||||
const WINDOW_SAMPLES: usize = 60; // Keep last 60 samples
|
||||
const MIN_SAMPLES: usize = 10; // Require at least 10 samples
|
||||
const ALPHA: f32 = 0.5; // Smoothing factor for weighted average
|
||||
|
||||
/// Update RTT estimates with a new sample
|
||||
pub fn update(&mut self, delay: u32) {
|
||||
// 1. Update historical minimum RTT
|
||||
match self.min_rtt {
|
||||
Some(min_rtt) if delay < min_rtt => self.min_rtt = Some(delay),
|
||||
None => self.min_rtt = Some(delay),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// 2. Update sample window
|
||||
if self.samples.len() >= Self::WINDOW_SAMPLES {
|
||||
self.samples.pop_front();
|
||||
}
|
||||
self.samples.push_back(delay);
|
||||
|
||||
// 3. Calculate minimum RTT within the window
|
||||
self.window_min_rtt = self.samples.iter().min().copied();
|
||||
|
||||
// 4. Calculate smoothed RTT
|
||||
// Use weighted average if we have enough samples
|
||||
if self.samples.len() >= Self::WINDOW_SAMPLES {
|
||||
if let (Some(min), Some(window_min)) = (self.min_rtt, self.window_min_rtt) {
|
||||
// Weighted average of historical minimum and window minimum
|
||||
let new_srtt =
|
||||
((1.0 - Self::ALPHA) * min as f32 + Self::ALPHA * window_min as f32) as u32;
|
||||
self.smoothed_rtt = Some(new_srtt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current RTT estimate
|
||||
/// Returns None if no valid estimation is available
|
||||
pub fn get_rtt(&self) -> Option<u32> {
|
||||
if let Some(rtt) = self.smoothed_rtt {
|
||||
return Some(rtt);
|
||||
}
|
||||
if self.samples.len() >= Self::MIN_SAMPLES {
|
||||
if let Some(rtt) = self.min_rtt {
|
||||
return Some(rtt);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ use scrap::vram::{VRamEncoder, VRamEncoderConfig};
|
||||
use scrap::Capturer;
|
||||
use scrap::{
|
||||
aom::AomEncoderConfig,
|
||||
codec::{Encoder, EncoderCfg, Quality},
|
||||
codec::{Encoder, EncoderCfg},
|
||||
record::{Recorder, RecorderContext},
|
||||
vpxcodec::{VpxEncoderConfig, VpxVideoCodecId},
|
||||
CodecFormat, Display, EncodeInput, TraitCapturer,
|
||||
@@ -413,9 +413,8 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
c.set_gdi();
|
||||
}
|
||||
let mut video_qos = VIDEO_QOS.lock().unwrap();
|
||||
video_qos.refresh(None);
|
||||
let mut spf;
|
||||
let mut quality = video_qos.quality();
|
||||
let mut spf = video_qos.spf();
|
||||
let mut quality = video_qos.ratio();
|
||||
let record_incoming = config::option2bool(
|
||||
"allow-auto-record-incoming",
|
||||
&Config::get_option("allow-auto-record-incoming"),
|
||||
@@ -461,7 +460,7 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
VIDEO_QOS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_support_abr(display_idx, encoder.support_abr());
|
||||
.set_support_changing_quality(display_idx, encoder.support_changing_quality());
|
||||
log::info!("initial quality: {quality:?}");
|
||||
|
||||
if sp.is_option_true(OPTION_REFRESH) {
|
||||
@@ -489,32 +488,20 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
let mut first_frame = true;
|
||||
let capture_width = c.width;
|
||||
let capture_height = c.height;
|
||||
let (mut second_instant, mut send_counter) = (Instant::now(), 0);
|
||||
|
||||
while sp.ok() {
|
||||
#[cfg(windows)]
|
||||
check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?;
|
||||
|
||||
let mut video_qos = VIDEO_QOS.lock().unwrap();
|
||||
spf = video_qos.spf();
|
||||
if quality != video_qos.quality() {
|
||||
log::debug!("quality: {:?} -> {:?}", quality, video_qos.quality());
|
||||
quality = video_qos.quality();
|
||||
if encoder.support_changing_quality() {
|
||||
allow_err!(encoder.set_quality(quality));
|
||||
video_qos.store_bitrate(encoder.bitrate());
|
||||
} else {
|
||||
if !video_qos.in_vbr_state() && !quality.is_custom() {
|
||||
log::info!("switch to change quality");
|
||||
bail!("SWITCH");
|
||||
}
|
||||
}
|
||||
}
|
||||
if client_record != video_qos.record() {
|
||||
log::info!("switch due to record changed");
|
||||
bail!("SWITCH");
|
||||
}
|
||||
drop(video_qos);
|
||||
|
||||
check_qos(
|
||||
&mut encoder,
|
||||
&mut quality,
|
||||
&mut spf,
|
||||
client_record,
|
||||
&mut send_counter,
|
||||
&mut second_instant,
|
||||
display_idx,
|
||||
)?;
|
||||
if sp.is_option_true(OPTION_REFRESH) {
|
||||
let _ = try_broadcast_display_changed(&sp, display_idx, &c, true);
|
||||
log::info!("switch to refresh");
|
||||
@@ -582,6 +569,7 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
capture_height,
|
||||
)?;
|
||||
frame_controller.set_send(now, send_conn_ids);
|
||||
send_counter += 1;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@@ -640,6 +628,7 @@ fn run(vs: VideoService) -> ResultType<()> {
|
||||
capture_height,
|
||||
)?;
|
||||
frame_controller.set_send(now, send_conn_ids);
|
||||
send_counter += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -691,6 +680,7 @@ struct Raii(usize);
|
||||
|
||||
impl Raii {
|
||||
fn new(display_idx: usize) -> Self {
|
||||
VIDEO_QOS.lock().unwrap().new_display(display_idx);
|
||||
Raii(display_idx)
|
||||
}
|
||||
}
|
||||
@@ -701,14 +691,14 @@ impl Drop for Raii {
|
||||
VRamEncoder::set_not_use(self.0, false);
|
||||
#[cfg(feature = "vram")]
|
||||
Encoder::update(scrap::codec::EncodingUpdate::Check);
|
||||
VIDEO_QOS.lock().unwrap().set_support_abr(self.0, true);
|
||||
VIDEO_QOS.lock().unwrap().remove_display(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_encoder(
|
||||
c: &CapturerInfo,
|
||||
display_idx: usize,
|
||||
quality: Quality,
|
||||
quality: f32,
|
||||
client_record: bool,
|
||||
record_incoming: bool,
|
||||
last_portable_service_running: bool,
|
||||
@@ -737,7 +727,7 @@ fn setup_encoder(
|
||||
fn get_encoder_config(
|
||||
c: &CapturerInfo,
|
||||
_display_idx: usize,
|
||||
quality: Quality,
|
||||
quality: f32,
|
||||
record: bool,
|
||||
_portable_service: bool,
|
||||
) -> EncoderCfg {
|
||||
@@ -1061,3 +1051,40 @@ pub fn make_display_changed_msg(
|
||||
msg_out.set_misc(misc);
|
||||
Some(msg_out)
|
||||
}
|
||||
|
||||
fn check_qos(
|
||||
encoder: &mut Encoder,
|
||||
ratio: &mut f32,
|
||||
spf: &mut Duration,
|
||||
client_record: bool,
|
||||
send_counter: &mut usize,
|
||||
second_instant: &mut Instant,
|
||||
display_idx: usize,
|
||||
) -> ResultType<()> {
|
||||
let mut video_qos = VIDEO_QOS.lock().unwrap();
|
||||
*spf = video_qos.spf();
|
||||
if *ratio != video_qos.ratio() {
|
||||
*ratio = video_qos.ratio();
|
||||
if encoder.support_changing_quality() {
|
||||
allow_err!(encoder.set_quality(*ratio));
|
||||
video_qos.store_bitrate(encoder.bitrate());
|
||||
} else {
|
||||
// Now only vaapi doesn't support changing quality
|
||||
if !video_qos.in_vbr_state() && !video_qos.latest_quality().is_custom() {
|
||||
log::info!("switch to change quality");
|
||||
bail!("SWITCH");
|
||||
}
|
||||
}
|
||||
}
|
||||
if client_record != video_qos.record() {
|
||||
log::info!("switch due to record changed");
|
||||
bail!("SWITCH");
|
||||
}
|
||||
if second_instant.elapsed() > Duration::from_secs(1) {
|
||||
*second_instant = Instant::now();
|
||||
video_qos.update_display_data(display_idx, *send_counter);
|
||||
*send_counter = 0;
|
||||
}
|
||||
drop(video_qos);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
11
src/service.rs
Normal file
11
src/service.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use librustdesk::*;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
crate::common::load_custom_client();
|
||||
hbb_common::init_log(false, "service");
|
||||
crate::start_os_service();
|
||||
}
|
||||
@@ -1739,18 +1739,6 @@ impl<T: InvokeUiSession> Session<T> {
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>, round: u32) {
|
||||
// It is ok to call this function multiple times.
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
all(
|
||||
any(target_os = "linux", target_os = "macos"),
|
||||
feature = "unix-file-copy-paste"
|
||||
)
|
||||
))]
|
||||
if !handler.is_file_transfer() && !handler.is_port_forward() {
|
||||
clipboard::ContextSend::enable(true);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
let (sender, receiver) = mpsc::unbounded_channel::<Data>();
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
|
||||
Reference in New Issue
Block a user