Compare commits

..

47 Commits

Author SHA1 Message Date
RustDesk 8aeafd5401 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-06 15:57:14 +08:00
rustdesk e264283bf2 fix review 2026-07-06 15:46:41 +08:00
rustdesk 1053b53b57 fix copilot false report 2026-07-06 15:31:12 +08:00
rustdesk 233eb49e4d refactor and simplify, remove mutex which is dangeours 2026-07-06 15:17:06 +08:00
rustdesk c974514710 remove wf-cliprdr invariant tests
Drop tests/test_invariant_wf_cliprdr.c on this branch as requested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:27:56 +08:00
rustdesk 52ff648411 remove the dedicated wf-cliprdr CI workflow
Drop .github/workflows/wf-cliprdr-ci.yml on this branch as requested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:16:05 +08:00
rustdesk 55ae167394 harden the format-data path against late/duplicate responses
The format-data rendezvous had the same single-slot race the file-contents
path just fixed: the channel thread rewrote clipboard->hmem with no lock while
explorer-thread consumers read/freed it, nothing serialized concurrent
requests, and no flag told an expected response from a stray one.

- Add format_request_mutex (serializes the whole request/response cycle) and
  hmem_mutex (guards the hmem hand-off and formatDataRespExpected).
- cliprdr_send_data_request now takes ownership of the response buffer under
  hmem_mutex and returns it to the caller, so a later response cannot touch a
  buffer a consumer is using. All three consumers (GetData, WM_RENDERFORMAT,
  DELAYED_RENDERING) and the WM_CLIPBOARDUPDATE cleanup use the returned/taken
  handle instead of the shared slot.
- The response handler drops any response arriving while formatDataRespExpected
  is clear (late/duplicate/unsolicited), consumes the flag on the first
  response, and no longer dereferences a NULL clipboard in the SetEvent path.

Pre-existing issue, not introduced by this branch; generalizes the
file-contents hardening to the format-data path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:23:58 +08:00
rustdesk ba5a535891 key the responder stream cache on connID as well as streamId
Per-stream ids restart from 1 in each peer process, so two connections can
emit the same streamId. The process-static pStreamStc cache keyed only on
streamId could then serve one peer the IStream cached for another peer (a
different file), silently returning wrong-file bytes. Add connID to the key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:08:55 +08:00
rustdesk 3ab9d4e46d serialize file-contents request state and poison streams after timeout
- Extract lock_mutex() for the WAIT_OBJECT_0/WAIT_ABANDONED idiom shared by
  take_req_fdata, the request-serialization acquire, and the response handler.
- Collapse the acquire/send/take/release cycle into
  cliprdr_request_filecontents_sync(), used by CliprdrStream_Read and the size
  probe in CliprdrStream_New.
- Publish req_f_stream_id_expected/req_f_size_requested under req_f_mutex in the
  sender and read them under the same lock in the response handler, removing the
  cross-thread data race on those fields.
- Poison a stream (m_failed) after a request fails/times out, so a late response
  carrying a previous offset's bytes cannot satisfy a later same-stream read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:07:55 +08:00
rustdesk bb8ffef7f0 address copilot review findings in wf_cliprdr.c
- Reject a negative FILECONTENTS_SIZE result: m_lSize is unsigned, so a
  negative value became a huge bogus stream size that keeps reads going.
- Use a unique per-stream counter as the CLIPRDR streamId instead of a
  truncated IStream pointer, which could collide or be reused after free
  (and leaked heap addresses to the peer).
- Add req_f_request_mutex to serialize whole file-contents request/response
  cycles, enforcing the previously assumed one-outstanding-request
  invariant when multiple streams are read concurrently. Bounded acquire
  so a wedged request fails the read instead of hanging a consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:42:32 +08:00
rustdesk 1c2188f80b add invariant tests for file contents request/response hardening
Cover the zeroed optional request fields, stream ID filtering,
oversized/NULL response rejection and the zero-byte EOF path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:31:32 +08:00
rustdesk ff1ca85827 condense hardening comments, fix style in wf_cliprdr.c
Comment-only cleanup of the review-justification comments; also move
the mutex wait result declaration to the top of the block and fix
continuation-line indentation. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:31:26 +08:00
rustdesk f695413ee7 fix review 2026-07-04 15:47:53 +08:00
rustdesk b0555639a2 fix review 2026-07-04 15:20:23 +08:00
RustDesk cd4ed15214 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:55:09 +08:00
RustDesk 96ab03aa65 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:55:00 +08:00
RustDesk a30af8a321 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:54:49 +08:00
RustDesk ed9a423570 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:54:30 +08:00
rustdesk 5cc7355dd2 fix copilot review 2026-07-04 14:41:16 +08:00
rustdesk a9eca51ab0 harden wf_cliprdr.c 2026-07-04 14:03:10 +08:00
fufesou 9fdb8410d3 fix: parse exit code of flutter web (#15501)
* fix: parse exit code of flutter web

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: exit-code, debug print

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-03 12:49:32 +08:00
alonginwind a2b79462ab fix: auto-close terminal tab/window when shell exits (#15448) 2026-07-02 16:43:27 +08:00
fufesou dce221be5a fix(clipboard): make CLIPRDR format-map growth checked (#15493)
* fix(clipboard): make CLIPRDR format-map growth checked

The Windows CLIPRDR format-list handler relies on map_ensure_capacity()
while processing peer-provided formats. The previous helper only attempted
growth: if realloc() failed, it returned silently and the caller continued
processing. A later iteration could then index past the allocated
format_mappings array.

Make format-map growth a checked operation. The handler now validates the
peer-provided format count, ensures the mapping array is large enough before
writing entries, and aborts processing if growth fails. Newly allocated slots
are zeroed so existing cleanup can safely run after partial processing.

Also bound remote format names before measuring/converting them. The chosen
limits follow Windows clipboard/atom constraints:
  - registered clipboard format IDs use 0xC000..0xFFFF
  - string atom names are limited to 255 bytes

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(clipboard): reject invalid remote format-list entries

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-07-02 16:18:07 +08:00
rustdesk 9d1ab3fba3 fix the AOM tile-control argument type 2026-07-01 11:47:08 +08:00
rustdesk b3bd18845d update hbb_common 2026-06-30 11:29:56 +08:00
rustdesk 435f6ec61d update copyright 2026-06-30 11:02:28 +08:00
21pages 0497814004 Add authentication details to connection audit (#15456)
* Add authentication details to connection audit

Signed-off-by: 21pages <sunboeasy@gmail.com>

* rename normalize_conn_audit_primary_auth to normalize_conn_audit_auth_fields

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Merge permanent password audit methods

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Simplify connection audit auth methods

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-29 16:04:24 +08:00
劉清揚 4b1ef9e20d fix(android): sync input service state with Flutter (#15419)
Signed-off-by: liuqiang <2465199797@qq.com>
2026-06-29 15:14:34 +08:00
twprh 10d5250d23 Update flutter-build.yml (#15454) 2026-06-28 17:18:41 +08:00
Maison da Silva 2ee580d49d Update translation for outdated installation message (#15427)
Update translation for outdated installation message
2026-06-28 12:11:03 +08:00
fufesou 4a54029cac fix(update): msi, norestart (#15440)
* fix(update): msi, norestart

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(update): escape path

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-27 16:45:27 +08:00
fufesou 001848bf2f fix(fuse): umount (#15426)
* fix(clipboard): clean up stale Linux FUSE mounts

Recover Linux file clipboard FUSE mount points before remounting and stop treating a cached
context as valid when the mount has already gone away.

This fixes the desktop file manager copy failure that shows dialogs such as
"Error while copying a" and "There was an error copying the file into xxx".

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(clipboard): fuse, reduce dups

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: clear Linux file clipboard before unmounting FUSE

Ensure Linux client teardown clears RustDesk file clipboard URLs while
the FUSE context is still available. Also prefer fusermount before
umount to avoid noisy unprivileged teardown attempts.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(clipboard): return and log errors

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-26 16:17:44 +08:00
21pages 989bf80fe8 Support controller user attribution in audit logs (#15407)
* Support controller user attribution in audit logs

This PR supports associating audit logs with the controller user.

  ## Implementation:
  - Add `ControlledContext { conn_audit_token }` to `PunchHole`, `RequestRelay`, and `FetchLocalAddr`.
  - The server sends a controller-user identity snapshot to the controlled client through rendezvous messages.
  - The controlled client sends the token back to the server when posting the `on_open` conn audit or IP whitelist alarm audit.
  - This lets the server attach the controller user to audit logs.

  ## How the controlled client helps identify the controller user:
  - Conn audit: sends the token to the server in `on_open`; the server creates the audit log and caches the user snapshot.
  - File audit: sends `id` and `conn_id`; the server uses them to find the cached user snapshot.
  - Alarm audit: IP whitelist sends the token directly; other alarm logs send `id` and `conn_id`, and the server uses them to find the cached user
  snapshot.

  ## Compatibility:
  - Supported only for logs created with a new server and a new controlled client.
  - Does not require upgrading the controller client.

  ## Test

  - [x] New/old clients connected to new/old servers, and conn/file/alarm audit logs worked normally.
  - [x] New client connected to new server generated searchable conn/file/alarm audit logs.
  - [x] Punch hole, local addr, and relay paths worked with audit logs and control role on new/old servers.
  - [x] Direct IP connections produced audit logs, but do not support user audit.

Signed-off-by: 21pages <sunboeasy@gmail.com>

* rename conn_audit_token to conn_audit_ref

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-06-26 15:07:27 +08:00
VenusGirl❤ 78b5f47668 Update ko.rs (#15395) 2026-06-26 13:51:08 +08:00
jkh0kr 97e9e44faa Update ko.rs (#15390)
Incorrect translation
2026-06-26 11:19:18 +08:00
RAIT-09 ff226f6d80 fix(clipboard): unix, refresh cached file size/mtime on re-copy (#15392)
* fix(clipboard): unix, refresh cached file size/mtime on re-copy

sync_files() deduped re-copies by path string only, so editing a file
and re-copying it (same path) skipped refreshing the cached size/mtime
and the file-group descriptor; the peer then received the file
truncated to the old cached size (silent corruption for PDF/zip/pptx).
Widen the early-return guard to also compare a top-level (size, mtime)
fingerprint and to always rebuild when a directory is selected. The
Windows wf_cliprdr.c path re-stats per request and is unaffected.

Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>

* opt(clipboard): unix, compute file fingerprint once and pass into sync_files

fingerprint() was computed before taking the CLIP_FILES lock and then
recomputed inside ClipFiles::sync_files under the lock. Pass the precomputed
value in so the top-level stat runs once and outside the critical section.
No behavior change.

Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>

---------

Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>
2026-06-25 09:50:33 +08:00
Daniel Marschall 0cbdb6ffb3 Fix tray icon click (regression due to breaking change in tray-icon 0.17) (#15413) 2026-06-25 09:43:04 +08:00
fufesou b8117c5c34 fix(fuse): fuse path broken, since ipc path changed (#15406)
* fix(fuse): fuse path broken, since ipc path changed

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(fuse): init, handle error

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(fuse): unmount attempt on newly created directory failed

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-24 16:52:27 +08:00
Maison da Silva a69614d464 Update translation for 'Control Actions' in ptbr.rs (#15386) 2026-06-24 12:26:36 +08:00
fufesou 58ee593e26 fix(custom-client): show options, incoming-only (#15394)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-24 00:26:09 +08:00
fufesou 09bc9056c9 fix(update): win aarch64 (#15389)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-23 17:59:29 +08:00
fufesou 0c6df924d1 refact: file transfer, do this for all conflicts(tasks) (#15385)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-23 11:23:42 +08:00
dependabot[bot] 456817b4f4 Git submodule: bump libs/hbb_common from e50ac3c to 387603f (#15384)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `e50ac3c` to `387603f`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](https://github.com/rustdesk/hbb_common/compare/e50ac3cd4897fa6c6ed545189adb2170c34df636...387603f47cbb15c0d3dc3d67ae3396d3eb707daf)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 387603f47cbb15c0d3dc3d67ae3396d3eb707daf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 10:28:08 +08:00
rustdesk 16570ee34f fix https://github.com/rustdesk/rustdesk/discussions/15355 2026-06-22 22:57:08 +08:00
just-some-tall-bloke 2b40c61d8e Fix spelling and grammar errors in comments (#15370)
- dbus.rs: fix grammar (add 'between', pluralize 'processes')
- win_impl.rs: fix typo 'hight' -> 'high', idiom 'such called' -> 'so-called'
- startwm.sh: fix typo 'loging' -> 'logging'
- lib.rs: fix copy-paste error in doc comments for scroll buttons
- message.proto: fix typo 'Clipobard' -> 'Clipboard'
2026-06-22 15:26:51 +08:00
RustDesk dcc64cdeae cjk (#15379) 2026-06-22 14:47:12 +08:00
RustDesk 2747d3d8b4 Revert "fix(arm64-linux): fix CJK font rendering on flutter-elinux (#15324)" (#15377)
This reverts commit c9391fb894.
2026-06-22 13:40:39 +08:00
101 changed files with 1620 additions and 687 deletions
+1
View File
@@ -1984,6 +1984,7 @@ jobs:
sudo apt-get install -y libarchive-tools libfuse2
# set-up appimage-builder
# https://github.com/AppImage/AppImageKit/issues/1395
sudo pip3 install "setuptools_scm<10"
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
# run appimage-builder
pushd appimage
-85
View File
@@ -1,85 +0,0 @@
name: wf-cliprdr CI
on:
workflow_dispatch:
pull_request:
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
push:
branches:
- master
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: wf_cliprdr invariant test
runs-on: windows-2022
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
with:
arch: x64
- name: Setup vcpkg with GitHub Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: C:\vcpkg
doNotCache: false
- name: Install vcpkg dependency
shell: pwsh
run: |
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
- name: Build test
shell: pwsh
run: |
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
New-Item -ItemType Directory -Force $testRoot | Out-Null
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
$cmakeLists = @(
'cmake_minimum_required(VERSION 3.20)'
'project(test_invariant_wf_cliprdr C)'
''
'set(CMAKE_C_STANDARD 11)'
'set(CMAKE_C_STANDARD_REQUIRED ON)'
'set(CMAKE_C_EXTENSIONS OFF)'
''
'find_package(check CONFIG REQUIRED)'
''
'add_executable(test_invariant_wf_cliprdr'
' "TEST_SOURCE"'
')'
''
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
')'
) -join [Environment]::NewLine
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build (Join-Path $testRoot 'out') --config Release
- name: Run test
shell: pwsh
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe
+1 -1
View File
@@ -213,7 +213,7 @@ exclude = ["vdi/host", "examples/custom_plugin"]
libxdo-sys = { path = "libs/libxdo-sys-stub" }
[package.metadata.winres]
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
ProductName = "RustDesk"
FileDescription = "RustDesk Remote Desktop"
OriginalFilename = "rustdesk.exe"
@@ -8,6 +8,7 @@ package com.carriez.flutter_hbb
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.content.Intent
import android.graphics.Path
import android.os.Build
import android.os.Bundle
@@ -68,6 +69,16 @@ class InputService : AccessibilityService() {
get() = ctx != null
}
private fun notifyInputState() {
val inputState = isOpen.toString()
Handler(Looper.getMainLooper()).post {
MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to inputState)
)
}
}
private val logTag = "input service"
private var leftIsDown = false
private var touchPath = Path()
@@ -716,6 +727,7 @@ class InputService : AccessibilityService() {
override fun onServiceConnected() {
super.onServiceConnected()
ctx = this
notifyInputState()
val info = AccessibilityServiceInfo()
if (Build.VERSION.SDK_INT >= 33) {
info.flags = FLAG_INPUT_METHOD_EDITOR or FLAG_RETRIEVE_INTERACTIVE_WINDOWS
@@ -734,8 +746,16 @@ class InputService : AccessibilityService() {
override fun onDestroy() {
ctx = null
// Keep this fallback even though onUnbind usually notifies first.
notifyInputState()
super.onDestroy()
}
override fun onUnbind(intent: Intent?): Boolean {
ctx = null
notifyInputState()
return super.onUnbind(intent)
}
override fun onInterrupt() {}
}
@@ -200,12 +200,13 @@ class MainActivity : FlutterActivity() {
"stop_input" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
InputService.ctx?.disableSelf()
} else {
InputService.ctx = null
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
}
InputService.ctx = null
Companion.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
result.success(true)
}
"cancel_notification" -> {
-16
View File
@@ -598,22 +598,6 @@ class MyTheme {
}
}
/// Applies [fallbacks] as fontFamilyFallback to every text style in both
/// themes. Called once at startup on ARM64 Linux after a CJK font has been
/// loaded via FontLoader (see flutter/flutter#139293).
static void applyFontFallback(List<String> fallbacks) {
lightTheme = lightTheme.copyWith(
textTheme: lightTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
lightTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
darkTheme = darkTheme.copyWith(
textTheme: darkTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
darkTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
}
static ThemeMode currentThemeMode() {
final preference = getThemeModePreference();
if (preference == ThemeMode.system) {
@@ -483,13 +483,15 @@ class _GeneralState extends State<_General> {
}
Widget other() {
final incomingOnly = bind.isIncomingOnly();
final outgoingOnly = bind.isOutgoingOnly();
final showAutoUpdate = isWindows && bind.mainIsInstalled();
final children = <Widget>[
if (!isWeb && !bind.isIncomingOnly())
if (!isWeb && !incomingOnly)
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs,
isServer: false),
if (!bind.isIncomingOnly())
if (!incomingOnly)
_OptionCheckBox(
context,
'allow-remote-toolbar-docking-any-edge',
@@ -499,9 +501,10 @@ class _GeneralState extends State<_General> {
reloadAllWindows();
},
),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb && !outgoingOnly)
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb) wallpaper(),
if (!isWeb && !bind.isIncomingOnly()) ...[
if (!isWeb && !incomingOnly) ...[
_OptionCheckBox(
context,
'Open connection in new tab',
@@ -540,40 +543,40 @@ class _GeneralState extends State<_General> {
isServer: false,
),
),
if (!isWeb && !bind.isCustomClient())
_OptionCheckBox(
context,
'Check for software update on startup',
kOptionEnableCheckUpdate,
isServer: false,
),
if (showAutoUpdate)
_OptionCheckBox(
context,
'Auto update',
kOptionAllowAutoUpdate,
isServer: true,
),
if (isWindows && !bind.isOutgoingOnly())
_OptionCheckBox(
context,
'Capture screen using DirectX',
kOptionDirectxCapture,
),
if (!bind.isIncomingOnly()) ...[
_OptionCheckBox(
context,
'Enable UDP hole punching',
kOptionEnableUdpPunch,
isServer: false,
),
_OptionCheckBox(
context,
'Enable IPv6 P2P connection',
kOptionEnableIpv6Punch,
isServer: false,
),
],
],
if (!isWeb && !bind.isCustomClient())
_OptionCheckBox(
context,
'Check for software update on startup',
kOptionEnableCheckUpdate,
isServer: false,
),
if (showAutoUpdate)
_OptionCheckBox(
context,
'Auto update',
kOptionAllowAutoUpdate,
isServer: true,
),
if (isWindows && !outgoingOnly)
_OptionCheckBox(
context,
'Capture screen using DirectX',
kOptionDirectxCapture,
),
if (!isWeb && !incomingOnly) ...[
_OptionCheckBox(
context,
'Enable UDP hole punching',
kOptionEnableUdpPunch,
isServer: false,
),
_OptionCheckBox(
context,
'Enable IPv6 P2P connection',
kOptionEnableIpv6Punch,
isServer: false,
),
],
];
@@ -2471,7 +2474,7 @@ class _AboutState extends State<_About> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Ltd.\n$license',
'Copyright © ${DateTime.now().toString().substring(0, 4)} Purslane Tech Pte. Ltd.\n$license',
style: const TextStyle(color: Colors.white),
),
Text(
@@ -95,6 +95,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
// Auto-close tab when shell exits
_terminalModel.onClosed = () {
if (mounted) {
widget.tabController.closeBy(widget.tabKey);
}
};
// Initialize terminal connection
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.tabController.onSelected?.call(widget.id);
@@ -1167,8 +1167,8 @@ class _MonitorMenu extends StatelessWidget {
tooltip: isMulti
? ''
: isAllMonitors
? 'all monitors'
: '#${i + 1} monitor',
? 'All monitors'
: '#{${i + 1}} monitor',
hMargin: isMulti ? null : 6,
vMargin: isMulti ? null : 12,
topLevel: false,
@@ -2852,7 +2852,7 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
button = Tooltip(
message: widget.tooltip,
message: translate(widget.tooltip),
child: button,
);
if (widget.topLevel) {
-22
View File
@@ -29,8 +29,6 @@ import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'mobile/widgets/deploy_dialog.dart';
import 'models/platform_model.dart';
import 'native/font_manager.dart'
if (dart.library.html) 'web/font_manager.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
@@ -39,15 +37,10 @@ import 'package:flutter_hbb/plugin/handlers.dart'
int? kWindowId;
WindowType? kWindowType;
late List<String> kBootArgs;
bool _cjkFontLoaded = false;
Future<void> main(List<String> args) async {
earlyAssert();
WidgetsFlutterBinding.ensureInitialized();
_cjkFontLoaded = await loadSystemCJKFonts();
if (_cjkFontLoaded) {
MyTheme.applyFontFallback([kLinuxCjkFontFamily]);
}
debugPrint("launch args: $args");
kBootArgs = List.from(args);
@@ -390,7 +383,6 @@ void _runApp(
builder: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
return child;
},
),
@@ -541,7 +533,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
if ((isDesktop && desktopType == DesktopType.main) ||
isWebDesktop) {
child = keyListenerBuilder(context, child);
@@ -595,19 +586,6 @@ _registerEventHandler() {
}
}
/// Merges the theme's fontFamilyFallback into [DefaultTextStyle] so that
/// bare [Text] widgets (and those with inherit:true styles) also pick up the
/// CJK fallback font loaded on ARM64 Linux.
Widget _mergeCjkFallback(BuildContext context, Widget? child) {
final result = child ?? Container();
final fallback = Theme.of(context).textTheme.bodyMedium?.fontFamilyFallback;
if (fallback == null || fallback.isEmpty) return result;
return DefaultTextStyle.merge(
style: TextStyle(fontFamilyFallback: fallback),
child: result,
);
}
Widget keyListenerBuilder(BuildContext context, Widget? child) {
return RawKeyboardListener(
focusNode: FocusNode(),
@@ -83,6 +83,13 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
// Auto-close connection when shell exits
_terminalModel.onClosed = () {
if (mounted) {
closeConnection(id: widget.id);
}
};
// Web desktop users have full hardware keyboard access, so the on-screen
// terminal extra keys bar is unnecessary and disabled.
_showTerminalExtraKeys = !isWebDesktop &&
+164 -13
View File
@@ -142,12 +142,22 @@ class FileModel {
}
Future<void> postOverrideFileConfirm(Map<String, dynamic> evt) async {
final id = int.tryParse(evt['id']?.toString() ?? '');
if (id == null || !jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore stale override confirm event: $evt");
return;
}
evtLoop.pushEvent(
_FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt));
}
Future<void> overrideFileConfirm(Map<String, dynamic> evt,
{bool? overrideConfirm, bool skip = false}) async {
final id = int.tryParse(evt['id']?.toString() ?? '') ?? 0;
if (id == 0 || !jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore override confirm for inactive job: $evt");
return;
}
// If `skip == true`, it means to skip this file without showing dialog.
// Because `resp` may be null after the user operation or the last remembered operation,
// and we should distinguish them.
@@ -156,15 +166,12 @@ class FileModel {
? await showFileConfirmDialog(translate("Overwrite"),
"${evt['read_path']}", true, evt['is_identical'] == "true")
: null);
final id = int.tryParse(evt['id']) ?? 0;
if (!jobController.hasTransferConflictJob(id)) {
debugPrint("Ignore override confirm result for inactive job: $evt");
return;
}
if (false == resp) {
final jobIndex = jobController.getJob(id);
if (jobIndex != -1) {
await jobController.cancelJob(id);
final job = jobController.jobTable[jobIndex];
job.state = JobState.done;
jobController.jobTable.refresh();
}
await jobController.cancelTransferConflictBatch(id);
} else {
var need_override = false;
if (resp == null) {
@@ -176,6 +183,7 @@ class FileModel {
}
// Update the loop config.
if (fileConfirmCheckboxRemember) {
jobController.rememberTransferConflictBatch(id, resp);
evtLoop.setSkip(!need_override);
}
await bind.sessionSetConfirmOverrideFile(
@@ -285,6 +293,8 @@ class FileModel {
final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden;
final jobID = jobController.addTransferJob(entry, false);
jobController.registerTransferConflictBatch([jobID],
batchId: int.tryParse(obj['batchId']?.toString() ?? ''));
webSendLocalFiles(
handleIndex: handleIndex,
actId: jobID,
@@ -570,8 +580,15 @@ class FileController {
final toPath = otherSideData.directory.path;
final isWindows = otherSideData.options.isWindows;
final showHidden = otherSideData.options.showHidden;
final transferJobs = <(Entry, int)>[];
final transferJobIds = <int>[];
for (var from in items.items) {
final jobID = jobController.addTransferJob(from, isRemoteToLocal);
transferJobs.add((from, jobID));
transferJobIds.add(jobID);
}
jobController.registerTransferConflictBatch(transferJobIds);
for (final (from, jobID) in transferJobs) {
bind.sessionSendFiles(
sessionId: sessionId,
actId: jobID,
@@ -917,6 +934,10 @@ class JobController {
static final JobID jobID = JobID();
final jobTable = List<JobProgress>.empty(growable: true).obs;
final jobResultListener = JobResultListener<Map<String, dynamic>>();
int _nextTransferConflictBatchId = 1;
final Map<int, int> _transferConflictJobToBatch = {};
int? _transferConflictRememberBatchId;
bool? _transferConflictRememberOverrideConfirm;
final GetSessionID getSessionID;
final GetDialogManager getDialogManager;
SessionID get sessionId => getSessionID();
@@ -929,6 +950,57 @@ class JobController {
return jobTable.indexWhere((element) => element.id == id);
}
void registerTransferConflictBatch(Iterable<int> jobIds, {int? batchId}) {
final ids = jobIds.toList(growable: false);
if (ids.isEmpty) {
return;
}
batchId ??= _nextTransferConflictBatchId++;
if (batchId >= _nextTransferConflictBatchId) {
_nextTransferConflictBatchId = batchId + 1;
}
for (final jobId in ids) {
_transferConflictJobToBatch[jobId] = batchId;
}
}
int? transferConflictBatchId(int jobId) {
return _transferConflictJobToBatch[jobId];
}
bool hasTransferConflictJob(int jobId) {
return transferConflictBatchId(jobId) != null;
}
bool isTransferConflictRememberBatch(int? batchId) {
return batchId != null && batchId == _transferConflictRememberBatchId;
}
bool? transferConflictRememberOverrideConfirm(int? batchId) {
if (!isTransferConflictRememberBatch(batchId)) {
return null;
}
return _transferConflictRememberOverrideConfirm;
}
void rememberTransferConflictBatch(int jobId, bool? overrideConfirm) {
_transferConflictRememberBatchId = _transferConflictJobToBatch[jobId];
_transferConflictRememberOverrideConfirm = overrideConfirm;
}
void unregisterTransferConflictJob(int jobId) {
final batchId = _transferConflictJobToBatch.remove(jobId);
if (batchId == null) {
return;
}
if (!_transferConflictJobToBatch.containsValue(batchId)) {
if (_transferConflictRememberBatchId == batchId) {
_transferConflictRememberBatchId = null;
_transferConflictRememberOverrideConfirm = null;
}
}
}
// return jobID
int addTransferJob(Entry from, bool isRemoteToLocal) {
final jobID = JobController.jobID.next();
@@ -1000,7 +1072,10 @@ class JobController {
id = int.parse(evt['id']);
} catch (_) {}
final jobIndex = getJob(id);
if (jobIndex == -1) return true;
if (jobIndex == -1) {
unregisterTransferConflictJob(id);
return true;
}
final job = jobTable[jobIndex];
job.recvJobRes = true;
if (job.type == JobType.deleteFile) {
@@ -1026,6 +1101,9 @@ class JobController {
job.state = JobState.done;
}
jobTable.refresh();
if (job.state == JobState.done || job.state == JobState.error) {
unregisterTransferConflictJob(id);
}
if (job.type == JobType.deleteDir) {
return job.state == JobState.done;
} else {
@@ -1035,9 +1113,15 @@ class JobController {
void jobError(Map<String, dynamic> evt) {
final err = evt['err'].toString();
int jobIndex = getJob(int.parse(evt['id']));
final id = int.tryParse(evt['id']?.toString() ?? '');
if (id == null) {
debugPrint("Ignore job error with invalid id: $evt");
return;
}
int jobIndex = getJob(id);
if (jobIndex != -1) {
final job = jobTable[jobIndex];
if (job.state == JobState.done && job.err == "cancel") return;
job.state = JobState.error;
job.err = err;
job.recvJobRes = true;
@@ -1060,6 +1144,11 @@ class JobController {
}
}
jobTable.refresh();
if (job.state == JobState.done || job.state == JobState.error) {
unregisterTransferConflictJob(job.id);
}
} else {
unregisterTransferConflictJob(id);
}
if (err == _kOneWayFileTransferError) {
if (DateTime.now().millisecondsSinceEpoch - _lastTimeShowMsgbox > 3000) {
@@ -1096,9 +1185,42 @@ class JobController {
}
Future<void> cancelJob(int id) async {
unregisterTransferConflictJob(id);
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
}
Future<void> cancelTransferConflictBatch(int jobId) async {
final batchId = _transferConflictJobToBatch[jobId];
final batchJobIds = batchId == null ? [jobId] : <int>[];
if (batchId != null) {
for (final entry in _transferConflictJobToBatch.entries) {
if (entry.value == batchId) {
batchJobIds.add(entry.key);
}
}
for (final id in batchJobIds) {
unregisterTransferConflictJob(id);
}
}
final jobIdsToCancel = batchJobIds.toSet();
for (final job in jobTable) {
if (!jobIdsToCancel.contains(job.id) || job.state == JobState.done) {
continue;
}
job.state = JobState.done;
job.err = "cancel";
job.recvJobRes = true;
}
jobTable.refresh();
for (final id in batchJobIds) {
try {
await bind.sessionCancelJob(sessionId: sessionId, actId: id);
} catch (e) {
debugPrint("Failed to cancel transfer job $id in conflict batch: $e");
}
}
}
Future<void> loadLastJob(Map<String, dynamic> evt) async {
debugPrint("load last job: $evt");
Map<String, dynamic> jobDetail = json.decode(evt['value']);
@@ -1145,7 +1267,7 @@ class JobController {
..state = JobState.paused;
jobTable.add(jobProgress);
}
registerTransferConflictBatch([currJobId]);
await bind.sessionAddJob(
sessionId: sessionId,
isRemote: isRemote,
@@ -1193,6 +1315,9 @@ class JobController {
void clear() {
jobTable.clear();
_transferConflictJobToBatch.clear();
_transferConflictRememberBatchId = null;
_transferConflictRememberOverrideConfirm = null;
jobResultListener.clear();
}
}
@@ -1535,6 +1660,9 @@ class JobProgress {
String display() {
if (type == JobType.transfer) {
if (state == JobState.done && err == "cancel") {
return translate("Cancel");
}
if (state == JobState.done && err == "skipped") {
return translate("Skipped");
}
@@ -1844,21 +1972,44 @@ class _FileDialogEvent extends BaseEvent<FileDialogType, Map<String, dynamic>> {
class FileDialogEventLoop
extends BaseEventLoop<FileDialogType, Map<String, dynamic>> {
int? _batchId;
bool? _overrideConfirm;
bool _skip = false;
@override
Future<void> onPreConsume(
BaseEvent<FileDialogType, Map<String, dynamic>> evt) async {
var event = evt as _FileDialogEvent;
final event = evt as _FileDialogEvent;
final model = event.fileModel.target;
final jobId = int.tryParse(evt.data['id']?.toString() ?? '');
final batchId = model == null || jobId == null
? null
: model.jobController.transferConflictBatchId(jobId);
final keepRemembered = model != null &&
model.jobController.isTransferConflictRememberBatch(batchId);
// The loop only preloads the remembered batch choice. The model updates it
// after the user answers the current overwrite dialog.
if (_batchId != batchId && !keepRemembered) {
_batchId = batchId;
_overrideConfirm = null;
_skip = false;
} else {
_batchId = batchId;
}
if (keepRemembered) {
_overrideConfirm =
model.jobController.transferConflictRememberOverrideConfirm(batchId);
_skip = _overrideConfirm == null;
}
event.setOverrideConfirm(_overrideConfirm);
event.setSkip(_skip);
debugPrint(
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} overrideConfirm: $_overrideConfirm, skip: $_skip>");
"FileDialogEventLoop: consuming<jobId: ${evt.data['id']} batchId: $_batchId overrideConfirm: $_overrideConfirm, skip: $_skip>");
}
@override
Future<void> onEventsClear() {
_batchId = null;
_overrideConfirm = null;
_skip = false;
return super.onEventsClear();
+34 -2
View File
@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
@@ -38,6 +37,10 @@ class TerminalModel with ChangeNotifier {
void Function(int w, int h, int pw, int ph)? onResizeExternal;
/// Called when the terminal session ends (shell exits).
/// The listener (typically TerminalPage) can use this to auto-close the tab/page.
VoidCallback? onClosed;
Future<void> _handleInput(String data) async {
// Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a
// real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'.
@@ -247,6 +250,33 @@ class TerminalModel with ChangeNotifier {
}
}
static int getExitCodeFromEvt(Map<String, dynamic> evt) {
if (evt.containsKey('exit_code')) {
final v = evt['exit_code'];
if (v is int) {
// Desktop and mobile send exit_code as an int
return v;
} else if (v is String) {
// Web sends exit_code as a string
final parsed = int.tryParse(v);
if (parsed != null) {
return parsed;
} else {
debugPrint(
'[TerminalModel] Failed to parse exit_code as integer: $v. Expected a numeric string.');
return 0;
}
} else {
debugPrint(
'[TerminalModel] Unexpected exit_code type: ${v.runtimeType}, value: $v. Expected int or String.');
return 0;
}
} else {
debugPrint('[TerminalModel] Event does not contain exit_code');
return 0;
}
}
void handleTerminalResponse(Map<String, dynamic> evt) {
final String? type = evt['type'];
final int evtTerminalId = getTerminalIdFromEvt(evt);
@@ -469,10 +499,12 @@ class TerminalModel with ChangeNotifier {
}
void _handleTerminalClosed(Map<String, dynamic> evt) {
final int exitCode = evt['exit_code'] ?? 0;
final int exitCode = getExitCodeFromEvt(evt);
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
_terminalOpened = false;
notifyListeners();
// Auto-close the tab/page
onClosed?.call();
}
void _handleTerminalError(Map<String, dynamic> evt) {
-109
View File
@@ -1,109 +0,0 @@
import 'dart:ffi' show Abi;
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// Font family name registered with [FontLoader] when a system CJK font is
/// successfully loaded on ARM64 Linux.
const kLinuxCjkFontFamily = 'SystemCJK';
const _kFontSearchPaths = [
// Debian / Ubuntu (noto-fonts / fonts-noto-cjk)
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf',
// Fedora / RHEL / Rocky (google-noto-sans-cjk-fonts)
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc',
// Arch Linux (noto-fonts-cjk)
'/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf',
// Generic fallback paths
'/usr/share/fonts/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto/NotoSansCJKsc-Regular.otf',
// WenQuanYi commonly pre-installed on CJK-locale systems
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/wqy-microhei/wqy-microhei.ttc',
'/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc',
];
/// Loads a system CJK font on ARM64 Linux into Flutter's font registry via
/// [FontLoader], working around the missing fontconfig support in the
/// flutter-elinux engine (https://github.com/flutter/flutter/issues/139293).
///
/// Returns true if a CJK font was successfully loaded; false otherwise.
/// On all other platforms this is a no-op and returns false immediately.
Future<bool> loadSystemCJKFonts() async {
if (Abi.current() != Abi.linuxArm64) return false;
final path = await _findCjkFontPath();
if (path == null) {
debugPrint('ARM64 Linux: no CJK font found; CJK text may not render');
return false;
}
try {
final loader = FontLoader(kLinuxCjkFontFamily);
final bytes = await File(path).readAsBytes();
loader.addFont(Future.value(ByteData.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes)));
await loader.load();
debugPrint('ARM64 Linux: loaded CJK font from $path');
return true;
} catch (e) {
debugPrint('ARM64 Linux: failed to load CJK font: $e');
return false;
}
}
Future<String?> _findCjkFontPath() async {
// Query fc-list for each CJK script separately. Fonts present in all three
// sets (zh ja ko) are true pan-CJK fonts; prefer them so we don't
// accidentally pick a Chinese-only font that lacks Japanese kana or Korean
// hangul glyphs. fc-list is a fontconfig CLI tool available on most Linux
// systems independent of whether the Flutter engine was built with fontconfig.
final byLang = <String, Set<String>>{};
for (final lang in const ['zh', 'ja', 'ko']) {
final paths = <String>{};
try {
final r =
await Process.run('fc-list', [':lang=$lang', '--format=%{file}\n']);
if (r.exitCode == 0) {
for (final line in r.stdout.toString().split('\n')) {
final p = line.trim();
if (p.isNotEmpty && File(p).existsSync()) paths.add(p);
}
}
} catch (e) {
debugPrint('ARM64 Linux: fc-list failed for lang=$lang: $e');
}
byLang[lang] = paths;
}
final panCjk = byLang['zh']!
.intersection(byLang['ja']!)
.intersection(byLang['ko']!);
final anyCjk =
byLang.values.fold(<String>{}, (acc, s) => acc..addAll(s));
// Among candidates, prefer well-known pan-CJK font families.
String? pick(Iterable<String> pool) {
const preferred = ['notosanscjk', 'sourcehansans', 'sourcehanserif'];
for (final name in preferred) {
for (final p in pool) {
if (p.toLowerCase().contains(name)) return p;
}
}
return pool.isNotEmpty ? pool.first : null;
}
final found = pick(panCjk) ?? pick(anyCjk);
if (found != null) return found;
for (final p in _kFontSearchPaths) {
if (File(p).existsSync()) return p;
}
return null;
}
-8
View File
@@ -1,8 +0,0 @@
/// Web stub for `native/font_manager.dart`.
///
/// The native implementation depends on `dart:io` (Process/File/Platform) to
/// load a system CJK font on ARM64 Linux, which cannot compile for the web
/// target. The web build has no such fontconfig limitation, so this is a no-op.
const kLinuxCjkFontFamily = 'SystemCJK';
Future<bool> loadSystemCJKFonts() async => false;
@@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved.
PRODUCT_COPYRIGHT = Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved.
+2 -2
View File
@@ -89,11 +89,11 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "Purslane Ltd" "\0"
VALUE "CompanyName", "Purslane Tech Pte. Ltd." "\0"
VALUE "FileDescription", "RustDesk Remote Desktop" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "rustdesk" "\0"
VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0"
VALUE "LegalCopyright", "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved." "\0"
VALUE "OriginalFilename", "rustdesk.exe" "\0"
VALUE "ProductName", "RustDesk" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
+3 -10
View File
@@ -533,7 +533,7 @@ impl FuseServer {
offset: i64,
size: u32,
) -> Result<Vec<u8>, std::io::Error> {
// todo: async and concurrent read, generate stream_id per request
let request_stream_id = rand::random();
let cb_requested = unsafe {
// convert `size` from u32 to i32
// yet with same bit representation
@@ -543,7 +543,7 @@ impl FuseServer {
let (n_position_high, n_position_low) =
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
let request = ClipboardFile::FileContentsRequest {
stream_id: node.stream_id,
stream_id: request_stream_id,
list_index: node.index as i32,
dw_flags: 2,
n_position_low,
@@ -573,7 +573,7 @@ impl FuseServer {
stream_id,
requested_data,
} => {
if stream_id != node.stream_id {
if stream_id != request_stream_id {
log::debug!("stream id mismatch, ignore");
continue;
}
@@ -611,11 +611,6 @@ struct FuseNode {
/// connection id
pub conn_id: i32,
// todo: use stream_id to identify a FileContents request-reply
// instead of a whole file
/// stream id
pub stream_id: i32,
/// file index in peer's file list
/// NOTE:
/// it is NOT the same as inode, this is the index in the file list
@@ -639,7 +634,6 @@ impl FuseNode {
pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
Self {
conn_id: desc.conn_id,
stream_id: rand::random(),
index: inode as usize - 2,
name: desc
.name
@@ -656,7 +650,6 @@ impl FuseNode {
pub fn new_root() -> Self {
Self {
conn_id: 0,
stream_id: rand::random(),
index: 0,
name: String::from("/"),
parent: None,
+352 -21
View File
@@ -4,23 +4,24 @@ use super::filetype::FileDescription;
use crate::{ClipboardFile, CliprdrError};
use cs::FuseServer;
use fuser::MountOption;
use hbb_common::{config::APP_NAME, log};
use hbb_common::{config::Config, log};
use parking_lot::Mutex;
use std::{
path::PathBuf,
io,
path::{Path, PathBuf},
sync::{mpsc::Sender, Arc},
time::Duration,
};
lazy_static::lazy_static! {
static ref FUSE_MOUNT_POINT_CLIENT: Arc<String> = {
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client");
let mnt_path = fuse_mount_point("cliprdr-client");
// No need to run `canonicalize()` here.
Arc::new(mnt_path)
};
static ref FUSE_MOUNT_POINT_SERVER: Arc<String> = {
let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server");
let mnt_path = fuse_mount_point("cliprdr-server");
// No need to run `canonicalize()` here.
Arc::new(mnt_path)
};
@@ -31,6 +32,21 @@ lazy_static::lazy_static! {
static FUSE_TIMEOUT: Duration = Duration::from_secs(3);
#[derive(Debug, PartialEq, Eq)]
enum MountPointState {
HealthyMount,
NotMounted,
StaleMount,
Unknown,
}
fn fuse_mount_point(name: &str) -> String {
let mut path = PathBuf::from(Config::ipc_path(""));
path.pop();
path.push(name);
path.to_string_lossy().to_string()
}
pub fn get_exclude_paths(is_client: bool) -> Arc<String> {
if is_client {
FUSE_MOUNT_POINT_CLIENT.clone()
@@ -53,8 +69,27 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
} else {
FUSE_CONTEXT_SERVER.lock()
};
if fuse_context_lock.is_some() {
return Ok(());
if let Some(ctx) = fuse_context_lock.as_ref() {
match inspect_mount_point_state(&ctx.mount_point) {
MountPointState::HealthyMount => return Ok(()),
MountPointState::StaleMount | MountPointState::NotMounted => {
log::warn!(
"clipboard FUSE mount {} is disconnected, remounting",
ctx.mount_point.display()
);
let stale_context = fuse_context_lock.take();
drop(fuse_context_lock);
drop(stale_context);
return init_fuse_context(is_client);
}
MountPointState::Unknown => {
log::warn!(
"failed to verify clipboard FUSE mount {}",
ctx.mount_point.display()
);
return Err(CliprdrError::CliprdrInit);
}
}
}
let mount_point = if is_client {
FUSE_MOUNT_POINT_CLIENT.clone()
@@ -63,10 +98,32 @@ pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> {
};
let mount_point = std::path::PathBuf::from(&*mount_point);
match inspect_mount_point_state(&mount_point) {
MountPointState::HealthyMount => {
log::warn!(
"clipboard FUSE mount {} is already active in another context",
mount_point.display()
);
return Err(CliprdrError::ClipboardOccupied);
}
MountPointState::StaleMount => {
log::warn!(
"clipboard FUSE mount {} is stale, cleaning up before remount",
mount_point.display()
);
unmount_fuse_mount_point(&mount_point);
validate_mount_state_after_stale_cleanup(
&mount_point,
inspect_mount_point_state(&mount_point),
)?;
}
MountPointState::Unknown => return Err(CliprdrError::CliprdrInit),
MountPointState::NotMounted => {}
}
let (server, tx) = FuseServer::new(FUSE_TIMEOUT);
let server = Arc::new(Mutex::new(server));
prepare_fuse_mount_point(&mount_point);
prepare_fuse_mount_point(&mount_point)?;
let mnt_opts = [
MountOption::FSName("rustdesk-cliprdr-fs".to_string()),
MountOption::NoAtime,
@@ -159,35 +216,246 @@ struct FuseContext {
}
// this function must be called after the main IPC is up
fn prepare_fuse_mount_point(mount_point: &PathBuf) {
fn prepare_fuse_mount_point(mount_point: &PathBuf) -> Result<(), CliprdrError> {
use std::{
fs::{self, Permissions},
os::unix::prelude::PermissionsExt,
};
fs::create_dir(mount_point).ok();
fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok();
if let Some(parent) = mount_point.parent() {
reject_symlink_path(parent)?;
if let Err(e) = fs::create_dir_all(parent) {
log::warn!("failed to create FUSE mount parent {:?}: {:?}", parent, e);
return Err(CliprdrError::CliprdrInit);
}
}
if let Err(e) = std::process::Command::new("umount")
.arg(mount_point)
.status()
{
log::warn!("umount {:?} may fail: {:?}", mount_point, e);
reject_symlink_path(mount_point)?;
let recovered_stale_mount = if let Err(e) = fs::create_dir_all(mount_point) {
log::warn!(
"failed to create clipboard FUSE mount point {}, trying stale mount cleanup: {:?}",
mount_point.display(),
e
);
unmount_fuse_mount_point(mount_point);
fs::create_dir_all(mount_point).map_err(|e| {
log::error!(
"failed to create clipboard FUSE mount point {} after cleanup: {:?}",
mount_point.display(),
e
);
CliprdrError::CliprdrInit
})?;
true
} else {
false
};
if let Err(e) = fs::set_permissions(mount_point, Permissions::from_mode(0o777)) {
log::warn!(
"failed to set clipboard FUSE mount point permissions {}: {:?}",
mount_point.display(),
e
);
}
if !recovered_stale_mount {
unmount_fuse_mount_point(mount_point);
}
Ok(())
}
fn inspect_mount_point_state(mount_point: &Path) -> MountPointState {
if ensure_mount_point_path_is_safe(mount_point).is_err() {
return MountPointState::Unknown;
}
inspect_mount_point_state_with(
mount_point,
std::fs::metadata(mount_point),
std::fs::read_to_string("/proc/self/mountinfo"),
)
}
fn validate_mount_state_after_stale_cleanup(
mount_point: &Path,
mount_state: MountPointState,
) -> Result<(), CliprdrError> {
match mount_state {
MountPointState::NotMounted => Ok(()),
MountPointState::HealthyMount => {
log::warn!(
"clipboard FUSE mount {} is still active after stale cleanup",
mount_point.display()
);
Err(CliprdrError::ClipboardOccupied)
}
MountPointState::StaleMount => {
log::warn!(
"clipboard FUSE mount {} is still stale after cleanup",
mount_point.display()
);
Err(CliprdrError::CliprdrInit)
}
MountPointState::Unknown => {
log::warn!(
"failed to verify clipboard FUSE mount {} after cleanup",
mount_point.display()
);
Err(CliprdrError::CliprdrInit)
}
}
}
fn uninit_fuse_context_(is_client: bool) {
if is_client {
let _ = FUSE_CONTEXT_CLIENT.lock().take();
} else {
let _ = FUSE_CONTEXT_SERVER.lock().take();
fn inspect_mount_point_state_with<T>(
mount_point: &Path,
metadata_result: io::Result<T>,
mountinfo_result: io::Result<String>,
) -> MountPointState {
match metadata_result {
Ok(_) => match mountinfo_result {
Ok(mountinfo) => {
if is_mount_point_listed_in_mountinfo(mount_point, &mountinfo) {
MountPointState::HealthyMount
} else {
MountPointState::NotMounted
}
}
Err(e) => {
log::warn!("failed to read mountinfo for {:?}: {:?}", mount_point, e);
MountPointState::Unknown
}
},
Err(e) if e.raw_os_error() == Some(libc::ENOTCONN) => MountPointState::StaleMount,
Err(e) if e.kind() == io::ErrorKind::NotFound => MountPointState::NotMounted,
Err(e) => {
log::warn!("failed to inspect FUSE mount {:?}: {:?}", mount_point, e);
MountPointState::Unknown
}
}
}
fn is_mount_point_listed_in_mountinfo(mount_point: &Path, mountinfo: &str) -> bool {
let mount_point = mount_point.to_string_lossy();
mountinfo.lines().any(|line| {
let mut fields = line.split_whitespace();
let _mount_id = fields.next();
let _parent_id = fields.next();
let _major_minor = fields.next();
let _root = fields.next();
let mount_path = fields.next();
mount_path == Some(mount_point.as_ref())
})
}
fn reject_symlink_metadata_result(
path: &Path,
metadata_result: io::Result<std::fs::Metadata>,
allow_disconnected_mount: bool,
) -> Result<(), CliprdrError> {
match metadata_result {
Ok(metadata) if metadata.file_type().is_symlink() => {
log::warn!("refusing to use symlinked FUSE path {:?}", path);
Err(CliprdrError::CliprdrInit)
}
Ok(_) => Ok(()),
Err(e) if allow_disconnected_mount && e.raw_os_error() == Some(libc::ENOTCONN) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => {
log::warn!("failed to inspect FUSE path {:?}: {:?}", path, e);
Err(CliprdrError::CliprdrInit)
}
}
}
fn reject_symlink_path(path: &Path) -> Result<(), CliprdrError> {
reject_symlink_metadata_result(path, std::fs::symlink_metadata(path), false)
}
fn ensure_mount_point_path_is_safe(mount_point: &Path) -> Result<(), CliprdrError> {
if let Some(parent) = mount_point.parent() {
reject_symlink_path(parent)?;
}
reject_symlink_metadata_result(mount_point, std::fs::symlink_metadata(mount_point), true)
}
fn unmount_fuse_mount_point(mount_point: &Path) {
if ensure_mount_point_path_is_safe(mount_point).is_err() {
log::warn!(
"refusing to unmount unsafe clipboard FUSE mount point {:?}",
mount_point
);
return;
}
if inspect_mount_point_state_with(
mount_point,
std::fs::metadata(mount_point),
std::fs::read_to_string("/proc/self/mountinfo"),
) == MountPointState::NotMounted
{
return;
}
for (program, args) in unmount_command_candidates() {
if run_unmount_command(program, args, mount_point) {
return;
}
}
log::warn!(
"failed to unmount clipboard FUSE mount point {:?}",
mount_point
);
}
fn unmount_command_candidates() -> [(&'static str, &'static [&'static str]); 3] {
[
("fusermount3", &["-uz"]),
("fusermount", &["-uz"]),
("umount", &["-l"]),
]
}
fn run_unmount_command(program: &str, args: &[&str], mount_point: &Path) -> bool {
match std::process::Command::new(program)
.args(args)
.arg(mount_point)
.status()
{
Ok(status) if status.success() => {}
Ok(status) => {
log::debug!(
"{} {:?} exited with status {:?}",
program,
mount_point,
status.code()
);
return false;
}
Err(e) => {
log::debug!("failed to run {} for {:?}: {:?}", program, mount_point, e);
return false;
}
}
true
}
fn uninit_fuse_context_(is_client: bool) {
let ctx = {
let mut fuse_context_lock = if is_client {
FUSE_CONTEXT_CLIENT.lock()
} else {
FUSE_CONTEXT_SERVER.lock()
};
fuse_context_lock.take()
};
drop(ctx);
}
impl Drop for FuseContext {
fn drop(&mut self) {
self.session.lock().take().map(|s| s.join());
log::info!("unmounting clipboard FUSE from {}", self.mount_point.display());
log::info!(
"unmounting clipboard FUSE from {}",
self.mount_point.display()
);
}
}
@@ -223,3 +491,66 @@ impl FuseContext {
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, io};
#[cfg(target_family = "unix")]
use std::os::unix::fs::symlink;
#[test]
fn classifies_mount_point_state_from_metadata_and_mountinfo() {
let mount_point = std::env::temp_dir().join(format!(
"rustdesk-fuse-mount-state-test-{}-{}",
std::process::id(),
line!()
));
let mountinfo = format!(
"123 1 0:45 / {} rw,nosuid,nodev - fuse.rustdesk rustdesk rw\n",
mount_point.display()
);
assert_eq!(
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(mountinfo)),
MountPointState::HealthyMount
);
assert_eq!(
inspect_mount_point_state_with(&mount_point, Ok(()), Ok(String::new())),
MountPointState::NotMounted
);
let disconnected_metadata: io::Result<()> =
Err(io::Error::from_raw_os_error(libc::ENOTCONN));
assert_eq!(
inspect_mount_point_state_with(&mount_point, disconnected_metadata, Ok(String::new())),
MountPointState::StaleMount
);
}
#[test]
#[cfg(target_family = "unix")]
fn rejects_symlink_mount_point() {
let base = std::env::temp_dir().join(format!(
"rustdesk-fuse-symlink-test-{}-{}",
std::process::id(),
line!()
));
let mount_parent = base.join("parent");
let mount_point = mount_parent.join("cliprdr-client");
let symlink_target = base.join("symlink-target");
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&base).unwrap();
fs::create_dir_all(&mount_parent).unwrap();
fs::create_dir_all(&symlink_target).unwrap();
symlink(&symlink_target, &mount_point).unwrap();
assert!(matches!(
prepare_fuse_mount_point(&mount_point),
Err(CliprdrError::CliprdrInit)
));
let _ = fs::remove_dir_all(&base);
}
}
+46 -12
View File
@@ -192,20 +192,16 @@ impl LocalFile {
});
};
if offset != self.offset.load(Ordering::Relaxed) {
let read_result = if offset != self.offset.load(Ordering::Relaxed) {
handle
.seek(std::io::SeekFrom::Start(offset))
.map_err(|e| CliprdrError::FileError {
path: self.path.to_string_lossy().to_string(),
err: e,
})?;
.and_then(|_| handle.read_exact(buf))
} else {
handle.read_exact(buf)
};
if let Err(e) = read_result {
return Err(self.invalidate_handle(e));
}
handle
.read_exact(buf)
.map_err(|e| CliprdrError::FileError {
path: self.path.to_string_lossy().to_string(),
err: e,
})?;
let new_offset = offset + (buf.len() as u64);
self.offset.store(new_offset, Ordering::Relaxed);
@@ -217,6 +213,15 @@ impl LocalFile {
Ok(())
}
fn invalidate_handle(&mut self, err: std::io::Error) -> CliprdrError {
self.offset.store(0, Ordering::Relaxed);
self.handle = None;
CliprdrError::FileError {
path: self.path.to_string_lossy().to_string(),
err,
}
}
}
pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, CliprdrError> {
@@ -278,7 +283,10 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result<Vec<LocalFile>, C
#[cfg(test)]
mod file_list_test {
use std::{path::PathBuf, sync::atomic::AtomicU64};
use std::{
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
};
use hbb_common::bytes::{BufMut, BytesMut};
@@ -384,4 +392,30 @@ mod file_list_test {
as_bin_parse_test("/test")?;
Ok(())
}
#[test]
fn read_exact_at_reopens_after_read_failure() -> Result<(), Box<dyn std::error::Error>> {
let file_path = std::env::temp_dir().join(format!(
"rustdesk-clipboard-local-file-{}",
std::process::id()
));
std::fs::write(&file_path, b"")?;
let mut file = LocalFile::try_open(&std::env::temp_dir(), &file_path)?;
file.size = 1;
let mut buf = [0u8; 1];
assert!(file.read_exact_at(&mut buf, 0).is_err());
assert!(file.handle.is_none());
assert_eq!(file.offset.load(Ordering::Relaxed), 0);
std::fs::write(&file_path, [42u8])?;
file.read_exact_at(&mut buf, 0)?;
assert_eq!(buf, [42u8]);
assert!(file.handle.is_none());
std::fs::remove_file(file_path)?;
Ok(())
}
}
+150 -4
View File
@@ -5,7 +5,7 @@ use hbb_common::{
log,
};
use parking_lot::Mutex;
use std::{path::PathBuf, sync::Arc, usize};
use std::{path::PathBuf, sync::Arc, time::SystemTime, usize};
lazy_static::lazy_static! {
// local files are cached, this value should not be changed when copying files
@@ -30,9 +30,35 @@ enum FileContentsRequest {
},
}
// Cheap fingerprint of one top-level selected entry. A change in size/mtime --
// or a directory in the selection -- forces sync_files() to rebuild (see below).
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct FileSig {
size: u64,
mtime: Option<SystemTime>,
is_dir: bool,
}
// Stat the top-level selected paths only (no recursion), same order as `files`.
fn fingerprint(files: &[String]) -> Vec<FileSig> {
files
.iter()
.map(|s| match std::fs::metadata(s) {
Ok(mt) => FileSig {
size: mt.len(),
mtime: mt.modified().ok(),
is_dir: mt.is_dir(),
},
Err(_) => FileSig::default(),
})
.collect()
}
#[derive(Default)]
struct ClipFiles {
files: Vec<String>,
// Fingerprint of `files` (same len/order); detects in-place edits on re-copy.
sigs: Vec<FileSig>,
file_list: Vec<LocalFile>,
first_file_index: usize,
files_pdu: Vec<u8>,
@@ -41,12 +67,17 @@ struct ClipFiles {
impl ClipFiles {
fn clear(&mut self) {
self.files.clear();
self.sigs.clear();
self.file_list.clear();
self.first_file_index = usize::MAX;
self.files_pdu.clear();
}
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
fn sync_files(
&mut self,
clipboard_files: &[String],
sigs: Vec<FileSig>,
) -> Result<(), CliprdrError> {
let clipboard_paths = clipboard_files
.iter()
.map(|s| PathBuf::from(s))
@@ -58,6 +89,7 @@ impl ClipFiles {
.position(|f| !f.path.is_dir())
.unwrap_or(usize::MAX);
self.files = clipboard_files.to_vec();
self.sigs = sigs;
Ok(())
}
@@ -258,14 +290,128 @@ pub fn read_file_contents(
}
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
// Dedup: skip the rebuild only when paths + sizes + mtimes match and no dir is
// selected (a dir's own mtime doesn't change when a file inside it is edited).
let current = fingerprint(files);
let mut files_lock = CLIP_FILES.lock();
if files_lock.files == files {
if files_lock.files == files
&& files_lock.sigs == current
&& !current.iter().any(|sig| sig.is_dir)
{
return Ok(());
}
files_lock.sync_files(files)?;
files_lock.sync_files(files, current)?;
Ok(files_lock.build_file_list_pdu())
}
pub fn get_file_list_pdu() -> Vec<u8> {
CLIP_FILES.lock().files_pdu.clone()
}
#[cfg(test)]
mod sig_test {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
// Unique temp dir under the system temp dir; removed on drop (no dev-dep).
struct TmpDir(PathBuf);
impl TmpDir {
fn new(tag: &str) -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut dir = std::env::temp_dir();
dir.push(format!("rustdesk_sig_test_{}_{}", tag, nanos));
fs::create_dir_all(&dir).unwrap();
TmpDir(dir)
}
fn join(&self, name: &str) -> PathBuf {
self.0.join(name)
}
}
impl Drop for TmpDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn path_str(p: &PathBuf) -> String {
p.to_string_lossy().to_string()
}
#[test]
fn fingerprint_missing_path_is_default() {
let tmp = TmpDir::new("missing");
let missing = path_str(&tmp.join("does_not_exist.bin"));
let sigs = fingerprint(&[missing]);
assert_eq!(sigs.len(), 1);
// A path that can't be stat'd -> default sig, which forces a rebuild.
assert_eq!(sigs[0], FileSig::default());
assert_eq!(sigs[0].mtime, None);
}
#[test]
fn fingerprint_detects_inplace_edit() {
let tmp = TmpDir::new("edit");
let file = tmp.join("a.bin");
fs::write(&file, b"small").unwrap();
let p = path_str(&file);
let before = fingerprint(&[p.clone()]);
// Same content, same path: fingerprint must be stable.
let again = fingerprint(&[p.clone()]);
assert_eq!(before, again);
assert_eq!(before[0].size, 5);
assert!(!before[0].is_dir);
// Edit in place so the file grows.
fs::write(&file, b"much larger contents than before").unwrap();
let after = fingerprint(&[p]);
assert_ne!(before, after);
assert!(after[0].size > before[0].size);
}
#[test]
fn fingerprint_flags_directory() {
let tmp = TmpDir::new("dir");
let sub = tmp.join("subdir");
fs::create_dir_all(&sub).unwrap();
let sigs = fingerprint(&[path_str(&sub)]);
assert_eq!(sigs.len(), 1);
assert!(sigs[0].is_dir);
}
#[test]
fn recopy_after_edit_refreshes_cached_size() {
let tmp = TmpDir::new("recopy");
let file = tmp.join("doc.bin");
fs::write(&file, b"v1").unwrap(); // 2 bytes
let files = vec![path_str(&file)];
// Drive the public, guarded `sync_files` over the global CLIP_FILES;
// reset first (this is the only test that touches the global).
clear_files();
sync_files(&files).unwrap();
{
let cache = CLIP_FILES.lock();
let idx = cache.first_file_index;
assert_eq!(cache.file_list[idx].size, 2);
}
// In-place edit grows the file; the re-copy must rebuild. Pre-fix the
// path-only guard early-returned and left the cached size stale at 2.
fs::write(&file, b"v2 is bigger").unwrap(); // 12 bytes
sync_files(&files).unwrap();
{
let cache = CLIP_FILES.lock();
let idx = cache.first_file_index;
assert_eq!(cache.file_list[idx].size, 12);
}
clear_files(); // leave the global clean for other tests
}
}
+168 -36
View File
@@ -41,6 +41,14 @@
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
#define WF_CLIPRDR_MAX_STREAMS 16384
/* Registered clipboard formats use IDs 0xC000 through 0xFFFF.
* https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclipboardformatw */
#define WF_CLIPRDR_MAX_FORMATS 0x4000u
/* Registered format names are string atoms; cap the converted WCHAR name.
* https://learn.microsoft.com/en-us/windows/win32/dataxchg/about-atom-tables */
#define WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS 255u
/* Bound the peer-provided UTF-8 scan separately from the converted Windows name. */
#define WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES (WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS * 4u)
/* Validates the remote descriptor array size after cItems has been read safely. */
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
@@ -61,6 +69,25 @@ static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
return size >= descriptors_size;
}
static BOOL wf_cliprdr_bounded_strlen(const char *value, size_t max_len, size_t *len)
{
size_t i;
if (!value || !len)
return FALSE;
for (i = 0; i <= max_len; i++)
{
if (value[i] == '\0')
{
*len = i;
return TRUE;
}
}
return FALSE;
}
/**
* Clipboard Formats
*/
@@ -205,6 +232,7 @@ struct _CliprdrStream
FILEDESCRIPTORW m_Dsc;
void *m_pData;
UINT32 m_connID;
UINT32 m_streamId; // unique CLIPRDR streamId; avoids leaking a heap pointer
};
typedef struct _CliprdrStream CliprdrStream;
@@ -258,6 +286,9 @@ struct wf_clipboard
char *req_fdata;
HANDLE req_fevent;
BOOL req_f_received;
UINT32 req_f_conn_id_expected; // connID of the outstanding request
UINT32 req_f_stream_id_expected; // streamId of the outstanding request; responses for another are dropped
LONG req_f_stream_id_seq; // source of unique per-stream ids
size_t nFiles;
size_t file_array_size;
@@ -288,7 +319,7 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format);
static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UINT32 format);
static UINT cliprdr_send_lock(wfClipboard *clipboard);
static UINT cliprdr_send_unlock(wfClipboard *clipboard);
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid,
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId,
ULONG index, UINT32 flag, DWORD positionhigh,
DWORD positionlow, ULONG request);
@@ -371,7 +402,7 @@ static ULONG STDMETHODCALLTYPE CliprdrStream_Release(IStream *This)
static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULONG cb,
ULONG *pcbRead)
{
int ret;
UINT ret;
CliprdrStream *instance = (CliprdrStream *)This;
wfClipboard *clipboard;
@@ -384,12 +415,23 @@ static HRESULT STDMETHODCALLTYPE CliprdrStream_Read(IStream *This, void *pv, ULO
if (instance->m_lOffset.QuadPart >= instance->m_lSize.QuadPart)
return S_FALSE;
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)This, instance->m_lIndex,
ret = cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId, instance->m_lIndex,
FILECONTENTS_RANGE, instance->m_lOffset.HighPart,
instance->m_lOffset.LowPart, cb);
if (ret < 0)
if (ret != CHANNEL_RC_OK)
{
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
return E_FAIL;
}
if (clipboard->req_fsize > cb)
{
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
return STG_E_READFAULT;
}
if (clipboard->req_fdata)
{
@@ -601,6 +643,7 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
instance->m_pData = pData;
instance->m_lOffset.QuadPart = 0;
instance->m_connID = connID;
instance->m_streamId = (UINT32)InterlockedIncrement(&clipboard->req_f_stream_id_seq);
if (instance->m_Dsc.dwFlags & FD_ATTRIBUTES)
{
@@ -611,16 +654,28 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData,
if (((instance->m_Dsc.dwFlags & FD_FILESIZE) == 0) && !isDir)
{
/* get content size of this stream */
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, (void *)instance,
if (cliprdr_send_request_filecontents(clipboard, instance->m_connID, instance->m_streamId,
instance->m_lIndex, FILECONTENTS_SIZE, 0, 0,
8) == CHANNEL_RC_OK)
{
success = TRUE;
}
if (clipboard->req_fdata != NULL)
if (clipboard->req_fdata != NULL && clipboard->req_fsize >= sizeof(LONGLONG))
{
LONGLONG sz = 0;
CopyMemory(&sz, clipboard->req_fdata, sizeof(sz));
if (sz < 0)
success = FALSE;
else
instance->m_lSize.QuadPart = sz;
}
else
{
success = FALSE;
}
if (clipboard->req_fdata)
{
instance->m_lSize.QuadPart = *((LONGLONG *)clipboard->req_fdata);
free(clipboard->req_fdata);
clipboard->req_fdata = NULL;
}
@@ -1406,25 +1461,35 @@ static UINT32 get_remote_format_id(wfClipboard *clipboard, UINT32 local_format)
return local_format;
}
static void map_ensure_capacity(wfClipboard *clipboard)
static BOOL map_ensure_capacity(wfClipboard *clipboard, size_t capacity)
{
size_t old_size;
formatMapping *new_map;
if (!clipboard)
return;
return FALSE;
if (clipboard->map_size >= clipboard->map_capacity)
{
size_t new_size;
formatMapping *new_map;
new_size = clipboard->map_capacity * 2;
new_map =
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * new_size);
if (!clipboard->format_mappings)
return FALSE;
if (!new_map)
return;
if (capacity <= clipboard->map_capacity)
return TRUE;
clipboard->format_mappings = new_map;
clipboard->map_capacity = new_size;
}
if (capacity > WF_CLIPRDR_MAX_FORMATS ||
capacity > ((size_t)-1) / sizeof(formatMapping))
return FALSE;
old_size = clipboard->map_capacity;
new_map =
(formatMapping *)realloc(clipboard->format_mappings, sizeof(formatMapping) * capacity);
if (!new_map)
return FALSE;
memset(new_map + old_size, 0, sizeof(formatMapping) * (capacity - old_size));
clipboard->format_mappings = new_map;
clipboard->map_capacity = capacity;
return TRUE;
}
static BOOL clear_format_map(wfClipboard *clipboard)
@@ -1451,6 +1516,13 @@ static BOOL clear_format_map(wfClipboard *clipboard)
return TRUE;
}
static UINT wf_cliprdr_server_format_list_fail(wfClipboard *clipboard)
{
clear_format_map(clipboard);
clipboard->copied = FALSE;
return ERROR_INTERNAL_ERROR;
}
static UINT cliprdr_send_tempdir(wfClipboard *clipboard)
{
CLIPRDR_TEMP_DIRECTORY tempDirectory;
@@ -1729,12 +1801,12 @@ static UINT cliprdr_send_data_request(UINT32 connID, wfClipboard *clipboard, UIN
return wait_response_event(connID, clipboard, clipboard->formatDataRespEvent, &clipboard->formatDataRespReceived, &clipboard->hmem);
}
UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, const void *streamid, ULONG index,
static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, UINT32 streamId, ULONG index,
UINT32 flag, DWORD positionhigh, DWORD positionlow,
ULONG nreq)
{
UINT rc;
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest;
CLIPRDR_FILE_CONTENTS_REQUEST fileContentsRequest = { 0 };
if (!clipboard || !clipboard->context || !clipboard->context->ClientFileContentsRequest)
return ERROR_INTERNAL_ERROR;
@@ -1745,12 +1817,11 @@ UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 connID, co
return rc;
}
clipboard->req_f_received = FALSE;
clipboard->req_f_conn_id_expected = connID;
clipboard->req_f_stream_id_expected = streamId;
fileContentsRequest.connID = connID;
// streamId is `IStream*` pointer, though it is not very good on a 64-bit system.
// But it is OK, because it is only used to check if the stream is the same in
// `wf_cliprdr_server_file_contents_request()` function.
fileContentsRequest.streamId = (UINT32)(ULONG_PTR)streamid;
fileContentsRequest.streamId = streamId;
fileContentsRequest.listIndex = index;
fileContentsRequest.dwFlags = flag;
fileContentsRequest.nPositionLow = positionlow;
@@ -2443,6 +2514,16 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
if (!clear_format_map(clipboard))
return ERROR_INTERNAL_ERROR;
clipboard->copied = FALSE;
if (formatList->numFormats > WF_CLIPRDR_MAX_FORMATS)
return ERROR_INTERNAL_ERROR;
if (formatList->numFormats > 0 && !formatList->formats)
return ERROR_INTERNAL_ERROR;
if (!map_ensure_capacity(clipboard, formatList->numFormats))
return ERROR_INTERNAL_ERROR;
clipboard->copied = TRUE;
@@ -2450,19 +2531,58 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
{
format = &(formatList->formats[i]);
mapping = &(clipboard->format_mappings[i]);
/* Do not validate the peer-provided formatId as a Windows registered format.
* It is only a remote protocol ID used when requesting data from the peer.
* For named formats, RegisterClipboardFormatW creates the local Windows
* clipboard ID below, and that local ID is checked before publishing. */
mapping->remote_format_id = format->formatId;
if (format->formatName)
{
int size = MultiByteToWideChar(CP_UTF8, 0, format->formatName,
strlen(format->formatName), NULL, 0);
mapping->name = calloc(size + 1, sizeof(WCHAR));
size_t name_len;
int size;
if (mapping->name)
if (!wf_cliprdr_bounded_strlen(format->formatName,
WF_CLIPRDR_MAX_FORMAT_NAME_UTF8_BYTES, &name_len))
{
MultiByteToWideChar(CP_UTF8, 0, format->formatName, strlen(format->formatName),
mapping->name, size);
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
return wf_cliprdr_server_format_list_fail(clipboard);
}
if (name_len == 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
size = MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
NULL, 0);
if (size <= 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
if ((UINT)size > WF_CLIPRDR_MAX_FORMAT_NAME_WCHARS)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
mapping->name = calloc((size_t)size + 1, sizeof(WCHAR));
if (!mapping->name)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
if (MultiByteToWideChar(CP_UTF8, 0, format->formatName, (int)name_len,
mapping->name, size) != size)
{
free(mapping->name);
mapping->name = NULL;
return wf_cliprdr_server_format_list_fail(clipboard);
}
mapping->local_format_id = RegisterClipboardFormatW((LPWSTR)mapping->name);
if (mapping->local_format_id == 0)
{
return wf_cliprdr_server_format_list_fail(clipboard);
}
}
else
@@ -2472,7 +2592,6 @@ static UINT wf_cliprdr_server_format_list(CliprdrClientContext *context,
}
clipboard->map_size++;
map_ensure_capacity(clipboard);
}
if (file_transferring(clipboard))
@@ -2923,6 +3042,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
BOOL bIsStreamFile = TRUE;
static LPSTREAM pStreamStc = NULL;
static UINT32 uStreamIdStc = 0;
static UINT32 uConnIdStc = 0;
wfClipboard *clipboard;
UINT rc = ERROR_INTERNAL_ERROR;
UINT sRc;
@@ -2996,7 +3116,8 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
vFormatEtc.lindex = fileContentsRequest->listIndex;
vFormatEtc.ptd = NULL;
if ((uStreamIdStc != fileContentsRequest->streamId) || !pStreamStc)
if ((uStreamIdStc != fileContentsRequest->streamId) ||
(uConnIdStc != fileContentsRequest->connID) || !pStreamStc)
{
LPENUMFORMATETC pEnumFormatEtc;
ULONG CeltFetched;
@@ -3027,6 +3148,7 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context,
{
pStreamStc = vStgMedium.pstm;
uStreamIdStc = fileContentsRequest->streamId;
uConnIdStc = fileContentsRequest->connID;
bIsStreamFile = TRUE;
}
@@ -3190,6 +3312,9 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
rc = ERROR_INTERNAL_ERROR;
break;
}
if (fileContentsResponse->connID != clipboard->req_f_conn_id_expected ||
fileContentsResponse->streamId != clipboard->req_f_stream_id_expected)
return CHANNEL_RC_OK;
clipboard->req_fsize = 0;
clipboard->req_fdata = NULL;
@@ -3200,6 +3325,13 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context,
}
clipboard->req_fsize = fileContentsResponse->cbRequested;
/*
* Keep the zero-size allocation: supported Windows builds use the Microsoft
* CRT, where malloc(0) returns a valid pointer. wait_response_event() also
* uses a non-NULL req_fdata to recognize a successful zero-byte response.
* The Rust FFI derives requestedData and cbRequested from the same Vec, so a
* nonzero length cannot have a NULL data pointer on the normal call path.
*/
clipboard->req_fdata = (char *)malloc(fileContentsResponse->cbRequested);
if (!clipboard->req_fdata)
{
+3 -3
View File
@@ -113,11 +113,11 @@ pub enum MouseButton {
/// Scroll up button
ScrollUp,
/// Left right button
/// Scroll down button
ScrollDown,
/// Left right button
/// Scroll left button
ScrollLeft,
/// Left right button
/// Scroll right button
ScrollRight,
}
+1 -1
View File
@@ -223,7 +223,7 @@ impl KeyboardControllable for Enigo {
// Windows uses uft-16 encoding. We need to check
// for variable length characters. As such some
// characters can be 32 bit long and those are
// encoded in such called hight and low surrogates
// encoded in so-called high and low surrogates
// each 16 bit wide that needs to be send after
// another to the SendInput function without
// being interrupted by "keyup"
+1 -1
View File
@@ -26,7 +26,7 @@ windows = { version = "0.61", features = [
native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]}
[package.metadata.winres]
LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved."
LegalCopyright = "Copyright © 2026 Purslane Tech Pte. Ltd. All rights reserved."
ProductName = "RustDesk"
OriginalFilename = "rustdesk.exe"
FileDescription = "RustDesk Remote Desktop"
+22 -2
View File
@@ -79,6 +79,10 @@ mod webrtc {
}
}
fn tile_log2(threads: u32) -> std::os::raw::c_uint {
(threads as f64).log2().ceil() as _
}
fn get_super_block_size(width: u32, height: u32, threads: u32) -> aom_superblock_size_t {
use aom_superblock_size::*;
let resolution = width * height;
@@ -160,8 +164,7 @@ mod webrtc {
} else {
AV1E_SET_TILE_COLUMNS
};
// Failed on android
call_ctl!(ctx, tile_set, (cfg.g_threads as f64 * 1.0f64).log2().ceil());
call_ctl!(ctx, tile_set, tile_log2(cfg.g_threads));
call_ctl!(ctx, AV1E_SET_ROW_MT, 1);
call_ctl!(ctx, AV1E_SET_ENABLE_OBMC, 0);
call_ctl!(ctx, AV1E_SET_NOISE_SENSITIVITY, 0);
@@ -197,6 +200,23 @@ mod webrtc {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::raw::c_uint;
#[test]
fn tile_log2_uses_c_uint_and_rounds_up() {
let one_thread: c_uint = tile_log2(1);
let three_threads: c_uint = tile_log2(3);
let max_threads: c_uint = tile_log2(64);
assert_eq!(one_thread, 0);
assert_eq!(three_threads, 2);
assert_eq!(max_threads, 6);
}
}
}
impl EncoderApi for AomEncoder {
+2 -2
View File
@@ -79,7 +79,7 @@ heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\li
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Tech Pte. Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
@@ -300,4 +300,4 @@ b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a6
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
+2 -2
View File
@@ -85,7 +85,7 @@ def make_parser():
"-m",
"--manufacturer",
type=str,
default="PURSLANE",
default="Purslane Tech Pte. Ltd.",
help="The app manufacturer.",
)
return parser
@@ -499,7 +499,7 @@ def update_license_file(app_name):
license_content = f.read()
license_content = license_content.replace("website rustdesk.com and other ", "")
license_content = license_content.replace("RustDesk", app_name)
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
license_content = re.sub(r"Purslane(?: Tech Pte\.)? Ltd", app_name, license_content, flags=re.IGNORECASE)
with open(license_file, "w", encoding="utf-8") as f:
f.write(license_content)
+1 -1
View File
@@ -45,7 +45,7 @@ pre_start()
return 0
}
# When loging out from the interactive shell, the execution sequence is:
# When logging out from the interactive shell, the execution sequence is:
#
# IF ~/.bash_logout exists THEN
# execute ~/.bash_logout
+10 -8
View File
@@ -941,21 +941,23 @@ impl Client {
#[cfg(not(target_os = "ios"))]
fn try_stop_clipboard() {
// There's a bug here.
// If session is closed by the peer, `has_sessions_running()` will always return true.
// It's better to check if the active session number.
// But it's not a problem, because the clipboard thread does not consume CPU.
//
// If we want to fix it, we can add a flag to indicate if session is active.
// But I think it's not necessary to introduce complexity at this point.
// Disconnected Flutter sessions may keep UI handlers alive, so only connected sessions
// should block clipboard cleanup.
#[cfg(feature = "flutter")]
if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) {
if crate::flutter::sessions::has_connected_sessions_running(ConnType::DEFAULT_CONN) {
return;
}
#[cfg(not(target_os = "android"))]
clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME);
CLIPBOARD_STATE.lock().unwrap().running = false;
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
if let Err(e) = crate::clipboard::try_empty_clipboard_files_sync(
crate::clipboard::ClipboardSide::Client,
0,
) {
log::error!("Failed to empty client clipboard files: {}", e);
}
#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))]
clipboard::platform::unix::fuse::uninit_fuse_context(true);
}
+2
View File
@@ -360,6 +360,8 @@ impl<T: InvokeUiSession> Remote<T> {
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
if self.handler.is_default() && _set_disconnected_ok {
// Linux client cleanup runs synchronously in try_stop_clipboard() before FUSE is
// unmounted. Keep this async path for other file-clipboard platforms.
crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id);
}
}
+40 -31
View File
@@ -151,41 +151,50 @@ pub fn update_clipboard_files(files: Vec<String>, side: ClipboardSide) {
#[cfg(feature = "unix-file-copy-paste")]
pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) {
std::thread::spawn(move || {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
match ClipboardContext::new() {
Ok(x) => {
*ctx = Some(x);
}
Err(e) => {
log::error!("Failed to create clipboard context: {}", e);
return;
}
}
}
#[allow(unused_mut)]
if let Some(mut ctx) = ctx.as_mut() {
#[cfg(target_os = "linux")]
{
use clipboard::platform::unix;
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
ctx.try_empty_clipboard_files(_side);
}
}
#[cfg(target_os = "macos")]
{
ctx.try_empty_clipboard_files(_side);
// No need to make sure the context is enabled.
clipboard::ContextSend::proc(|context| -> ResultType<()> {
context.empty_clipboard(_conn_id).ok();
Ok(())
})
.ok();
}
if let Err(e) = try_empty_clipboard_files_sync(_side, _conn_id) {
log::error!("Failed to empty clipboard files: {}", e);
}
});
}
#[cfg(feature = "unix-file-copy-paste")]
pub fn try_empty_clipboard_files_sync(_side: ClipboardSide, _conn_id: i32) -> ResultType<()> {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
match ClipboardContext::new() {
Ok(x) => {
*ctx = Some(x);
}
Err(e) => {
log::error!("Failed to create clipboard context: {}", e);
bail!("Failed to create clipboard context: {}", e);
}
}
}
#[allow(unused_mut)]
if let Some(mut ctx) = ctx.as_mut() {
#[cfg(target_os = "linux")]
{
use clipboard::platform::unix;
if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) {
ctx.try_empty_clipboard_files(_side);
}
}
#[cfg(target_os = "macos")]
{
ctx.try_empty_clipboard_files(_side);
// No need to make sure the context is enabled.
clipboard::ContextSend::proc(|context| -> ResultType<()> {
if !context.empty_clipboard(_conn_id)? {
bail!("Failed to empty clipboard files for conn_id {}", _conn_id);
}
Ok(())
})?;
}
}
Ok(())
}
#[cfg(target_os = "windows")]
pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) {
log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id);
+20 -12
View File
@@ -332,12 +332,16 @@ pub mod unix_file_clip {
log::debug!("format data response: msg_flags: {}", msg_flags);
if msg_flags != 0x1 {
// return failure message?
log::error!(
"peer reported clipboard format data failure: {}",
msg_flags
);
return vec![];
}
log::debug!("parsing file descriptors");
if fuse::init_fuse_context(true).is_ok() {
match fuse::format_data_response_to_urls(
match fuse::init_fuse_context(side == ClipboardSide::Client) {
Ok(()) => match fuse::format_data_response_to_urls(
side == ClipboardSide::Client,
format_data,
conn_id,
@@ -348,9 +352,10 @@ pub mod unix_file_clip {
Err(e) => {
log::error!("failed to parse file descriptors: {:?}", e);
}
},
Err(e) => {
log::error!("failed to initialize clipboard FUSE context: {:?}", e);
}
} else {
// send error message to server
}
}
ClipboardFile::FileContentsRequest {
@@ -386,6 +391,7 @@ pub mod unix_file_clip {
ClipboardFile::FileContentsResponse {
msg_flags,
stream_id,
requested_data,
..
} => {
log::debug!(
@@ -393,13 +399,15 @@ pub mod unix_file_clip {
msg_flags,
stream_id,
);
if fuse::init_fuse_context(true).is_ok() {
hbb_common::allow_err!(fuse::handle_file_content_response(
side == ClipboardSide::Client,
clip
));
} else {
// send error message to server
let response = ClipboardFile::FileContentsResponse {
msg_flags,
stream_id,
requested_data,
};
if let Err(e) =
fuse::handle_file_content_response(side == ClipboardSide::Client, response)
{
log::error!("failed to handle file contents response: {:?}", e);
}
}
ClipboardFile::NotifyCallback {
+10
View File
@@ -2297,6 +2297,16 @@ pub mod sessions {
*r#type == conn_type && s.session_handlers.read().unwrap().len() != 0
})
}
#[inline]
#[cfg(not(target_os = "ios"))]
pub fn has_connected_sessions_running(conn_type: ConnType) -> bool {
SESSIONS.read().unwrap().iter().any(|((_, r#type), s)| {
*r#type == conn_type
&& s.session_handlers.read().unwrap().len() != 0
&& s.connection_round_state.lock().unwrap().is_connected()
})
}
}
pub(super) mod async_tasks {
+10 -2
View File
@@ -2852,8 +2852,16 @@ pub fn main_get_common(key: String) -> String {
crate::platform::windows::is_msi_installed(),
crate::common::is_custom_client(),
) {
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"),
(Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"),
(Ok(true), false) => match crate::platform::windows::release_arch_suffix() {
Some(arch) => format!("rustdesk-{_version}-{arch}.msi"),
None => "error:unsupported".to_owned(),
},
(Ok(true), true) | (Ok(false), _) => {
match crate::platform::windows::release_arch_suffix() {
Some(arch) => format!("rustdesk-{_version}-{arch}.exe"),
None => "error:unsupported".to_owned(),
}
}
(Err(e), _) => {
log::error!("Failed to check if is msi: {}", e);
format!("error:update-failed-check-msi-tip")
+67 -7
View File
@@ -103,15 +103,29 @@ pub const LANGS: &[(&str, &str)] = &[
("gu", "ગુજરાતી"),
];
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
let locale = sys_locale::get_locale().unwrap_or_default();
translate_locale(name, &locale)
pub(crate) fn cjk_ui_unavailable() -> bool {
cfg!(all(
target_os = "linux",
target_arch = "aarch64",
feature = "flutter"
))
}
pub fn translate_locale(name: String, locale: &str) -> String {
pub(crate) fn is_cjk_lang(lang_or_locale: &str) -> bool {
let lang = lang_or_locale
.split(|c| c == '-' || c == '_')
.next()
.unwrap_or_default()
.to_lowercase();
matches!(lang.as_str(), "zh" | "ja" | "ko")
}
fn resolve_lang(saved_lang: &str, locale: &str, cjk_fallback: bool) -> String {
let locale = locale.to_lowercase();
let mut lang = hbb_common::config::LocalConfig::get_option("lang").to_lowercase();
let mut lang = saved_lang.to_lowercase();
if cjk_fallback && is_cjk_lang(&lang) {
return "en".to_owned();
}
if lang.is_empty() {
// zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android
if locale.starts_with("zh") {
@@ -131,7 +145,25 @@ pub fn translate_locale(name: String, locale: &str) -> String {
.unwrap_or_default()
.to_owned();
}
let lang = lang.to_lowercase();
if cjk_fallback && is_cjk_lang(&lang) {
"en".to_owned()
} else {
lang
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
let locale = sys_locale::get_locale().unwrap_or_default();
translate_locale(name, &locale)
}
pub fn translate_locale(name: String, locale: &str) -> String {
let lang = resolve_lang(
&hbb_common::config::LocalConfig::get_option("lang"),
locale,
cjk_ui_unavailable(),
);
let m = match lang.as_str() {
"fr" => fr::T.deref(),
"zh-cn" => cn::T.deref(),
@@ -275,4 +307,32 @@ mod test {
("{} times {4} makes {8}".to_string(), Some("2".to_string()))
);
}
#[test]
fn test_resolve_lang_forces_english_for_saved_cjk_when_target_disables_cjk() {
use super::resolve_lang as f;
assert_eq!(f("zh-cn", "en-US", true), "en");
assert_eq!(f("zh-tw", "en-US", true), "en");
assert_eq!(f("ja", "en-US", true), "en");
assert_eq!(f("ko", "en-US", true), "en");
}
#[test]
fn test_resolve_lang_forces_english_for_cjk_locale_when_target_disables_cjk() {
use super::resolve_lang as f;
assert_eq!(f("", "zh_CN", true), "en");
assert_eq!(f("", "ja-JP", true), "en");
assert_eq!(f("", "ko_KR", true), "en");
}
#[test]
fn test_resolve_lang_preserves_cjk_when_target_allows_cjk() {
use super::resolve_lang as f;
assert_eq!(f("zh-cn", "en-US", false), "zh-cn");
assert_eq!(f("", "zh_TW", false), "zh-tw");
assert_eq!(f("", "ja-JP", false), "ja");
}
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "تبديل الشاشة"),
("Show monitor switch button on the main toolbar", "إظهار زر تبديل الشاشة على شريط الأدوات الرئيسي"),
("Show on the minimized toolbar", "الإظهار على شريط الأدوات المُصغّر"),
("All monitors", "جميع الشاشات"),
("#{} monitor", "الشاشة رقم {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Пераключыць дысплэй"),
("Show monitor switch button on the main toolbar", "Паказваць кнопку пераключэння манітора на галоўнай панэлі інструментаў"),
("Show on the minimized toolbar", "Паказваць на згорнутай панэлі інструментаў"),
("All monitors", "Усе манітори"),
("#{} monitor", "Манітор {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Превключване на дисплея"),
("Show monitor switch button on the main toolbar", "Показване на бутона за превключване на монитора в главната лента с инструменти"),
("Show on the minimized toolbar", "Показване в минимизираната лента с инструменти"),
("All monitors", "Всички монитори"),
("#{} monitor", "Монитор {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Canvia de pantalla"),
("Show monitor switch button on the main toolbar", "Mostra el botó de canvi de monitor a la barra deines principal"),
("Show on the minimized toolbar", "Mostra a la barra deines minimitzada"),
("All monitors", "Tots els monitors"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "切换显示器"),
("Show monitor switch button on the main toolbar", "在主工具栏上显示显示器切换按钮"),
("Show on the minimized toolbar", "在最小化工具栏上显示"),
("All monitors", "所有显示器"),
("#{} monitor", "{}号显示器"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Přepnout obrazovku"),
("Show monitor switch button on the main toolbar", "Zobrazit tlačítko přepnutí monitoru na hlavním panelu nástrojů"),
("Show on the minimized toolbar", "Zobrazit na minimalizovaném panelu nástrojů"),
("All monitors", "Všechny monitory"),
("#{} monitor", "Monitor č. {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Skift skærm"),
("Show monitor switch button on the main toolbar", "Vis knap til skærmskift på hovedværktøjslinjen"),
("Show on the minimized toolbar", "Vis på den minimerede værktøjslinje"),
("All monitors", "Alle skærme"),
("#{} monitor", "Skærm {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Anzeige wechseln"),
("Show monitor switch button on the main toolbar", "Schaltfläche zum Monitorwechsel in der Haupt-Symbolleiste anzeigen"),
("Show on the minimized toolbar", "In der minimierten Symbolleiste anzeigen"),
("All monitors", "Alle Bildschirme"),
("#{} monitor", "Bildschirm {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Εναλλαγή οθόνης"),
("Show monitor switch button on the main toolbar", "Εμφάνιση κουμπιού εναλλαγής οθόνης στην κύρια γραμμή εργαλείων"),
("Show on the minimized toolbar", "Εμφάνιση στην ελαχιστοποιημένη γραμμή εργαλείων"),
("All monitors", "Όλες οι οθόνες"),
("#{} monitor", "Οθόνη {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Ŝalti ekranon"),
("Show monitor switch button on the main toolbar", "Montri ekran-ŝaltan butonon en la ĉefa ilobreto"),
("Show on the minimized toolbar", "Montri en la minimumigita ilobreto"),
("All monitors", "Ĉiuj monitoroj"),
("#{} monitor", "Monitoro {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Cambiar de pantalla"),
("Show monitor switch button on the main toolbar", "Mostrar el botón de cambio de monitor en la barra de herramientas principal"),
("Show on the minimized toolbar", "Mostrar en la barra de herramientas minimizada"),
("All monitors", "Todos los monitores"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Vaheta kuva"),
("Show monitor switch button on the main toolbar", "Näita monitori vahetamise nuppu peamisel tööriistaribal"),
("Show on the minimized toolbar", "Näita minimeeritud tööriistaribal"),
("All monitors", "Kõik kuvarid"),
("#{} monitor", "Kuvar {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Aldatu pantaila"),
("Show monitor switch button on the main toolbar", "Erakutsi monitorea aldatzeko botoia tresna-barra nagusian"),
("Show on the minimized toolbar", "Erakutsi minimizatutako tresna-barran"),
("All monitors", "Monitore guztiak"),
("#{} monitor", "{}. monitorea"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "تعویض نمایشگر"),
("Show monitor switch button on the main toolbar", "نمایش دکمه تعویض نمایشگر در نوار ابزار اصلی"),
("Show on the minimized toolbar", "نمایش در نوار ابزار کوچک‌شده"),
("All monitors", "همه نمایشگرها"),
("#{} monitor", "نمایشگر {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Vaihda näyttöä"),
("Show monitor switch button on the main toolbar", "Näytä näytön vaihtopainike päätyökalurivillä"),
("Show on the minimized toolbar", "Näytä pienennetyssä työkalurivissä"),
("All monitors", "Kaikki näytöt"),
("#{} monitor", "Näyttö {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Changer d’écran"),
("Show monitor switch button on the main toolbar", "Afficher le bouton de changement d’écran dans la barre doutils principale"),
("Show on the minimized toolbar", "Afficher dans la barre doutils réduite"),
("All monitors", "Tous les moniteurs"),
("#{} monitor", "Moniteur {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ეკრანის გადართვა"),
("Show monitor switch button on the main toolbar", "მონიტორის გადართვის ღილაკის ჩვენება მთავარ ხელსაწყოთა ზოლზე"),
("Show on the minimized toolbar", "ჩვენება ჩაკეცილ ხელსაწყოთა ზოლზე"),
("All monitors", "ყველა მონიტორი"),
("#{} monitor", "მონიტორი {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ડિસ્પ્લે બદલો"),
("Show monitor switch button on the main toolbar", "મુખ્ય ટૂલબાર પર મોનિટર સ્વિચ બટન બતાવો"),
("Show on the minimized toolbar", "ન્યૂનતમ કરેલા ટૂલબાર પર બતાવો"),
("All monitors", "બધા મોનિટર"),
("#{} monitor", "મોનિટર {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "החלפת צג"),
("Show monitor switch button on the main toolbar", "הצגת לחצן החלפת צג בסרגל הכלים הראשי"),
("Show on the minimized toolbar", "הצגה בסרגל הכלים הממוזער"),
("All monitors", "כל המסכים"),
("#{} monitor", "מסך {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "डिस्प्ले बदलें"),
("Show monitor switch button on the main toolbar", "मुख्य टूलबार पर मॉनिटर स्विच बटन दिखाएं"),
("Show on the minimized toolbar", "न्यूनतम किए गए टूलबार पर दिखाएं"),
("All monitors", "सभी मॉनिटर"),
("#{} monitor", "मॉनिटर {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Promijeni zaslon"),
("Show monitor switch button on the main toolbar", "Prikaži gumb za prebacivanje monitora na glavnoj alatnoj traci"),
("Show on the minimized toolbar", "Prikaži na minimiziranoj alatnoj traci"),
("All monitors", "Svi monitori"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Kijelző váltása"),
("Show monitor switch button on the main toolbar", "Monitorváltó gomb megjelenítése a fő eszköztáron"),
("Show on the minimized toolbar", "Megjelenítés a kis méretű eszköztáron"),
("All monitors", "Minden monitor"),
("#{} monitor", "{}. monitor"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Ganti tampilan"),
("Show monitor switch button on the main toolbar", "Tampilkan tombol pengalih monitor di bilah alat utama"),
("Show on the minimized toolbar", "Tampilkan di bilah alat yang diperkecil"),
("All monitors", "Semua monitor"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Cambia schermo"),
("Show monitor switch button on the main toolbar", "Visualizza nella barra strumenti principale il pulsante per il cambio schermo"),
("Show on the minimized toolbar", "Visualizza nella barra strumenti ridotta a icona"),
("All monitors", "Tutti gli schermi"),
("#{} monitor", "Schermo {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ディスプレイを切り替え"),
("Show monitor switch button on the main toolbar", "メインツールバーにモニター切り替えボタンを表示"),
("Show on the minimized toolbar", "最小化したツールバーに表示"),
("All monitors", "すべてのディスプレイ"),
("#{} monitor", "ディスプレイ {}"),
].iter().cloned().collect();
}
+9 -7
View File
@@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_change_tip", "a-z, A-Z, 0-9, -(대시) 및 _(밑줄) 문자만 허용됩니다. 첫 글자는 a-z, A-Z여야 합니다. 길이는 6에서 16 사이여야 합니다."),
("Website", "웹사이트"),
("About", "정보"),
("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다! - 한국어 번역: 비너스걸"),
("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다!"),
("Privacy Statement", "개인정보 보호정책"),
("Mute", "음소거"),
("Build Date", "빌드 날짜"),
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "화면 공유"),
("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."),
("wayland-requires-higher-linux-version", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."),
("xdp-portal-unavailable", "Wayland 화면 캡처에 실패했습니다. XDG Desktop Portal이 충돌했거나 사용할 수 없는 상태일 수 있습니다. `systemctl --user restart xdg-desktop-portal` 명령으로 다시 시작해 보세요."),
("xdp-portal-unavailable", ""),
("JumpLink", "점프 링크"),
("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"),
("Show RustDesk", "RustDesk 표시"),
@@ -749,17 +749,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Deploy", "배포"),
("Custom ID (optional)", "사용자 지정 ID (선택 사항)"),
("server_requires_deployment_tip", "서버에서 이 장치를 명시적으로 배포하도록 요구합니다. 지금 배포하시겠습니까?"),
("The server does not require explicit deployment.", "서버에서 명시적 배포를 요구하지 않습니다."),
("The server does not require explicit deployment.", "서버에서 명시적 배포를 요구하지 않습니다."),
("Unknown response.", "알 수 없는 응답입니다."),
("wayland-keyboard-input-disabled-tip", "키보드 입력을 허용하시겠습니까?"),
("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용(비밀번호 포함)은 해당 컴퓨터의 다른 앱 읽을 수 있습니다."),
("wayland-keyboard-input-consent-tip", "이 원격 컴퓨터에서 입력하는 내용 (비밀번호 포함)은 해당 컴퓨터의 다른 앱에서 읽을 수 있습니다."),
("wayland-keyboard-input-applies-to-tip", "이 선택이 적용되는 대상:"),
("wayland-soft-keyboard-input-label", "소프트 키보드 입력"),
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 초기화"),
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않"),
("Why this happens", " 현상이 발생하는 이유"),
("wayland-keyboard-input-reset-choice-tip", "키보드 입력 선택 재설정"),
("remember-wayland-keyboard-choice-tip", "이 원격 컴퓨터에 대해 다시 묻지 않"),
("Why this happens", "이 현상이 발생하는 이유"),
("Switch display", "디스플레이 전환"),
("Show monitor switch button on the main toolbar", "기본 도구 모음에 모니터 전환 버튼 표시"),
("Show on the minimized toolbar", "최소화된 도구 모음에 표시"),
("All monitors", "모든 모니터"),
("#{} monitor", "#{} 모니터"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Дисплейді ауыстыру"),
("Show monitor switch button on the main toolbar", "Негізгі құралдар тақтасында мониторды ауыстыру түймесін көрсету"),
("Show on the minimized toolbar", "Кішірейтілген құралдар тақтасында көрсету"),
("All monitors", "Барлық мониторлар"),
("#{} monitor", "Монитор {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Perjungti ekraną"),
("Show monitor switch button on the main toolbar", "Rodyti monitoriaus perjungimo mygtuką pagrindinėje įrankių juostoje"),
("Show on the minimized toolbar", "Rodyti sumažintoje įrankių juostoje"),
("All monitors", "Visi monitoriai"),
("#{} monitor", "Monitorius {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Pārslēgt displeju"),
("Show monitor switch button on the main toolbar", "Rādīt monitora pārslēgšanas pogu galvenajā rīkjoslā"),
("Show on the minimized toolbar", "Rādīt minimizētajā rīkjoslā"),
("All monitors", "Visi monitori"),
("#{} monitor", "Monitors {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "ഡിസ്പ്ലേ മാറ്റുക"),
("Show monitor switch button on the main toolbar", "പ്രധാന ടൂൾബാറിൽ മോണിറ്റർ സ്വിച്ച് ബട്ടൺ കാണിക്കുക"),
("Show on the minimized toolbar", "ചെറുതാക്കിയ ടൂൾബാറിൽ കാണിക്കുക"),
("All monitors", "എല്ലാ മോണിറ്ററുകളും"),
("#{} monitor", "മോണിറ്റർ {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Bytt skjerm"),
("Show monitor switch button on the main toolbar", "Vis knapp for skjermbytte på hovedverktøylinjen"),
("Show on the minimized toolbar", "Vis på den minimerte verktøylinjen"),
("All monitors", "Alle skjermer"),
("#{} monitor", "Skjerm {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Beeldscherm wisselen"),
("Show monitor switch button on the main toolbar", "Knop voor monitorwisseling weergeven op de hoofdwerkbalk"),
("Show on the minimized toolbar", "Weergeven op de geminimaliseerde werkbalk"),
("All monitors", "Alle monitoren"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Przełącz ekran"),
("Show monitor switch button on the main toolbar", "Pokaż przycisk przełączania monitora na głównym pasku narzędzi"),
("Show on the minimized toolbar", "Pokaż na zminimalizowanym pasku narzędzi"),
("All monitors", "Wszystkie ekrany"),
("#{} monitor", "Ekran {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Trocar de ecrã"),
("Show monitor switch button on the main toolbar", "Mostrar o botão de troca de monitor na barra de ferramentas principal"),
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
("All monitors", "Todos os monitores"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+4 -2
View File
@@ -160,7 +160,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accept and Install", "Aceitar e Instalar"),
("End-user license agreement", "Acordo de licença do usuário final"),
("Generating ...", "Gerando ..."),
("Your installation is lower version.", "Instalação desatualizada"),
("Your installation is lower version.", "Sua instalação está com uma versão desatualizada."),
("not_close_tcp_tip", "Não feche esta janela enquanto estiver utilizando o túnel"),
("Listening ...", "Escutando ..."),
("Remote Host", "Host Remoto"),
@@ -321,7 +321,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Fullscreen", "Tela Cheia"),
("Mobile Actions", "Ações móveis"),
("Select Monitor", "Selecionar tela"),
("Control Actions", "Controlar ações"),
("Control Actions", "Ações de controle"),
("Display Settings", "Configurações de exibição"),
("Ratio", "Proporção"),
("Image Quality", "Qualidade de imagem"),
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Trocar de tela"),
("Show monitor switch button on the main toolbar", "Mostrar botão de troca de tela na barra de ferramentas"),
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
("All monitors", "Todas as telas"),
("#{} monitor", "Tela {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Comută afișajul"),
("Show monitor switch button on the main toolbar", "Afișează butonul de comutare a monitorului în bara de instrumente principală"),
("Show on the minimized toolbar", "Afișează în bara de instrumente minimizată"),
("All monitors", "Toate monitoarele"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Переключить дисплей"),
("Show monitor switch button on the main toolbar", "Показывать кнопку переключения монитора на главной панели инструментов"),
("Show on the minimized toolbar", "Показывать на свёрнутой панели инструментов"),
("All monitors", "Все мониторы"),
("#{} monitor", "Монитор {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Càmbia ischermu"),
("Show monitor switch button on the main toolbar", "Mustra su butone de càmbiu de monitor in sa barra de aina printzipale"),
("Show on the minimized toolbar", "Mustra in sa barra de aina minimizada"),
("All monitors", "Totu sos ischermos"),
("#{} monitor", "Ischermu {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Prepnúť obrazovku"),
("Show monitor switch button on the main toolbar", "Zobraziť tlačidlo prepnutia monitora na hlavnom paneli nástrojov"),
("Show on the minimized toolbar", "Zobraziť na minimalizovanom paneli nástrojov"),
("All monitors", "Všetky monitory"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Preklopi zaslon"),
("Show monitor switch button on the main toolbar", "Pokaži gumb za preklop monitorja v glavni orodni vrstici"),
("Show on the minimized toolbar", "Pokaži v pomanjšani orodni vrstici"),
("All monitors", "Vsi zasloni"),
("#{} monitor", "Zaslon {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Ndërro ekranin"),
("Show monitor switch button on the main toolbar", "Shfaq butonin e ndërrimit të monitorit te shiriti kryesor i veglave"),
("Show on the minimized toolbar", "Shfaq te shiriti i minimizuar i veglave"),
("All monitors", "Të gjithë monitorët"),
("#{} monitor", "Monitori {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Промени екран"),
("Show monitor switch button on the main toolbar", "Прикажи дугме за пребацивање монитора на главној траци са алаткама"),
("Show on the minimized toolbar", "Прикажи на умањеној траци са алаткама"),
("All monitors", "Svi monitori"),
("#{} monitor", "Monitor {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Växla skärm"),
("Show monitor switch button on the main toolbar", "Visa knapp för skärmväxling i huvudverktygsfältet"),
("Show on the minimized toolbar", "Visa i det minimerade verktygsfältet"),
("All monitors", "Alla skärmar"),
("#{} monitor", "Skärm {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "திரையை மாற்று"),
("Show monitor switch button on the main toolbar", "முதன்மை கருவிப்பட்டையில் திரை மாற்று பொத்தானைக் காட்டு"),
("Show on the minimized toolbar", "சிறிதாக்கப்பட்ட கருவிப்பட்டையில் காட்டு"),
("All monitors", "அனைத்து மானிட்டர்களும்"),
("#{} monitor", "மானிட்டர் {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", ""),
("Show monitor switch button on the main toolbar", ""),
("Show on the minimized toolbar", ""),
("All monitors", ""),
("#{} monitor", ""),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "สลับจอแสดงผล"),
("Show monitor switch button on the main toolbar", "แสดงปุ่มสลับจอภาพบนแถบเครื่องมือหลัก"),
("Show on the minimized toolbar", "แสดงบนแถบเครื่องมือที่ย่อเล็กสุด"),
("All monitors", "จอภาพทั้งหมด"),
("#{} monitor", "จอภาพ {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Ekranı değiştir"),
("Show monitor switch button on the main toolbar", "Ana araç çubuğunda monitör değiştirme düğmesini göster"),
("Show on the minimized toolbar", "Simge durumuna küçültülmüş araç çubuğunda göster"),
("All monitors", "Tüm monitörler"),
("#{} monitor", "Monitör {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "切換螢幕"),
("Show monitor switch button on the main toolbar", "在主工具列上顯示螢幕切換按鈕"),
("Show on the minimized toolbar", "在最小化工具列上顯示"),
("All monitors", "所有顯示器"),
("#{} monitor", "{}號顯示器"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Перемкнути дисплей"),
("Show monitor switch button on the main toolbar", "Показувати кнопку перемикання монітора на головній панелі інструментів"),
("Show on the minimized toolbar", "Показувати на згорнутій панелі інструментів"),
("All monitors", "Усі монітори"),
("#{} monitor", "Монітор {}"),
].iter().cloned().collect();
}
+2
View File
@@ -761,5 +761,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Switch display", "Chuyển màn hình"),
("Show monitor switch button on the main toolbar", "Hiển thị nút chuyển đổi màn hình trên thanh công cụ chính"),
("Show on the minimized toolbar", "Hiển thị trên thanh công cụ thu nhỏ"),
("All monitors", "Tất cả màn hình"),
("#{} monitor", "Màn hình {}"),
].iter().cloned().collect();
}
+1 -1
View File
@@ -48,7 +48,7 @@ fn main() {
);
let matches = App::new("rustdesk")
.version(crate::VERSION)
.author("Purslane Ltd<info@rustdesk.com>")
.author("Purslane Tech Pte. Ltd.<info@rustdesk.com>")
.about("RustDesk command line tool")
.args_from_usage(&args)
.get_matches();
+11 -4
View File
@@ -3647,10 +3647,9 @@ pub fn update_to(file: &str) -> ResultType<()> {
// `1` and `3` must be done in custom actions.
// We need also to handle the command line parsing to find the tray processes.
pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
let cmds = format!(
"chcp 65001 && msiexec /i {msi} {}",
if quiet { "/qn LAUNCH_TRAY_APP=N" } else { "" }
);
let quiet_args = if quiet { " /qn LAUNCH_TRAY_APP=N" } else { "" };
let cmds =
format!("chcp 65001 && msiexec /i \"{msi}\"{quiet_args} REBOOT=ReallySuppress /norestart");
run_cmds(cmds, false, "update-msi")?;
Ok(())
}
@@ -3944,6 +3943,14 @@ pub fn is_x64() -> bool {
unsafe { sys_info.u.s().wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 }
}
pub fn release_arch_suffix() -> Option<&'static str> {
match std::env::consts::ARCH {
"x86_64" => Some("x86_64"),
"aarch64" => Some("aarch64"),
_ => None,
}
}
pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> {
// Kill rustdesk.exe without extra arg, should only be called by --server
// We can find the exact process which occupies the ipc, see more from https://github.com/winsiderss/systeminformer
+43 -60
View File
@@ -28,11 +28,21 @@ use hbb_common::{
use crate::{
check_port,
server::{check_zombie, new as new_server, ServerPtr},
server::{check_zombie, new as new_server, ConnectionMeta, ServerPtr},
};
type Message = RendezvousMessage;
fn connection_meta(
control_permissions: Option<ControlPermissions>,
controlled_context: Option<ControlledContext>,
) -> ConnectionMeta {
ConnectionMeta {
control_permissions,
controlled_context,
}
}
lazy_static::lazy_static! {
static ref SOLVING_PK_MISMATCH: Mutex<String> = Default::default();
static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now()));
@@ -490,6 +500,10 @@ impl RendezvousMediator {
if last.0 == addr && last.1.elapsed().as_millis() < 100 {
return Ok(());
}
let meta = connection_meta(
rr.control_permissions.into_option(),
rr.controlled_context.into_option(),
);
self.create_relay(
rr.socket_addr.into(),
@@ -499,7 +513,7 @@ impl RendezvousMediator {
rr.secure,
false,
Default::default(),
rr.control_permissions.clone().into_option(),
meta,
)
.await
}
@@ -513,7 +527,7 @@ impl RendezvousMediator {
secure: bool,
initiate: bool,
socket_addr_v6: bytes::Bytes,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) -> ResultType<()> {
let peer_addr = AddrMangle::decode(&socket_addr);
log::info!(
@@ -547,7 +561,7 @@ impl RendezvousMediator {
peer_addr,
secure,
is_ipv4(&self.addr),
control_permissions,
meta,
)
.await;
Ok(())
@@ -565,14 +579,12 @@ impl RendezvousMediator {
let relay_server = self.get_relay_server(fla.relay_server.clone());
let relay = use_ws() || Config::is_proxy();
let mut socket_addr_v6 = Default::default();
let meta = connection_meta(
fla.control_permissions.clone().into_option(),
fla.controlled_context.clone().into_option(),
);
if peer_addr_v6.port() > 0 && !relay {
socket_addr_v6 = start_ipv6(
peer_addr_v6,
addr,
server.clone(),
fla.control_permissions.clone().into_option(),
)
.await;
socket_addr_v6 = start_ipv6(peer_addr_v6, addr, server.clone(), meta.clone()).await;
}
if is_ipv4(&self.addr) && !relay && !config::is_disable_tcp_listen() {
if let Err(err) = self
@@ -581,6 +593,7 @@ impl RendezvousMediator {
server.clone(),
relay_server.clone(),
socket_addr_v6.clone(),
meta.clone(),
)
.await
{
@@ -598,7 +611,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6,
fla.control_permissions.into_option(),
meta,
)
.await
}
@@ -609,6 +622,7 @@ impl RendezvousMediator {
server: ServerPtr,
relay_server: String,
socket_addr_v6: bytes::Bytes,
meta: ConnectionMeta,
) -> ResultType<()> {
let peer_addr = AddrMangle::decode(&fla.socket_addr);
log::debug!("Handle intranet from {:?}", peer_addr);
@@ -629,14 +643,7 @@ impl RendezvousMediator {
});
let bytes = msg_out.write_to_bytes()?;
socket.send_raw(bytes).await?;
crate::accept_connection(
server.clone(),
socket,
peer_addr,
true,
fla.control_permissions.into_option(),
)
.await;
crate::accept_connection(server.clone(), socket, peer_addr, true, meta).await;
Ok(())
}
@@ -651,15 +658,13 @@ impl RendezvousMediator {
let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6);
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
let mut socket_addr_v6 = Default::default();
let control_permissions = ph.control_permissions.into_option();
let meta = connection_meta(
ph.control_permissions.into_option(),
ph.controlled_context.into_option(),
);
if peer_addr_v6.port() > 0 && !relay {
socket_addr_v6 = start_ipv6(
peer_addr_v6,
peer_addr,
server.clone(),
control_permissions.clone(),
)
.await;
socket_addr_v6 =
start_ipv6(peer_addr_v6, peer_addr, server.clone(), meta.clone()).await;
}
let relay_server = self.get_relay_server(ph.relay_server);
// for ensure, websocket go relay directly
@@ -678,7 +683,7 @@ impl RendezvousMediator {
true,
true,
socket_addr_v6.clone(),
control_permissions,
meta,
)
.await;
}
@@ -695,7 +700,7 @@ impl RendezvousMediator {
};
if ph.udp_port > 0 {
peer_addr.set_port(ph.udp_port as u16);
self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions)
self.punch_udp_hole(peer_addr, server, msg_punch, meta)
.await?;
return Ok(());
}
@@ -712,8 +717,7 @@ impl RendezvousMediator {
msg_out.set_punch_hole_sent(msg_punch);
let bytes = msg_out.write_to_bytes()?;
socket.send_raw(bytes).await?;
crate::accept_connection(server.clone(), socket, peer_addr, true, control_permissions)
.await;
crate::accept_connection(server.clone(), socket, peer_addr, true, meta).await;
Ok(())
}
@@ -722,7 +726,7 @@ impl RendezvousMediator {
peer_addr: SocketAddr,
server: ServerPtr,
msg_punch: PunchHoleSent,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) -> ResultType<()> {
let mut msg_out = Message::new();
msg_out.set_punch_hole_sent(msg_punch);
@@ -737,14 +741,7 @@ impl RendezvousMediator {
socket.send_to(&data, addr).await.ok();
}
});
udp_nat_listen(
socket_cloned.clone(),
peer_addr,
peer_addr,
server,
control_permissions,
)
.await?;
udp_nat_listen(socket_cloned.clone(), peer_addr, peer_addr, server, meta).await?;
Ok(())
}
@@ -901,7 +898,7 @@ async fn direct_server(server: ServerPtr) {
hbb_common::Stream::from(stream, local_addr),
addr,
false,
None, // Direct connections don't have control_permissions
ConnectionMeta::default(), // Direct connections don't have server-side user context.
)
.await
);
@@ -933,21 +930,14 @@ async fn start_ipv6(
peer_addr_v6: SocketAddr,
peer_addr_v4: SocketAddr,
server: ServerPtr,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) -> bytes::Bytes {
crate::test_ipv6().await;
if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await {
let server = server.clone();
tokio::spawn(async move {
allow_err!(
udp_nat_listen(
socket.clone(),
peer_addr_v6,
peer_addr_v4,
server,
control_permissions
)
.await
udp_nat_listen(socket.clone(), peer_addr_v6, peer_addr_v4, server, meta).await
);
});
return local_addr_v6;
@@ -960,7 +950,7 @@ async fn udp_nat_listen(
peer_addr: SocketAddr,
peer_addr_v4: SocketAddr,
server: ServerPtr,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) -> ResultType<()> {
let tm = Instant::now();
let socket_cloned = socket.clone();
@@ -973,14 +963,7 @@ async fn udp_nat_listen(
res,
)
.await?;
crate::server::create_tcp_connection(
server,
stream.1,
peer_addr_v4,
true,
control_permissions,
)
.await?;
crate::server::create_tcp_connection(server, stream.1, peer_addr_v4, true, meta).await?;
Ok(())
};
func.await.map_err(|e: anyhow::Error| {
+16 -17
View File
@@ -81,6 +81,12 @@ pub mod printer_service;
pub type Childs = Arc<Mutex<Vec<std::process::Child>>>;
type ConnMap = HashMap<i32, ConnInner>;
#[derive(Clone, Default)]
pub struct ConnectionMeta {
pub control_permissions: Option<ControlPermissions>,
pub controlled_context: Option<ControlledContext>,
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3;
#[cfg(any(target_os = "macos", target_os = "linux"))]
@@ -163,7 +169,7 @@ async fn accept_connection_(
server: ServerPtr,
socket: Stream,
secure: bool,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) -> ResultType<()> {
let local_addr = socket.local_addr();
drop(socket);
@@ -180,7 +186,7 @@ async fn accept_connection_(
Stream::from(stream, stream_addr),
addr,
secure,
control_permissions,
meta,
)
.await?;
}
@@ -192,7 +198,7 @@ pub async fn create_tcp_connection(
stream: Stream,
addr: SocketAddr,
secure: bool,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) -> ResultType<()> {
let mut stream = stream;
let id = server.write().unwrap().get_new_id();
@@ -260,14 +266,7 @@ pub async fn create_tcp_connection(
}
log::info!("wake up macos");
}
Connection::start(
addr,
stream,
id,
Arc::downgrade(&server),
control_permissions,
)
.await;
Connection::start(addr, stream, id, Arc::downgrade(&server), meta).await;
Ok(())
}
@@ -276,9 +275,9 @@ pub async fn accept_connection(
socket: Stream,
peer_addr: SocketAddr,
secure: bool,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) {
if let Err(err) = accept_connection_(server, socket, secure, control_permissions).await {
if let Err(err) = accept_connection_(server, socket, secure, meta).await {
log::warn!("Failed to accept connection from {}: {}", peer_addr, err);
}
}
@@ -290,7 +289,7 @@ pub async fn create_relay_connection(
peer_addr: SocketAddr,
secure: bool,
ipv4: bool,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) {
if let Err(err) = create_relay_connection_(
server,
@@ -299,7 +298,7 @@ pub async fn create_relay_connection(
peer_addr,
secure,
ipv4,
control_permissions,
meta,
)
.await
{
@@ -319,7 +318,7 @@ async fn create_relay_connection_(
peer_addr: SocketAddr,
secure: bool,
ipv4: bool,
control_permissions: Option<ControlPermissions>,
meta: ConnectionMeta,
) -> ResultType<()> {
let mut stream = socket_client::connect_tcp(
socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4),
@@ -334,7 +333,7 @@ async fn create_relay_connection_(
..Default::default()
});
stream.send(&msg_out).await?;
create_tcp_connection(server, stream, peer_addr, secure, control_permissions).await?;
create_tcp_connection(server, stream, peer_addr, secure, meta).await?;
Ok(())
}
+112 -13
View File
@@ -240,6 +240,36 @@ pub enum AuthConnType {
Terminal,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i64)]
enum ConnAuditPrimaryAuth {
None = 0,
Click = 1,
TemporaryPassword = 2,
PermanentPassword = 3,
SwitchSides = 4,
}
impl ConnAuditPrimaryAuth {
fn as_i64(self) -> i64 {
self as i64
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i64)]
enum ConnAuditTwoFactor {
None = 0,
Totp = 1,
TrustedDevice = 2,
}
impl ConnAuditTwoFactor {
fn as_i64(self) -> i64 {
self as i64
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[derive(Clone, Debug)]
enum TerminalUserToken {
@@ -310,6 +340,7 @@ pub struct Connection {
video_ack_required: bool,
server_audit_conn: String,
server_audit_file: String,
controlled_context: Option<ControlledContext>,
lr: LoginRequest,
peer_argb: u32,
session_last_recv_time: Option<Arc<Mutex<Instant>>>,
@@ -344,6 +375,8 @@ pub struct Connection {
// For post requests that need to be sent sequentially.
// eg. post_conn_audit
tx_post_seq: mpsc::UnboundedSender<(String, Value)>,
conn_audit_primary_auth: ConnAuditPrimaryAuth,
conn_audit_two_factor: ConnAuditTwoFactor,
// Tracks read job IDs delegated to CM process.
// When a read job is delegated to CM (via FS::ReadFile), the job id is added here.
// Used to filter stale responses (FileBlockFromCM, FileReadDone, etc.) for
@@ -407,8 +440,12 @@ impl Connection {
stream: super::Stream,
id: i32,
server: super::ServerPtrWeak,
control_permissions: Option<ControlPermissions>,
meta: super::ConnectionMeta,
) {
let super::ConnectionMeta {
control_permissions,
controlled_context,
} = meta;
// Android is not supported yet, so we always set control_permissions to None.
#[cfg(target_os = "android")]
let control_permissions = None;
@@ -495,6 +532,7 @@ impl Connection {
video_ack_required: false,
server_audit_conn: "".to_owned(),
server_audit_file: "".to_owned(),
controlled_context,
lr: Default::default(),
peer_argb: 0u32,
session_last_recv_time: None,
@@ -536,6 +574,8 @@ impl Connection {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
terminal_user_token: None,
terminal_generic_service: None,
conn_audit_primary_auth: ConnAuditPrimaryAuth::None,
conn_audit_two_factor: ConnAuditTwoFactor::None,
};
let addr = hbb_common::try_into_v4(addr);
if !conn.on_open(addr).await {
@@ -619,6 +659,7 @@ impl Connection {
Some(data) = rx_from_cm.recv() => {
match data {
ipc::Data::Authorize => {
conn.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::Click);
conn.require_2fa.take();
if !conn.send_logon_response_and_keep_alive().await {
break;
@@ -1308,7 +1349,7 @@ impl Connection {
{
self.send_login_error("Your ip is blocked by the peer")
.await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::IpWhitelist, //"ip whitelist",
json!({ "ip":addr.ip() }),
);
@@ -1334,10 +1375,14 @@ impl Connection {
msg_out.set_hash(self.hash.clone());
self.send(msg_out).await;
self.get_api_server();
self.post_conn_audit(json!({
let mut audit = json!({
"ip": addr.ip(),
"action": "new",
}));
});
if let Some(audit_ref) = self.conn_audit_ref() {
audit["conn_audit_ref"] = json!(audit_ref);
}
self.post_conn_audit(audit);
true
}
@@ -1354,6 +1399,18 @@ impl Connection {
);
}
fn conn_audit_ref(&self) -> Option<&str> {
let audit_ref = self
.controlled_context
.as_ref()
.map(|c| c.conn_audit_ref.as_str())?;
if audit_ref.is_empty() {
None
} else {
Some(audit_ref)
}
}
fn post_conn_audit(&self, v: Value) {
if self.server_audit_conn.is_empty() {
return;
@@ -1408,6 +1465,7 @@ impl Connection {
"id":json!(Config::get_id()),
"uuid":json!(crate::encode64(hbb_common::get_uuid())),
"peer_id":json!(self.lr.my_id),
"conn_id":json!(self.inner.id()),
"type": r#type as i8,
"path":path,
"is_file":is_file,
@@ -1418,7 +1476,7 @@ impl Connection {
});
}
pub fn post_alarm_audit(typ: AlarmAuditType, info: Value) {
fn post_alarm_audit(&self, typ: AlarmAuditType, info: Value) {
let url = crate::get_audit_server(
Config::get_option("api-server"),
Config::get_option("custom-rendezvous-server"),
@@ -1432,6 +1490,12 @@ impl Connection {
v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
v["typ"] = json!(typ as i8);
v["info"] = serde_json::Value::String(info.to_string());
v["conn_id"] = json!(self.inner.id());
if typ == AlarmAuditType::IpWhitelist {
if let Some(audit_ref) = self.conn_audit_ref() {
v["conn_audit_ref"] = json!(audit_ref);
}
}
tokio::spawn(async move {
allow_err!(Self::post_audit_async(url, v).await);
});
@@ -1442,6 +1506,23 @@ impl Connection {
crate::post_request(url, v.to_string(), "").await
}
fn set_conn_audit_primary_auth(&mut self, method: ConnAuditPrimaryAuth) {
self.conn_audit_primary_auth = method;
}
fn set_conn_audit_two_factor(&mut self, two_factor: ConnAuditTwoFactor) {
self.conn_audit_two_factor = two_factor;
}
fn normalize_conn_audit_auth_fields(&mut self) {
if matches!(
self.conn_audit_primary_auth,
ConnAuditPrimaryAuth::Click | ConnAuditPrimaryAuth::SwitchSides
) {
self.conn_audit_two_factor = ConnAuditTwoFactor::None;
}
}
fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) {
let mut is_rdp = false;
if pf.host == "RDP" && pf.port == 0 {
@@ -1565,9 +1646,15 @@ impl Connection {
.unwrap()
.get(&self.session_key())
.map(|s| s.last_recv_time.clone());
self.post_conn_audit(
json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}),
);
self.normalize_conn_audit_auth_fields();
let mut audit = json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type});
if self.conn_audit_primary_auth != ConnAuditPrimaryAuth::None {
audit["primary_auth"] = json!(self.conn_audit_primary_auth.as_i64());
}
if self.conn_audit_two_factor != ConnAuditTwoFactor::None {
audit["two_factor"] = json!(self.conn_audit_two_factor.as_i64());
}
self.post_conn_audit(audit);
#[allow(unused_mut)]
let mut username = crate::platform::get_active_username();
let mut res = LoginResponse::new();
@@ -2179,6 +2266,7 @@ impl Connection {
if password::temporary_enabled() {
let password = password::temporary_password();
if self.validate_password_plain(&password) {
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::TemporaryPassword);
raii::AuthedConnID::update_or_insert_session(
self.session_key(),
Some(password),
@@ -2202,6 +2290,7 @@ impl Connection {
if local_permanent_password_storage_is_usable_for_auth(&local_storage, &local_salt)
&& self.validate_password_storage(&local_storage)
{
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::PermanentPassword);
print_fallback();
return true;
}
@@ -2210,6 +2299,7 @@ impl Connection {
if preset_permanent_password_storage_is_usable_for_auth(&hard, &salt)
&& self.validate_preset_password_storage(&hard, &salt)
{
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::PermanentPassword);
print_fallback();
return true;
}
@@ -2234,6 +2324,11 @@ impl Connection {
&& (tfa && session.tfa
|| !tfa && self.validate_password_plain(&session.random_password))
{
if tfa {
self.set_conn_audit_two_factor(ConnAuditTwoFactor::Totp);
} else {
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::TemporaryPassword);
}
log::info!("is recent session");
return true;
}
@@ -2327,6 +2422,7 @@ impl Connection {
&& device.platform == lr.my_platform
{
log::info!("2FA bypassed by trusted devices");
self.set_conn_audit_two_factor(ConnAuditTwoFactor::TrustedDevice);
self.require_2fa = None;
}
}
@@ -2619,6 +2715,7 @@ impl Connection {
if res {
self.update_failure(failure, true, 1);
self.require_2fa.take();
self.set_conn_audit_two_factor(ConnAuditTwoFactor::Totp);
raii::AuthedConnID::set_session_2fa(self.session_key());
if !self.send_logon_response_and_keep_alive().await {
return false;
@@ -2674,6 +2771,7 @@ impl Connection {
if let Some((_instant, uuid_old)) = uuid_old {
if uuid == uuid_old {
self.from_switch = true;
self.set_conn_audit_primary_auth(ConnAuditPrimaryAuth::SwitchSides);
if !self.send_logon_response_and_keep_alive().await {
return false;
}
@@ -3668,7 +3766,7 @@ impl Connection {
);
self.send_login_error("Please try 1 minute later").await;
sleep(1.).await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::TerminalOsLoginConcurrency,
json!({
"ip": self.ip,
@@ -3856,7 +3954,7 @@ impl Connection {
prefix_num
))
.await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::ExceedIPv6PrefixAttempts,
json!({
"ip": self.ip,
@@ -3901,7 +3999,7 @@ impl Connection {
if let Some(audit) = decision.audit {
// For OS blocked/backoff events, we currently emit one alarm report per blocked attempt.
// TODO: Add unified cumulative/aggregation fields across alarm producers.
Self::post_alarm_audit(
self.post_alarm_audit(
audit,
json!({
"ip": self.ip,
@@ -3938,7 +4036,7 @@ impl Connection {
let res = if failure.2 > 30 {
self.send_login_error("Too many wrong attempts").await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::ExceedThirtyAttempts,
json!({
"ip": self.ip,
@@ -3949,7 +4047,7 @@ impl Connection {
false
} else if time == failure.0 && failure.1 > 6 {
self.send_login_error("Please try 1 minute later").await;
Self::post_alarm_audit(
self.post_alarm_audit(
AlarmAuditType::SixAttemptsWithinOneMinute,
json!({
"ip": self.ip,
@@ -5490,6 +5588,7 @@ fn try_activate_screen() {
});
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AlarmAuditType {
IpWhitelist = 0,
ExceedThirtyAttempts = 1,
+1 -1
View File
@@ -1,7 +1,7 @@
/// Url handler based on dbus
///
/// Note:
/// On linux, we use dbus to communicate multiple rustdesk process.
/// On linux, we use dbus to communicate between multiple rustdesk processes.
/// [Flutter]: handle uni links for linux
use dbus::blocking::Connection;
use dbus_crossroads::{Crossroads, IfaceBuilder};
+28 -3
View File
@@ -1857,15 +1857,33 @@ impl TerminalServiceProxy {
// Process each session with its own lock
for (terminal_id, session_arc) in sessions {
if let Ok(mut session) = session_arc.try_lock() {
// Check if reader thread is still alive and we haven't sent closed message yet
// Check if the session has ended (reader thread finished or child exited).
// On Linux, the PTY reader thread may not return EOF when the shell exits
// (the cloned master fd keeps the read side open), so we also poll the child
// process via try_wait() as a fallback detection mechanism.
let mut should_send_closed = false;
if !session.closed_message_sent {
if let Some(thread) = &session.reader_thread {
if thread.is_finished() {
should_send_closed = true;
session.closed_message_sent = true;
}
}
if !should_send_closed {
if let Some(child) = &mut session.child {
match child.try_wait() {
Ok(Some(_)) => {
should_send_closed = true;
}
Ok(None) => {} // still running
Err(e) => {
log::warn!("Terminal {} child wait error: {}", terminal_id, e);
}
}
}
}
if should_send_closed {
session.closed_message_sent = true;
}
}
// It's Ok to put the closed message here.
// Because the `reader_thread` is joined in `stop()`,
@@ -2018,7 +2036,8 @@ impl TerminalServiceProxy {
}
}
} else {
// For persistent sessions, just clear the child reference
// For persistent sessions, clear the child reference and remove the session
// if the closed message has been sent (shell has exited).
if let Some(session_arc) = sessions.get(&terminal_id) {
let mut session = session_arc.lock().unwrap();
if let Some(mut child) = session.child.take() {
@@ -2028,6 +2047,12 @@ impl TerminalServiceProxy {
}
add_to_reaper(child);
}
if session.closed_message_sent {
// Shell has exited, remove the dead session
drop(session);
sessions.remove(&terminal_id);
service.lock().unwrap().sessions.remove(&terminal_id);
}
}
}
+13 -4
View File
@@ -143,12 +143,21 @@ fn make_tray() -> hbb_common::ResultType<()> {
}
// We create the icon once the event loop is actually running
// to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90
let tray = TrayIconBuilder::new()
let mut builder = TrayIconBuilder::new()
.with_menu(Box::new(tray_menu.clone()))
.with_tooltip(tooltip(0))
.with_icon(icon.clone())
.with_icon_as_template(true) // mac only
.build();
.with_icon(icon.clone());
#[cfg(target_os = "macos")]
{
builder = builder.with_icon_as_template(true);
}
#[cfg(target_os = "windows")]
{
// Required since tray-icon 0.17
// Fixes #15215, #15222, #15410
builder = builder.with_menu_on_left_click(false);
}
let tray = builder.build();
match tray {
Ok(tray) => _tray_icon = Arc::new(Mutex::new(Some(tray))),
Err(err) => {
+72 -19
View File
@@ -72,6 +72,48 @@ function getExt(name) {
class JobTable: Reactor.Component {
this var jobs = [];
this var job_map = {};
this var next_conflict_batch_id = 1;
this var remembered_write_strategy = {};
function nextConflictBatchId() {
return this.next_conflict_batch_id++;
}
function getRememberedWriteStrategy(conflict_batch_id) {
var is_override = this.remembered_write_strategy[conflict_batch_id];
if (is_override == true || is_override == false) return is_override;
return null;
}
function rememberWriteStrategy(conflict_batch_id, is_override) {
this.remembered_write_strategy[conflict_batch_id] = is_override;
}
function cancelTransferJob(job) {
job.finished = true;
job.err = "cancel";
this.updateJob(job);
}
function cancelTransferConflictBatch(id) {
var job = this.job_map[id];
if (!job) return;
var conflict_batch_id = job.conflict_batch_id;
if (conflict_batch_id == null) {
this.cancelTransferJob(job);
handler.cancel_job(job.id);
refreshDir(!job.is_remote);
return;
}
delete this.remembered_write_strategy[conflict_batch_id];
for (var i = 0; i < this.jobs.length; ++i) {
var current_job = this.jobs[i];
if (current_job.conflict_batch_id != conflict_batch_id || current_job.finished) continue;
this.cancelTransferJob(current_job);
handler.cancel_job(current_job.id);
}
refreshDir(!job.is_remote);
}
function render() {
var me = this;
@@ -109,10 +151,12 @@ class JobTable: Reactor.Component {
function clearAllJobs() {
this.jobs = [];
this.job_map = {};
this.next_conflict_batch_id = 1;
this.remembered_write_strategy = {};
this.update();
}
function send(path, is_remote) {
function send(path, is_remote, conflict_batch_id = null) {
var to;
var show_hidden;
if (is_remote) {
@@ -123,13 +167,15 @@ class JobTable: Reactor.Component {
show_hidden = file_transfer.local_folder_view.show_hidden;
}
if (!to) return;
if (conflict_batch_id == null) conflict_batch_id = this.nextConflictBatchId();
to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path);
var id = handler.get_next_job_id();
this.jobs.push({ type: "transfer",
id: id, path: path, to: to,
include_hidden: show_hidden,
is_remote: is_remote,
is_last: false
is_last: false,
conflict_batch_id: conflict_batch_id
});
this.job_map[id] = this.jobs[this.jobs.length - 1];
handler.send_files(id, 0, path, to, 0, show_hidden, is_remote);
@@ -141,7 +187,8 @@ class JobTable: Reactor.Component {
var job = { type: "transfer",
id: id, path: path, to: to,
include_hidden: show_hidden,
is_remote: is_remote, is_last: true, file_num: file_num };
is_remote: is_remote, is_last: true, file_num: file_num,
conflict_batch_id: this.nextConflictBatchId() };
this.jobs.push(job);
this.job_map[id] = this.jobs[this.jobs.length - 1];
handler.update_next_job_id(id + 1);
@@ -230,6 +277,7 @@ class JobTable: Reactor.Component {
else return translate("Waiting");
}
}
if (job.err == "cancel") return translate("Cancel");
if (!job.entries) return translate("Waiting");
var i = job.file_num + 1;
var n = job.num_entries || job.entries.length;
@@ -262,6 +310,8 @@ class JobTable: Reactor.Component {
function updateJobStatus(id, file_num = -1, err = null, speed = null, finished_size = 0) {
var job = this.job_map[id];
if (!job) return;
if (job.finished && job.err == "cancel") return;
if (job.type == "del-file"){
job.finished = true;
job.err = err;
@@ -269,7 +319,6 @@ class JobTable: Reactor.Component {
this.updateJob(job);
return;
}
if (!job) return;
if (file_num < job.file_num) return;
job.file_num = file_num;
var n = job.num_entries || job.entries.length;
@@ -601,8 +650,9 @@ class FolderView : Reactor.Component {
event click $(.send) () {
var rows = this.getCurrentRows();
if (!rows || rows.length == 0) return;
var conflict_batch_id = file_transfer.job_table.nextConflictBatchId();
for (var i = 0; i < rows.length; ++i) {
file_transfer.job_table.send(rows[i][0], this.is_remote);
file_transfer.job_table.send(rows[i][0], this.is_remote, conflict_batch_id);
}
}
@@ -780,6 +830,13 @@ handler.confirmDeleteFiles = function(id, i, name) {
handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) {
var jt = file_transfer.job_table;
var job = jt.job_map[id];
if (!job || job.finished) return;
var remembered = jt.getRememberedWriteStrategy(job.conflict_batch_id);
if (remembered == true || remembered == false) {
handler.set_write_override(id, file_num, remembered, true, is_upload);
return;
}
var identical_msg = is_identical ? translate("identical_file_tip"): "";
msgbox("custom-skip", "Confirm Write Strategy", "<div .form> \
<div>" + translate('Overwrite') + " " + translate('files') + ".</div> \
@@ -788,22 +845,18 @@ handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical
<div>" + identical_msg + "</div> \
<div><button|checkbox(remember) {ts}>" + translate('Do this for all conflicts') + "</button></div> \
</div>", "", function(res=null) {
var current_job = jt.job_map[id];
if (!current_job || current_job.finished) return;
if (!res) {
jt.updateJobStatus(id, -1, "cancel");
handler.cancel_job(id);
} else if (res.skip) {
if (res.remember){
handler.set_write_override(id,file_num,false,true, is_upload); //
} else {
handler.set_write_override(id,file_num,false,false,is_upload); //
}
} else {
if (res.remember){
handler.set_write_override(id,file_num,true,true,is_upload); //
} else {
handler.set_write_override(id,file_num,true,false,is_upload); //
}
jt.cancelTransferConflictBatch(id);
return;
}
var is_override = !res.skip;
var remember = res.remember ? true : false;
if (remember) {
jt.rememberWriteStrategy(current_job.conflict_batch_id, is_override);
}
handler.set_write_override(id, file_num, is_override, remember, is_upload);
});
}
+1 -1
View File
@@ -603,7 +603,7 @@ class MyIdMenu: Reactor.Component {
<div>Fingerprint: " + handler.get_fingerprint() + " \
<div .link .custom-event url='https://rustdesk.com/privacy.html'>" + translate("Privacy Statement") + "</div> \
<div .link .custom-event url='https://rustdesk.com'>" + translate("Website") + "</div> \
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright &copy; 2025 Purslane Ltd.\
<div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright &copy; 2026 Purslane Tech Pte. Ltd.\
<br />" + handler.get_license() + " \
<p style='font-weight: bold'>" + translate("Slogan_tip") + "</p>\
</div>\
+2
View File
@@ -902,8 +902,10 @@ pub fn get_async_job_status() -> String {
#[inline]
pub fn get_langs() -> String {
use serde_json::json;
let hide_cjk = crate::lang::cjk_ui_unavailable();
let mut x: Vec<(&str, String)> = crate::lang::LANGS
.iter()
.filter(|a| !hide_cjk || !crate::lang::is_cjk_lang(a.0))
.map(|a| (a.0, format!("{} ({})", a.1, a.0)))
.collect();
x.sort_by(|a, b| a.0.cmp(b.0));
+4
View File
@@ -128,6 +128,10 @@ impl ConnectionRoundState {
true
}
}
pub fn is_connected(&self) -> bool {
matches!(self.state, ConnectionState::Connected)
}
}
impl Default for ConnectionRoundState {
+8 -1
View File
@@ -136,10 +136,17 @@ fn check_update(manually: bool) -> ResultType<()> {
let version = download_url.split('/').last().unwrap_or_default();
#[cfg(target_os = "windows")]
let download_url = if cfg!(feature = "flutter") {
let Some(arch) = crate::platform::windows::release_arch_suffix() else {
bail!(
"Unsupported Windows release architecture: {}",
std::env::consts::ARCH
);
};
format!(
"{}/rustdesk-{}-x86_64.{}",
"{}/rustdesk-{}-{}.{}",
download_url,
version,
arch,
if update_msi { "msi" } else { "exe" }
)
} else {

Some files were not shown because too many files have changed in this diff Show More