Compare commits

..

38 Commits

Author SHA1 Message Date
John Fowler
fd7bcf54bd Hungarian language file update (#14497)
* Update Hungarian translations in hu.rs

Translation of new strings and some fixes.
John Fowler.

* Escape quotes in Hungarian language strings

Replacing Hungarian quotation marks

* Update Hungarian translations for various terms

Upload a new translation (hu.rs) file.

* Hungarian language file correction

New character strings translation, error correction.

* Hungarian language file update

New string translations.
2026-03-09 21:28:37 +08:00
layla
db3f5fe816 Fix typo: Rustdesk to RustDesk in Russian README (#14468) 2026-03-08 19:18:59 +08:00
fufesou
0d3016fcd8 fix(flutter): reduce accidental horizontal trackpad scrolling during vertical pan (#14460)
* fix(flutter): reduce accidental horizontal trackpad scrolling during vertical pan

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

* refact: comments

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-05 23:10:39 +08:00
21pages
1abc897c45 fix avatar fallback (#14458)
* fix avatar fallback

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

* fix(ui): improve avatar fallback handling and layout consistency

  - Always show spacing in account section regardless of avatar presence
  - Handle null return from buildAvatarWidget with proper fallback
  - Adjust mobile settings avatar size to 28

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-03-05 12:30:40 +08:00
RustDesk
ab64a32f30 avatar (#14440)
* avatar

* refactor avatar display: unify rendering and resolve at use time

  - Extract buildAvatarWidget() in common.dart to share avatar rendering
    logic across desktop settings, desktop CM and mobile CM
  - Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn),
    to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs
  - Store avatar as-is in local config, only resolve when displaying
    (settings page) or sending (LoginRequest)
  - Resolve avatar in LoginRequest before sending to remote peer
  - Add error handling for network image load failures
  - Guard against empty client.name[0] crash
  - Show avatar in mobile settings page account tile

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

* web: implement mainResolveAvatarUrl via js getByName

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

* increase ipc Data enum size limit to 120 bytes

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-03-04 21:43:19 +08:00
RustDesk
52b66e71d1 Move port mapping afterwards (#14448)
* move port mapping after auth in port forwarding

* fix(port-forward): try connect after 2fa

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

* fix(security): gate port-forward connect on full auth and clarify login flow semantics

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

* refact(port-forward): comments and logs

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-03-04 15:48:42 +08:00
fufesou
41ab5bbdd8 fix(update): macos, test before update (#14446)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-03 10:47:32 +08:00
fufesou
732b250815 fix(keyboard): legacy mode (#14435)
* fix(keyboard): legacy mode

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

* Simple refactor

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

* fix(keyboard): legacy mode, chr to seq

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

* fix(keyboard): legacy mode, early return if (!hotkey)&down

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

* fix(keyboard): legacy mode, pair down/up

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-02 19:07:09 +08:00
rustdesk
157dbdc543 fix avatar in hbb_common 2026-03-02 12:14:26 +08:00
rustdesk
6ba23683d5 avatar in libs/hbb_comon 2026-03-02 12:06:20 +08:00
fufesou
80a5865db3 macOS update: restore LaunchAgent in GUI session and isolate temp update dir by euid (#14434)
* fix(update): macos, load agent

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

* fix(update): macos, isolate temp update dir by euid

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

* refact(update): macos script

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-01 20:06:04 +08:00
MichaIng
9cb6f38aea packaging: deb: remove obsolete Python version check (#14429)
It was used to conditionally install a Python module in the past. But that is not the case anymore since https://github.com/rustdesk/rustdesk/commit/37dbfcc. Now the check is obsolete.

Due to `set -e`, the check leads to a package configuration failure if Python is not installed, which however otherwise is not needed for RustDesk.

The commit includes an indentation fix and trailing space removal.

Signed-off-by: MichaIng <micha@dietpi.com>
2026-03-01 18:05:19 +08:00
fufesou
cd7e3e4505 fix(update): macos, input password (#14430)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-01 15:19:07 +08:00
fufesou
1833cb0655 fix(update): revert check (#14424)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-28 18:17:26 +08:00
fufesou
e4208aa9cf fix(update): revert check (#14423)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-28 16:33:54 +08:00
Amirhosein Akhlaghpoor
bb3501a4f9 ui: scale wheel lines on Windows/Linux to Mac (#14395)
* input: accelerate wheel bursts on Windows->Mac

- boost fast wheel bursts without affecting single-step scrolls\n- use dominant-axis smooth detection and velocity gate\n- reset wheel timestamp on enter/leave\n- enforce single-axis scrolling\n- extract/tune Sciter wheel accel thresholds

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* input: clarify wheel burst tuning

- add comments on acceleration rules and units\n- apply burst accel on Windows/Linux to macOS\n- reset wheel timing on enter/leave

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* input: align wheel burst velocity thresholds

- match Flutter velocity gate with Sciter

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* input: restore flutter wheel velocity threshold

- keep burst threshold at 0.002 delta/us

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

---------

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>
2026-02-28 10:56:25 +08:00
fufesou
4abdb2e08b feat: windows, custom client, update (#13687)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-27 21:50:20 +08:00
rustdesk
d49ae493b2 bump to 1.4.6 2026-02-27 20:53:40 +08:00
VenusGirl❤
394079833e Update ko.rs (#14418) 2026-02-27 20:14:14 +08:00
Alex Rijckaert
12d6789c2e Update translation (#14413) 2026-02-27 12:09:47 +08:00
memory_clear
34803f8e9b Update labels for keep awake during sessions (#14391) 2026-02-26 18:28:19 +08:00
solokot
fd43184406 Update ru.rs (#14386) 2026-02-26 18:28:09 +08:00
Mr-Update
3cc3315081 Update de.rs (#14385) 2026-02-26 18:27:58 +08:00
RustDesk
6aee70fa18 fix https://github.com/rustdesk/rustdesk/issues/609#issuecomment-3931613118 (#14364) 2026-02-25 17:09:51 +08:00
rustdesk
82a9fd1540 change port forward listen to localhost 2026-02-24 21:57:55 +08:00
rustdesk
dc760d6ca8 remove .claude 2026-02-24 21:52:26 +08:00
Amirhosein Akhlaghpoor
eb239501bc Fix logon-screen password with click approval (#14335) 2026-02-24 21:14:18 +08:00
fufesou
0016033937 feat(terminal): add reconnection buffer support for persistent sessions (#14377)
* feat(terminal): add reconnection buffer support for persistent sessions

Fix two related issues:
1. Reconnecting to persistent sessions shows blank screen - server now
   automatically sends historical buffer on reconnection via SessionState
   machine with pending_buffer, eliminating the need for client-initiated
   buffer requests.
2. Terminal output before view ready causes NaN errors - buffer output
   chunks on client side until terminal view has valid dimensions, then
   flush in order on first valid resize.

Rust side:
- Introduce SessionState enum (Closed/Active) replacing bool is_opened
- Auto-attach pending buffer on reconnection in handle_open()
- Always drain output channel in read_outputs() to prevent overflow
- Increase channel buffer from 100 to 500
- Optimize get_recent() to collect whole chunks (avoids ANSI truncation)
- Extract create_terminal_data_response() helper (DRY)
- Add reconnected flag to TerminalOpened protobuf message

Flutter side:
- Buffer output chunks until terminal view has valid dimensions
- Flush buffered output on first valid resize via _markViewReady()
- Clear terminal on reconnection to avoid duplicate output from buffer replay
- Fix max_bytes type (u32) to match protobuf definition
- Pass reconnected field through FlutterHandler event

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

* fix(terminal): add two-phase SIGWINCH for TUI app redraw and session remap on reconnection

Fix TUI apps (top, htop) not redrawing after reconnection. A single
resize-then-restore is too fast for ncurses to detect a size change,
so split across two read_outputs() polling cycles (~30ms apart) to
force a full redraw.

Also fix reconnection failure when client terminal_id doesn't match
any surviving server-side session ID by remapping the lowest surviving
session to the requested ID.

Rust side:
- Add two-phase SIGWINCH state machine (SigwinchPhase: TempResize →
  Restore → Idle) with retry logic (max 3 attempts per phase)
- Add do_sigwinch_resize() for cross-platform PTY resize (direct PTY
  and Windows helper mode)
- Add session remap logic for non-contiguous terminal_id reconnection
- Extract try_send_output() helper with rate-limited drop logging (DRY)
- Add 3-byte limit to UTF-8 continuation byte skipping in get_recent()
  to prevent runaway on non-UTF-8 binary data
- Remove reconnected flag from flutter.rs (unused on client side)

Flutter side:
- Add reconnection screen clear and deferred flush logic
- Filter self from persistent_sessions restore list
- Add comments for web-related changes

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-24 21:12:06 +08:00
bilimiyorum
50c62d5eac Update tr.rs (#14376)
New string entry
2026-02-24 16:30:32 +08:00
Lynilia
91ac48912e Update fr.rs (#14383) 2026-02-24 16:30:12 +08:00
John Fowler
272a6604cd Hungarian language file correction (#14382)
* Update Hungarian translations in hu.rs

Translation of new strings and some fixes.
John Fowler.

* Escape quotes in Hungarian language strings

Replacing Hungarian quotation marks

* Update Hungarian translations for various terms

Upload a new translation (hu.rs) file.

* Hungarian language file correction

New character strings translation, error correction.
2026-02-24 16:29:54 +08:00
westor
8a889d3ebb Update el.rs translation (#14378)
- Added missing language strings.
- Fixed some previously typo translations.
- Updated some translation strings.
2026-02-24 16:29:43 +08:00
bovirus
17a3f2ae52 Italian language update (#14375) 2026-02-23 16:37:53 +08:00
RustDesk
6c3515588f - UI display: display_name first (#14358)
* - UI display: display_name first
  - Fallback: name
  - Technical identity: still name

  ### What changed

  - Added account display helpers and display_name state in user model:
      - flutter/lib/models/user_model.dart:16
  - Account/logout label now uses display_name (@name) when both exist:
      - flutter/lib/mobile/pages/settings_page.dart:689
      - flutter/lib/desktop/pages/desktop_setting_page.dart:2016
      - flutter/lib/desktop/pages/desktop_setting_page.dart:2135
  - Desktop Account info now shows both when applicable:
      - Display Name: ...
      - Username: ...
      - flutter/lib/desktop/pages/desktop_setting_page.dart:2039
  - Previously done group-list behavior remains:
      - group user list displays display_name with name fallback
      - flutter/lib/common/widgets/my_group.dart:187
  - Persistence path for display_name remains enabled (including group cache/submodule field):
      - libs/hbb_common/src/config.rs:2347
  - src/client.rs:2630
  - LoginRequest.my_name now resolves as:
      1. OPTION_DISPLAY_NAME (manual override)
      2. user_info.display_name
      3. user_info.name
      4. OS username fallback

* 1. GUID key (...Uninstall\{GUID}) is MSI-native metadata generated by Windows Installer.
  2. Non-GUID key (...Uninstall\RustDesk) is explicitly written by RustDesk’s MSI compatibility component in res/msi/Package/Components/Regs.wxs:44, populated by preprocess.py --arp from .github/workflows/
     flutter-build.yml:262.

  So they were not using the same EstimatedSize logic:

  - MSI GUID key: MSI-calculated size (KB).
  - RustDesk key: custom script value from res/msi/preprocess.py:339 (previously bytes, now fixed to KB).

  That mismatch is exactly why you saw different sizes.

* improve display name handling

  - Append (@username) when multiple users share the same display name
  - Trim whitespace from display_name before comparison and display
  - Add missing translate() for Logout button on desktop

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

* group peer filter match both user's display name and user's name

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

* case-insensitive search in group peer filter

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-02-22 14:59:25 +08:00
fufesou
4d2d2118a2 Fix/terminal tab close persistent (#14359)
* fix(terminal): ensure tab close is resilient to session cleanup failures

- Wrap _closeTerminalSessionIfNeeded in isolated try/catch so that
  tabController.closeBy always executes even if FFI calls throw
- Add clarifying comment in handleWindowCloseButton for single-tab
  audit dialog flow

* fix(terminal): fix session reconnect ID mismatch and tab close race condition

Remap surviving persistent sessions to client-requested terminal IDs on
reconnect, preventing new shell creation when IDs are non-contiguous.
Snapshot peerTabCount before async operations in _closeTab to avoid race
with concurrent _closeAllTabs clearing the tab controller.
Remove debug log statements.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-21 11:06:13 +08:00
fufesou
483fe80308 fix(terminal): fix new tab auto-focus and NaN error on data before layout (#14357)
- Fix new tab not auto-focusing: add FocusNode to TerminalView and
  request focus when tab is selected via tab state listener
- Fix NaN error when data arrives before terminal view layout: buffer
  output data until terminal view has valid dimensions, flush on first
  valid resize callback

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-20 14:44:25 +08:00
fufesou
34ceeac36e fix(terminal): fix tabKey parsing for peerIds containing underscores (#14354)
Terminal tab keys use the format "peerId_terminalId". The previous code
used split('_')[0] or startsWith('$peerId_') to extract the peerId,
which breaks when the peerId itself contains underscores.

This can happen in two scenarios:
- Hostname-based ID: when OPTION_ALLOW_HOSTNAME_AS_ID is enabled, the
  peerId is derived from the system hostname, which commonly contains
  underscores (e.g. "my_dev_machine").
- Custom ID: the validation regex ^[a-zA-Z][\w-]{5,15}$ allows
  underscores since \w matches [a-zA-Z0-9_], so IDs like "my_dev_01"
  are valid.

Fix all three parsing sites in terminal_tab_page.dart to use
lastIndexOf('_'), which is safe because terminalId is always a plain
integer with no underscores.
2026-02-19 23:45:06 +08:00
cui
20f11018ce fix: lte should be lt like in linux.rs (#14344) 2026-02-19 22:24:32 +08:00
71 changed files with 2569 additions and 752 deletions

View File

@@ -1,56 +0,0 @@
You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to analyze and improve the instructions for Claude Code.
Follow these steps carefully:
1. Analysis Phase:
Review the chat history in your context window.
Then, examine the current Claude instructions, commands and config
<claude_instructions>
/CLAUDE.md
/.claude/commands/*
**/CLAUDE.md
.claude/settings.json
.claude/settings.local.json
</claude_instructions>
Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for:
- Inconsistencies in Claude's responses
- Misunderstandings of user requests
- Areas where Claude could provide more detailed or accurate information
- Opportunities to enhance Claude's ability to handle specific types of queries or tasks
- New commands or improvements to a commands name, function or response
- Permissions and MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them for the command to work
2. Interaction Phase:
Present your findings and improvement ideas to the human. For each suggestion:
a) Explain the current issue you've identified
b) Propose a specific change or addition to the instructions
c) Describe how this change would improve Claude's performance
Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the implementation phase. If not, refine your suggestion or move on to the next idea.
3. Implementation Phase:
For each approved change:
a) Clearly state the section of the instructions you're modifying
b) Present the new or modified text for that section
c) Explain how this change addresses the issue identified in the analysis phase
4. Output Format:
Present your final output in the following structure:
<analysis>
[List the issues identified and potential improvements]
</analysis>
<improvements>
[For each approved improvement:
1. Section being modified
2. New or modified instruction text
3. Explanation of how this addresses the identified issue]
</improvements>
<final_instructions>
[Present the complete, updated set of instructions for Claude, incorporating all approved changes]
</final_instructions>
Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your implementations.

View File

@@ -39,7 +39,7 @@ env:
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.4.5"
VERSION: "1.4.6"
NDK_VERSION: "r27c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"

View File

@@ -17,7 +17,7 @@ env:
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.5"
VERSION: "1.4.6"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"

View File

@@ -10,6 +10,6 @@ jobs:
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: RustDesk.RustDesk
version: "1.4.5"
release-tag: "1.4.5"
version: "1.4.6"
release-tag: "1.4.6"
token: ${{ secrets.WINGET_TOKEN }}

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
.vscode
.idea
.DS_Store
.env
libsciter-gtk.so
src/ui/inline.rs
extractor

4
Cargo.lock generated
View File

@@ -7134,7 +7134,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.4.5"
version = "1.4.6"
dependencies = [
"android-wakelock",
"android_logger",
@@ -7249,7 +7249,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
version = "1.4.5"
version = "1.4.6"
dependencies = [
"brotli",
"dirs 5.0.1",

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.4.5"
version = "1.4.6"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.5
version: 1.4.6
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.5
version: 1.4.6
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -167,7 +167,7 @@ target/release/rustdesk
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс на Sciter (устаревшее)
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио, буфера обмена, ввода, видео и сетевых подключений
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое соединение
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером Rustdesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter

View File

@@ -3063,6 +3063,11 @@ Future<void> start_service(bool is_start) async {
}
Future<bool> canBeBlocked() async {
if (isWeb) {
// Web can only act as a controller, never as a controlled side,
// so it should never be blocked by a remote session.
return false;
}
// First check control permission
final controlPermission = await bind.mainGetCommon(
key: "is-remote-modify-enabled-by-control-permissions");
@@ -4113,3 +4118,43 @@ String mouseButtonsToPeer(int buttons) {
return '';
}
}
/// Build an avatar widget from an avatar URL or data URI string.
/// Returns [fallback] if avatar is empty or cannot be decoded.
/// [borderRadius] defaults to [size]/2 (circle).
Widget? buildAvatarWidget({
required String avatar,
required double size,
double? borderRadius,
Widget? fallback,
}) {
final trimmed = avatar.trim();
if (trimmed.isEmpty) return fallback;
ImageProvider? imageProvider;
if (trimmed.startsWith('data:image/')) {
final comma = trimmed.indexOf(',');
if (comma > 0) {
try {
imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1)));
} catch (_) {}
}
} else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
imageProvider = NetworkImage(trimmed);
}
if (imageProvider == null) return fallback;
final radius = borderRadius ?? size / 2;
return ClipRRect(
borderRadius: BorderRadius.circular(radius),
child: Image(
image: imageProvider,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
fallback ?? SizedBox.shrink(),
),
);
}

View File

@@ -26,6 +26,7 @@ enum UserStatus { kDisabled, kNormal, kUnverified }
class UserPayload {
String name = '';
String displayName = '';
String avatar = '';
String email = '';
String note = '';
String? verifier;
@@ -35,6 +36,7 @@ class UserPayload {
UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
displayName = json['display_name'] ?? '',
avatar = json['avatar'] ?? '',
email = json['email'] ?? '',
note = json['note'] ?? '',
verifier = json['verifier'],
@@ -49,6 +51,7 @@ class UserPayload {
final Map<String, dynamic> map = {
'name': name,
'display_name': displayName,
'avatar': avatar,
'status': status == UserStatus.kDisabled
? 0
: status == UserStatus.kUnverified

View File

@@ -570,11 +570,14 @@ class MyGroupPeerView extends BasePeersView {
static bool filter(Peer peer) {
final model = gFFI.groupModel;
if (model.searchAccessibleItemNameText.isNotEmpty) {
final text = model.searchAccessibleItemNameText.value;
final searchPeersOfUser = peer.loginName.contains(text) &&
model.users.any((user) => user.name == peer.loginName);
final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) &&
model.deviceGroups.any((g) => g.name == peer.device_group_name);
final text = model.searchAccessibleItemNameText.value.toLowerCase();
final searchPeersOfUser = model.users.any((user) =>
user.name == peer.loginName &&
(user.name.toLowerCase().contains(text) ||
user.displayNameOrName.toLowerCase().contains(text)));
final searchPeersOfDeviceGroup =
peer.device_group_name.toLowerCase().contains(text) &&
model.deviceGroups.any((g) => g.name == peer.device_group_name);
if (!searchPeersOfUser && !searchPeersOfDeviceGroup) {
return false;
}

View File

@@ -473,8 +473,7 @@ class _GeneralState extends State<_General> {
}
Widget other() {
final showAutoUpdate =
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
final showAutoUpdate = isWindows && bind.mainIsInstalled();
final children = <Widget>[
if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
@@ -2027,28 +2026,65 @@ class _AccountState extends State<_Account> {
}
Widget useInfo() {
text(String key, String value) {
return Align(
alignment: Alignment.centerLeft,
child: SelectionArea(child: Text('${translate(key)}: $value'))
.marginSymmetric(vertical: 4),
);
}
return Obx(() => Offstage(
offstage: gFFI.userModel.userName.value.isEmpty,
child: Column(
children: [
if (gFFI.userModel.displayName.value.trim().isNotEmpty &&
gFFI.userModel.displayName.value.trim() !=
gFFI.userModel.userName.value.trim())
text('Display Name', gFFI.userModel.displayName.value.trim()),
text('Username', gFFI.userModel.userName.value),
// text('Group', gFFI.groupModel.groupName.value),
],
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(10),
),
child: Builder(builder: (context) {
final avatarWidget = _buildUserAvatar();
return Row(
children: [
if (avatarWidget != null) avatarWidget,
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
gFFI.userModel.displayNameOrUserName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
SelectionArea(
child: Text(
'@${gFFI.userModel.userName.value}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color:
Theme.of(context).textTheme.bodySmall?.color,
),
),
),
],
),
),
],
);
}),
),
)).marginOnly(left: 18, top: 16);
}
Widget? _buildUserAvatar() {
// Resolve relative avatar path at display time
final avatar =
bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
return buildAvatarWidget(
avatar: avatar,
size: 44,
);
}
}
class _Checkbox extends StatefulWidget {

View File

@@ -462,23 +462,7 @@ class _CmHeaderState extends State<_CmHeader>
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 70,
height: 70,
alignment: Alignment.center,
decoration: BoxDecoration(
color: str2color(client.name),
borderRadius: BorderRadius.circular(15.0),
),
child: Text(
client.name[0],
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 55,
),
),
).marginOnly(right: 10.0),
_buildClientAvatar().marginOnly(right: 10.0),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
@@ -582,6 +566,36 @@ class _CmHeaderState extends State<_CmHeader>
@override
bool get wantKeepAlive => true;
Widget _buildClientAvatar() {
return buildAvatarWidget(
avatar: client.avatar,
size: 70,
borderRadius: 15,
fallback: _buildInitialAvatar(),
) ??
_buildInitialAvatar();
}
Widget _buildInitialAvatar() {
return Container(
width: 70,
height: 70,
alignment: Alignment.center,
decoration: BoxDecoration(
color: str2color(client.name),
borderRadius: BorderRadius.circular(15.0),
),
child: Text(
client.name.isNotEmpty ? client.name[0] : '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 55,
),
),
);
}
}
class _PrivilegeBoard extends StatefulWidget {

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
@@ -15,6 +16,7 @@ class TerminalPage extends StatefulWidget {
required this.tabController,
required this.isSharedPassword,
required this.terminalId,
required this.tabKey,
this.forceRelay,
this.connToken,
}) : super(key: key);
@@ -25,6 +27,8 @@ class TerminalPage extends StatefulWidget {
final bool? isSharedPassword;
final String? connToken;
final int terminalId;
/// Tab key for focus management, passed from parent to avoid duplicate construction
final String tabKey;
final SimpleWrapper<State<TerminalPage>?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi;
@@ -42,11 +46,16 @@ class _TerminalPageState extends State<TerminalPage>
late FFI _ffi;
late TerminalModel _terminalModel;
double? _cellHeight;
final FocusNode _terminalFocusNode = FocusNode(canRequestFocus: false);
StreamSubscription<DesktopTabState>? _tabStateSubscription;
@override
void initState() {
super.initState();
// Listen for tab selection changes to request focus
_tabStateSubscription = widget.tabController.state.listen(_onTabStateChanged);
// Use shared FFI instance from connection manager
_ffi = TerminalConnectionManager.getConnection(
peerId: widget.id,
@@ -64,6 +73,13 @@ class _TerminalPageState extends State<TerminalPage>
_terminalModel.onResizeExternal = (w, h, pw, ph) {
_cellHeight = ph * 1.0;
// Enable focus once terminal has valid dimensions (first valid resize)
if (!_terminalFocusNode.canRequestFocus && w > 0 && h > 0) {
_terminalFocusNode.canRequestFocus = true;
// Auto-focus if this tab is currently selected
_requestFocusIfSelected();
}
// Schedule the setState for the next frame
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@@ -99,14 +115,42 @@ class _TerminalPageState extends State<TerminalPage>
@override
void dispose() {
// Cancel tab state subscription to prevent memory leak
_tabStateSubscription?.cancel();
// Unregister terminal model from FFI
_ffi.unregisterTerminalModel(widget.terminalId);
_terminalModel.dispose();
_terminalFocusNode.dispose();
// Release connection reference instead of closing directly
TerminalConnectionManager.releaseConnection(widget.id);
super.dispose();
}
void _onTabStateChanged(DesktopTabState state) {
// Check if this tab is now selected and request focus
if (state.selected >= 0 && state.selected < state.tabs.length) {
final selectedTab = state.tabs[state.selected];
if (selectedTab.key == widget.tabKey && mounted) {
_requestFocusIfSelected();
}
}
}
void _requestFocusIfSelected() {
if (!mounted || !_terminalFocusNode.canRequestFocus) return;
// Use post-frame callback to ensure widget is fully laid out in focus tree
WidgetsBinding.instance.addPostFrameCallback((_) {
// Re-check conditions after frame: mounted, focusable, still selected, not already focused
if (!mounted || !_terminalFocusNode.canRequestFocus || _terminalFocusNode.hasFocus) return;
final state = widget.tabController.state.value;
if (state.selected >= 0 && state.selected < state.tabs.length) {
if (state.tabs[state.selected].key == widget.tabKey) {
_terminalFocusNode.requestFocus();
}
}
});
}
// This method ensures that the number of visible rows is an integer by computing the
// extra space left after dividing the available height by the height of a single
// terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding.
@@ -131,7 +175,9 @@ class _TerminalPageState extends State<TerminalPage>
return TerminalView(
_terminalModel.terminal,
controller: _terminalModel.terminalController,
autofocus: true,
focusNode: _terminalFocusNode,
// Note: autofocus is not used here because focus is managed manually
// via _onTabStateChanged() to handle tab switching properly.
backgroundOpacity: 0.7,
padding: _calculatePadding(heightPx),
onSecondaryTapDown: (details, offset) async {

View File

@@ -34,6 +34,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
static const IconData selectedIcon = Icons.terminal;
static const IconData unselectedIcon = Icons.terminal_outlined;
int _nextTerminalId = 1;
// Lightweight idempotency guard for async close operations
final Set<String> _closingTabs = {};
// When true, all session cleanup should persist (window-level close in progress)
bool _windowClosing = false;
_TerminalTabPageState(Map<String, dynamic> params) {
Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
@@ -70,28 +74,12 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
label: tabLabel,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () async {
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabKey,
tabController: tabController,
)) {
return;
}
// Close the terminal session first
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
if (ffi != null) {
final terminalModel = ffi.terminalModels[terminalId];
if (terminalModel != null) {
await terminalModel.closeTerminal();
}
}
// Then close the tab
tabController.closeBy(tabKey);
},
onTabCloseButton: () => _closeTab(tabKey),
page: TerminalPage(
key: ValueKey(tabKey),
id: peerId,
terminalId: terminalId,
tabKey: tabKey,
password: password,
isSharedPassword: isSharedPassword,
tabController: tabController,
@@ -101,6 +89,159 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
);
}
/// Unified tab close handler for all close paths (button, shortcut, programmatic).
/// Shows audit dialog, cleans up session if not persistent, then removes the UI tab.
Future<void> _closeTab(String tabKey) async {
// Idempotency guard: skip if already closing this tab
if (_closingTabs.contains(tabKey)) return;
_closingTabs.add(tabKey);
try {
// Snapshot peerTabCount BEFORE any await to avoid race with concurrent
// _closeAllTabs clearing tabController (which would make the live count
// drop to 0 and incorrectly trigger session persistence).
// Note: the snapshot may become stale if other individual tabs are closed
// during the audit dialog, but this is an acceptable trade-off.
int? snapshotPeerTabCount;
final parsed = _parseTabKey(tabKey);
if (parsed != null) {
final (peerId, _) = parsed;
snapshotPeerTabCount = tabController.state.value.tabs.where((t) {
final p = _parseTabKey(t.key);
return p != null && p.$1 == peerId;
}).length;
}
if (await desktopTryShowTabAuditDialogCloseCancelled(
id: tabKey,
tabController: tabController,
)) {
return;
}
// Close terminal session if not in persistent mode.
// Wrapped separately so session cleanup failure never blocks UI tab removal.
try {
await _closeTerminalSessionIfNeeded(tabKey,
peerTabCount: snapshotPeerTabCount);
} catch (e) {
debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e');
}
// Always close the tab from UI, regardless of session cleanup result
tabController.closeBy(tabKey);
} catch (e) {
debugPrint('[TerminalTabPage] Error closing tab $tabKey: $e');
} finally {
_closingTabs.remove(tabKey);
}
}
/// Close all tabs with session cleanup.
/// Used for window-level close operations (onDestroy, handleWindowCloseButton).
/// UI tabs are removed immediately; session cleanup runs in parallel with a
/// bounded timeout so window close is not blocked indefinitely.
Future<void> _closeAllTabs() async {
_windowClosing = true;
final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList();
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
tabController.clear();
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
final futures = tabKeys
.where((tabKey) => !_closingTabs.contains(tabKey))
.map((tabKey) async {
try {
await _closeTerminalSessionIfNeeded(tabKey, persistAll: true);
} catch (e) {
debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e');
}
}).toList();
if (futures.isNotEmpty) {
await Future.wait(futures).timeout(
const Duration(seconds: 4),
onTimeout: () {
debugPrint(
'[TerminalTabPage] Session cleanup timed out for batch close');
return [];
},
);
}
}
/// Close the terminal session on server side based on persistent mode.
///
/// [persistAll] controls behavior when persistent mode is enabled:
/// - `true` (window close): persist all sessions, don't close any.
/// - `false` (tab close): only persist the last session for the peer,
/// close others so only the most recent disconnected session survives.
///
/// Note: if [_windowClosing] is true, persistAll is forced to true so that
/// in-flight _closeTab() calls don't accidentally close sessions that the
/// window-close flow intends to preserve.
Future<void> _closeTerminalSessionIfNeeded(String tabKey,
{bool persistAll = false, int? peerTabCount}) async {
// If window close is in progress, override to persist all sessions
// even if this call originated from an individual tab close.
if (_windowClosing) {
persistAll = true;
}
final parsed = _parseTabKey(tabKey);
if (parsed == null) return;
final (peerId, terminalId) = parsed;
final ffi = TerminalConnectionManager.getExistingConnection(peerId);
if (ffi == null) return;
final isPersistent = bind.sessionGetToggleOptionSync(
sessionId: ffi.sessionId,
arg: kOptionTerminalPersistent,
);
if (isPersistent) {
if (persistAll) {
// Window close: persist all sessions
return;
}
// Tab close: only persist if this is the last tab for this peer.
// Use the snapshot value if provided (avoids race with concurrent tab removal).
final effectivePeerTabCount = peerTabCount ??
tabController.state.value.tabs.where((t) {
final p = _parseTabKey(t.key);
return p != null && p.$1 == peerId;
}).length;
if (effectivePeerTabCount <= 1) {
// Last tab for this peer — persist the session
return;
}
// Not the last tab — fall through to close the session
}
final terminalModel = ffi.terminalModels[terminalId];
if (terminalModel != null) {
// closeTerminal() has internal 3s timeout, no need for external timeout
await terminalModel.closeTerminal();
}
}
/// Parse tabKey (format: "peerId_terminalId") into its components.
/// Note: peerId may contain underscores, so we use lastIndexOf('_').
/// Returns null if tabKey format is invalid.
(String peerId, int terminalId)? _parseTabKey(String tabKey) {
final lastUnderscore = tabKey.lastIndexOf('_');
if (lastUnderscore <= 0) {
debugPrint('[TerminalTabPage] Invalid tabKey format: $tabKey');
return null;
}
final terminalIdStr = tabKey.substring(lastUnderscore + 1);
final terminalId = int.tryParse(terminalIdStr);
if (terminalId == null) {
debugPrint('[TerminalTabPage] Invalid terminalId in tabKey: $tabKey');
return null;
}
final peerId = tabKey.substring(0, lastUnderscore);
return (peerId, terminalId);
}
Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) {
final List<MenuEntryBase<String>> menu = [];
const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0);
@@ -184,7 +325,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
} else if (call.method == kWindowEventRestoreTerminalSessions) {
_restoreSessions(call.arguments);
} else if (call.method == "onDestroy") {
tabController.clear();
// Clean up sessions before window destruction (bounded wait)
await _closeAllTabs();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
@@ -194,7 +336,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
final currentTab = tabController.state.value.selectedTabInfo;
assert(call.arguments is String,
"Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}");
if (currentTab.key.startsWith(call.arguments)) {
// Use lastIndexOf to handle peerIds containing underscores
final lastUnderscore = currentTab.key.lastIndexOf('_');
if (lastUnderscore > 0 &&
currentTab.key.substring(0, lastUnderscore) == call.arguments) {
windowOnTop(windowId());
return true;
}
@@ -265,7 +410,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
// macOS: Cmd+W (standard for close tab)
final currentTab = tabController.state.value.selectedTabInfo;
if (tabController.state.value.tabs.length > 1) {
tabController.closeBy(currentTab.key);
_closeTab(currentTab.key);
return true;
}
} else if (!isMacOS &&
@@ -274,7 +419,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
// Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete)
final currentTab = tabController.state.value.selectedTabInfo;
if (tabController.state.value.tabs.length > 1) {
tabController.closeBy(currentTab.key);
_closeTab(currentTab.key);
return true;
}
}
@@ -329,7 +474,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
void _addNewTerminal(String peerId, {int? terminalId}) {
// Find first tab for this peer to get connection parameters
final firstTab = tabController.state.value.tabs.firstWhere(
(tab) => tab.key.startsWith('$peerId\_'),
(tab) {
final last = tab.key.lastIndexOf('_');
return last > 0 && tab.key.substring(0, last) == peerId;
},
);
if (firstTab.page is TerminalPage) {
final page = firstTab.page as TerminalPage;
@@ -350,11 +498,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
void _addNewTerminalForCurrentPeer({int? terminalId}) {
final currentTab = tabController.state.value.selectedTabInfo;
final parts = currentTab.key.split('_');
if (parts.isNotEmpty) {
final peerId = parts[0];
_addNewTerminal(peerId, terminalId: terminalId);
}
final parsed = _parseTabKey(currentTab.key);
if (parsed == null) return;
final (peerId, _) = parsed;
_addNewTerminal(peerId, terminalId: terminalId);
}
@override
@@ -368,10 +515,9 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
selectedBorderColor: MyTheme.accent,
labelGetter: DesktopTab.tablabelGetter,
tabMenuBuilder: (key) {
// Extract peerId from tab key (format: "peerId_terminalId")
final parts = key.split('_');
if (parts.isEmpty) return Container();
final peerId = parts[0];
final parsed = _parseTabKey(key);
if (parsed == null) return Container();
final (peerId, _) = parsed;
return _tabMenuBuilder(peerId, () {});
},
));
@@ -426,7 +572,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
}
}
if (connLength <= 1) {
tabController.clear();
await _closeAllTabs();
return true;
} else {
final bool res;
@@ -437,7 +583,7 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
res = await closeConfirmDialog();
}
if (res) {
tabController.clear();
await _closeAllTabs();
}
return res;
}

View File

@@ -841,13 +841,7 @@ class ClientInfo extends StatelessWidget {
flex: -1,
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: CircleAvatar(
backgroundColor: str2color(
client.name,
Theme.of(context).brightness == Brightness.light
? 255
: 150),
child: Text(client.name[0])))),
child: _buildAvatar(context))),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -860,6 +854,20 @@ class ClientInfo extends StatelessWidget {
),
]));
}
Widget _buildAvatar(BuildContext context) {
final fallback = CircleAvatar(
backgroundColor: str2color(client.name,
Theme.of(context).brightness == Brightness.light ? 255 : 150),
child: Text(client.name.isNotEmpty ? client.name[0] : '?'),
);
return buildAvatarWidget(
avatar: client.avatar,
size: 40,
fallback: fallback,
) ??
fallback;
}
}
void androidChannelInit() {

View File

@@ -617,7 +617,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onToggle: (bool v) async {
await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v);
final newValue =
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
setState(() {
_showTerminalExtraKeys = newValue;
});
@@ -689,7 +689,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
? translate('Login')
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
leading: Icon(Icons.person),
leading: Obx(() {
final avatar = bind.mainResolveAvatarUrl(
avatar: gFFI.userModel.avatar.value);
return buildAvatarWidget(
avatar: avatar,
size: 28,
borderRadius: null,
fallback: Icon(Icons.person),
) ??
Icon(Icons.person);
}),
onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) {
loginDialog();
@@ -829,10 +839,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
),
if (!incomingOnly)
SettingsTile.switchTile(
title: Text(translate('keep-awake-during-outgoing-sessions-label')),
title:
Text(translate('keep-awake-during-outgoing-sessions-label')),
initialValue: _preventSleepWhileConnected,
onToggle: (v) async {
await mainSetLocalBoolOption(kOptionKeepAwakeDuringOutgoingSessions, v);
await mainSetLocalBoolOption(
kOptionKeepAwakeDuringOutgoingSessions, v);
setState(() {
_preventSleepWhileConnected = v;
});

View File

@@ -83,7 +83,10 @@ class _TerminalPageState extends State<TerminalPage>
// Register this terminal model with FFI for event routing
_ffi.registerTerminalModel(widget.terminalId, _terminalModel);
_showTerminalExtraKeys = mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
// Web desktop users have full hardware keyboard access, so the on-screen
// terminal extra keys bar is unnecessary and disabled.
_showTerminalExtraKeys = !isWebDesktop &&
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
// Initialize terminal connection
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager

View File

@@ -348,6 +348,12 @@ class InputModel {
final _trackpadAdjustPeerLinux = 0.06;
// This is an experience value.
final _trackpadAdjustMacToWin = 2.50;
// Ignore directional locking for very small deltas on both axes (including
// tiny single-axis movement) to avoid over-filtering near zero.
static const double _trackpadAxisNoiseThreshold = 0.2;
// Lock to dominant axis only when one axis is clearly stronger.
// 1.6 means the dominant axis must be >= 60% larger than the other.
static const double _trackpadAxisLockRatio = 1.6;
int _trackpadSpeed = kDefaultTrackpadSpeed;
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
var _trackpadScrollUnsent = Offset.zero;
@@ -365,6 +371,16 @@ class InputModel {
final isPhysicalMouse = false.obs;
int _lastButtons = 0;
Offset lastMousePos = Offset.zero;
int _lastWheelTsUs = 0;
// Wheel acceleration thresholds.
static const int _wheelAccelFastThresholdUs = 40000; // 40ms
static const int _wheelAccelMediumThresholdUs = 80000; // 80ms
static const double _wheelBurstVelocityThreshold =
0.002; // delta units per microsecond
// Wheel burst acceleration (empirical tuning).
// Applies only to fast, non-smooth bursts to preserve single-step scrolling.
// Flutter uses microseconds for dt, so velocity is in delta/us.
// Relative mouse mode (for games/3D apps).
final relativeMouseMode = false.obs;
@@ -964,6 +980,7 @@ class InputModel {
toReleaseRawKeys.release(handleRawKeyEvent);
_pointerMovedAfterEnter = false;
_pointerInsideImage = enter;
_lastWheelTsUs = 0;
// Fix status
if (!enter) {
@@ -1161,6 +1178,7 @@ class InputModel {
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
delta *= _trackpadAdjustMacToWin;
}
delta = _filterTrackpadDeltaAxis(delta);
_trackpadLastDelta = delta;
var x = delta.dx.toInt();
@@ -1193,6 +1211,24 @@ class InputModel {
}
}
Offset _filterTrackpadDeltaAxis(Offset delta) {
final absDx = delta.dx.abs();
final absDy = delta.dy.abs();
// Keep diagonal intent when movement is tiny on both axes.
if (absDx < _trackpadAxisNoiseThreshold &&
absDy < _trackpadAxisNoiseThreshold) {
return delta;
}
// Dominant-axis lock to reduce accidental cross-axis scrolling noise.
if (absDy >= absDx * _trackpadAxisLockRatio) {
return Offset(0, delta.dy);
}
if (absDx >= absDy * _trackpadAxisLockRatio) {
return Offset(delta.dx, 0);
}
return delta;
}
void _scheduleFling(double x, double y, int delay) {
if (isViewCamera) return;
if ((x == 0 && y == 0) || _stopFling) {
@@ -1407,17 +1443,44 @@ class InputModel {
if (isViewOnly) return;
if (isViewCamera) return;
if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt();
var dy = e.scrollDelta.dy.toInt();
final rawDx = e.scrollDelta.dx;
final rawDy = e.scrollDelta.dy;
final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs();
final isSmooth = dominantDelta < 1;
final nowUs = DateTime.now().microsecondsSinceEpoch;
final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs;
_lastWheelTsUs = nowUs;
int accel = 1;
if (!isSmooth &&
dtUs > 0 &&
dtUs <= _wheelAccelMediumThresholdUs &&
(isWindows || isLinux) &&
peerPlatform == kPeerPlatformMacOS) {
final velocity = dominantDelta / dtUs;
if (velocity >= _wheelBurstVelocityThreshold) {
if (dtUs < _wheelAccelFastThresholdUs) {
accel = 3;
} else {
accel = 2;
}
}
}
var dx = rawDx.toInt();
var dy = rawDy.toInt();
if (rawDx.abs() > rawDy.abs()) {
dy = 0;
} else {
dx = 0;
}
if (dx > 0) {
dx = -1;
dx = -accel;
} else if (dx < 0) {
dx = 1;
dx = accel;
}
if (dy > 0) {
dy = -1;
dy = -accel;
} else if (dy < 0) {
dy = 1;
dy = accel;
}
bind.sessionSendMouse(
sessionId: sessionId,

View File

@@ -820,6 +820,7 @@ class Client {
bool isTerminal = false;
String portForward = "";
String name = "";
String avatar = "";
String peerId = ""; // peer user's id,show at app
bool keyboard = false;
bool clipboard = false;
@@ -847,6 +848,7 @@ class Client {
isTerminal = json['is_terminal'] ?? false;
portForward = json['port_forward'];
name = json['name'];
avatar = json['avatar'] ?? '';
peerId = json['peer_id'];
keyboard = json['keyboard'];
clipboard = json['clipboard'];
@@ -870,6 +872,7 @@ class Client {
data['is_terminal'] = isTerminal;
data['port_forward'] = portForward;
data['name'] = name;
data['avatar'] = avatar;
data['peer_id'] = peerId;
data['keyboard'] = keyboard;
data['clipboard'] = clipboard;

View File

@@ -24,6 +24,13 @@ class TerminalModel with ChangeNotifier {
bool _disposed = false;
final _inputBuffer = <String>[];
// Buffer for output data received before terminal view has valid dimensions.
// This prevents NaN errors when writing to terminal before layout is complete.
final _pendingOutputChunks = <String>[];
int _pendingOutputSize = 0;
static const int _kMaxOutputBufferChars = 8 * 1024;
// View ready state: true when terminal has valid dimensions, safe to write
bool _terminalViewReady = false;
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
@@ -74,6 +81,12 @@ class TerminalModel with ChangeNotifier {
// This piece of code must be placed before the conditional check in order to initialize properly.
onResizeExternal?.call(w, h, pw, ph);
// Mark terminal view as ready and flush any buffered output on first valid resize.
// Must be after onResizeExternal so the view layer has valid dimensions before flushing.
if (!_terminalViewReady) {
_markViewReady();
}
if (_terminalOpened) {
// Notify remote terminal of resize
try {
@@ -141,7 +154,7 @@ class TerminalModel with ChangeNotifier {
debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e');
// Optionally show error to user
if (e is TimeoutException) {
terminal.write('Failed to open terminal: Connection timeout\r\n');
_writeToTerminal('Failed to open terminal: Connection timeout\r\n');
}
}
}
@@ -253,8 +266,8 @@ class TerminalModel with ChangeNotifier {
void _handleTerminalOpened(Map<String, dynamic> evt) {
final bool success = getSuccessFromEvt(evt);
final String message = evt['message'] ?? '';
final String? serviceId = evt['service_id'];
final String message = evt['message']?.toString() ?? '';
final String? serviceId = evt['service_id']?.toString();
debugPrint(
'[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId');
@@ -262,7 +275,18 @@ class TerminalModel with ChangeNotifier {
if (success) {
_terminalOpened = true;
// Service ID is now saved on the Rust side in handle_terminal_response
// On reconnect ("Reconnected to existing terminal"), server may replay recent output.
// If this TerminalView instance is reused (not rebuilt), duplicate lines can appear.
// We intentionally accept this tradeoff for now to keep logic simple.
// Fallback: if terminal view is not yet ready but already has valid
// dimensions (e.g. layout completed before open response arrived),
// mark view ready now to avoid output stuck in buffer indefinitely.
if (!_terminalViewReady &&
terminal.viewWidth > 0 &&
terminal.viewHeight > 0) {
_markViewReady();
}
// Process any buffered input
_processBufferedInputAsync().then((_) {
@@ -283,7 +307,7 @@ class TerminalModel with ChangeNotifier {
}));
}
} else {
terminal.write('Failed to open terminal: $message\r\n');
_writeToTerminal('Failed to open terminal: $message\r\n');
}
}
@@ -327,29 +351,82 @@ class TerminalModel with ChangeNotifier {
return;
}
terminal.write(text);
_writeToTerminal(text);
} catch (e) {
debugPrint('[TerminalModel] Failed to process terminal data: $e');
}
}
}
/// Write text to terminal, buffering if the view is not yet ready.
/// All terminal output should go through this method to avoid NaN errors
/// from writing before the terminal view has valid layout dimensions.
void _writeToTerminal(String text) {
if (!_terminalViewReady) {
// If a single chunk exceeds the cap, keep only its tail.
// Note: truncation may split a multi-byte ANSI escape sequence,
// which can cause a brief visual glitch on flush. This is acceptable
// because it only affects the pre-layout buffering window and the
// terminal will self-correct on subsequent output.
if (text.length >= _kMaxOutputBufferChars) {
final truncated = text.substring(text.length - _kMaxOutputBufferChars);
_pendingOutputChunks
..clear()
..add(truncated);
_pendingOutputSize = truncated.length;
} else {
_pendingOutputChunks.add(text);
_pendingOutputSize += text.length;
// Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences)
while (_pendingOutputSize > _kMaxOutputBufferChars &&
_pendingOutputChunks.length > 1) {
final removed = _pendingOutputChunks.removeAt(0);
_pendingOutputSize -= removed.length;
}
}
return;
}
terminal.write(text);
}
void _flushOutputBuffer() {
if (_pendingOutputChunks.isEmpty) return;
debugPrint(
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
for (final chunk in _pendingOutputChunks) {
terminal.write(chunk);
}
_pendingOutputChunks.clear();
_pendingOutputSize = 0;
}
/// Mark terminal view as ready and flush buffered output.
void _markViewReady() {
if (_terminalViewReady) return;
_terminalViewReady = true;
_flushOutputBuffer();
}
void _handleTerminalClosed(Map<String, dynamic> evt) {
final int exitCode = evt['exit_code'] ?? 0;
terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n');
_writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n');
_terminalOpened = false;
notifyListeners();
}
void _handleTerminalError(Map<String, dynamic> evt) {
final String message = evt['message'] ?? 'Unknown error';
terminal.write('\r\nTerminal error: $message\r\n');
_writeToTerminal('\r\nTerminal error: $message\r\n');
}
@override
void dispose() {
if (_disposed) return;
_disposed = true;
// Clear buffers to free memory
_inputBuffer.clear();
_pendingOutputChunks.clear();
_pendingOutputSize = 0;
// Terminal cleanup is handled server-side when service closes
super.dispose();
}

View File

@@ -17,6 +17,7 @@ bool refreshingUser = false;
class UserModel {
final RxString userName = ''.obs;
final RxString displayName = ''.obs;
final RxString avatar = ''.obs;
final RxBool isAdmin = false.obs;
final RxString networkError = ''.obs;
bool get isLogin => userName.isNotEmpty;
@@ -33,6 +34,7 @@ class UserModel {
}
return '$preferred (@$username)';
}
WeakReference<FFI> parent;
UserModel(this.parent) {
@@ -114,6 +116,7 @@ class UserModel {
if (userInfo != null) {
userName.value = (userInfo['name'] ?? '').toString();
displayName.value = (userInfo['display_name'] ?? '').toString();
avatar.value = (userInfo['avatar'] ?? '').toString();
}
}
@@ -126,11 +129,13 @@ class UserModel {
}
userName.value = '';
displayName.value = '';
avatar.value = '';
}
_parseAndUpdateUser(UserPayload user) {
userName.value = user.name;
displayName.value = user.displayName;
avatar.value = user.avatar;
isAdmin.value = user.isAdmin;
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
if (isWeb) {

View File

@@ -2034,5 +2034,9 @@ class RustdeskImpl {
return false;
}
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
}
void dispose() {}
}

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
version: 1.4.5+63
version: 1.4.6+64
environment:
sdk: '^3.1.0'

View File

@@ -7,6 +7,7 @@
#include <cstdlib> // for getenv and _putenv
#include <cstring> // for strcmp
#include <string> // for std::wstring
namespace {
@@ -15,6 +16,43 @@ constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
// Static variable to hold the custom icon (needs cleanup on exit)
static HICON g_custom_icon_ = nullptr;
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists.
// Returns nullptr if the file doesn't exist or can't be loaded.
HICON LoadCustomIcon() {
if (g_custom_icon_ != nullptr) {
return g_custom_icon_;
}
wchar_t exe_path[MAX_PATH];
if (!GetModuleFileNameW(nullptr, exe_path, MAX_PATH)) {
return nullptr;
}
std::wstring icon_path = exe_path;
size_t last_slash = icon_path.find_last_of(L"\\/");
if (last_slash == std::wstring::npos) {
return nullptr;
}
icon_path = icon_path.substr(0, last_slash + 1);
icon_path += L"data\\flutter_assets\\assets\\icon.ico";
// Check file attributes - reject if missing, directory, or reparse point (symlink/junction)
DWORD file_attr = GetFileAttributesW(icon_path.c_str());
if (file_attr == INVALID_FILE_ATTRIBUTES ||
(file_attr & FILE_ATTRIBUTE_DIRECTORY) ||
(file_attr & FILE_ATTRIBUTE_REPARSE_POINT)) {
return nullptr;
}
g_custom_icon_ = (HICON)LoadImageW(
nullptr, icon_path.c_str(), IMAGE_ICON, 0, 0,
LR_LOADFROMFILE | LR_DEFAULTSIZE);
return g_custom_icon_;
}
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
@@ -81,8 +119,16 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists
HICON custom_icon = LoadCustomIcon();
if (custom_icon != nullptr) {
window_class.hIcon = custom_icon;
} else {
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
}
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
@@ -95,6 +141,12 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
// Clean up the custom icon if it was loaded
if (g_custom_icon_ != nullptr) {
DestroyIcon(g_custom_icon_);
g_custom_icon_ = nullptr;
}
}
Win32Window::Win32Window() {

View File

@@ -269,7 +269,7 @@ impl KeyboardControllable for Enigo {
for pos in 0..mod_len {
let rpos = mod_len - 1 - pos;
if flag & (0x0001 << rpos) != 0 {
self.key_up(modifiers[pos]);
self.key_up(modifiers[rpos]);
}
}
@@ -298,7 +298,18 @@ impl KeyboardControllable for Enigo {
}
fn key_up(&mut self, key: Key) {
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
match key {
Key::Layout(c) => {
let code = self.get_layoutdependent_keycode(c);
if code as u16 != 0xFFFF {
let vk = code & 0x00FF;
keybd_event(KEYEVENTF_KEYUP, vk, 0);
}
}
_ => {
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
}
}
}
fn get_key_state(&mut self, key: Key) -> bool {

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.4.5"
version = "1.4.6"
edition = "2021"
description = "RustDesk Remote Desktop"

View File

@@ -6,15 +6,13 @@ if [ "$1" = configure ]; then
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; then
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
fi
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
parsedVersion=$(echo "${version//./}")
mkdir -p /usr/lib/systemd/system/
mkdir -p /usr/lib/systemd/system/
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
# try fix error in Ubuntu 18.04
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.

View File

@@ -1,5 +1,5 @@
pkgname=rustdesk
pkgver=1.4.5
pkgver=1.4.6
pkgrel=0
epoch=
pkgdesc=""

View File

@@ -31,22 +31,168 @@ LExit:
return WcaFinalize(er);
}
// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail.
// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied.
// `msiexec /i package.msi /qn`
// Helper function to safely delete a file or directory using handle-based deletion.
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
BOOL SafeDeleteItem(LPCWSTR fullPath)
{
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
// to prevent following symlinks.
// Use shared access to allow deletion even when other processes have the file open.
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
HANDLE hFile = CreateFileW(
fullPath,
DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
NULL,
OPEN_EXISTING,
flags,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to open '%ls'. Error: %lu", fullPath, GetLastError());
return FALSE;
}
// Use SetFileInformationByHandle to mark for deletion.
// The file will be deleted when the handle is closed.
FILE_DISPOSITION_INFO dispInfo;
dispInfo.DeleteFile = TRUE;
BOOL result = SetFileInformationByHandle(
hFile,
FileDispositionInfo,
&dispInfo,
sizeof(dispInfo)
);
if (!result)
{
DWORD error = GetLastError();
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to mark '%ls' for deletion. Error: %lu", fullPath, error);
}
CloseHandle(hFile);
return result;
}
// Helper function to recursively delete a directory's contents with detailed logging.
void RecursiveDelete(LPCWSTR path)
{
// Ensure the path is not empty or null.
if (path == NULL || path[0] == L'\0')
{
return;
}
// Extra safety: never operate directly on a root path.
if (PathIsRootW(path))
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
return;
}
// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR searchPath[MAX_PATH];
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
return;
}
WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileW(searchPath, &findData);
if (hFind == INVALID_HANDLE_VALUE)
{
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
return;
}
do
{
// Skip '.' and '..' directories.
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
{
continue;
}
// MAX_PATH is enough here since the installer should not be using longer paths.
// No need to handle extended-length paths (\\?\) in this context.
WCHAR fullPath[MAX_PATH];
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
continue;
}
// Before acting, ensure the read-only attribute is not set.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
{
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
}
}
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
// Do not follow reparse points, only remove the link itself.
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
SafeDeleteItem(fullPath);
}
else
{
// Recursively delete directory contents first
RecursiveDelete(fullPath);
// Then delete the directory itself
SafeDeleteItem(fullPath);
}
}
else
{
// Delete file using safe handle-based deletion
SafeDeleteItem(fullPath);
}
} while (FindNextFileW(hFind, &findData) != 0);
DWORD lastError = GetLastError();
if (lastError != ERROR_NO_MORE_FILES)
{
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
}
FindClose(hFind);
}
// See `Package.wxs` for the sequence of this custom action.
//
// So we need to delete the files separately in install folder.
// Upgrade/uninstall sequence:
// 1. InstallInitialize
// 2. RemoveExistingProducts
// ├─ TerminateProcesses
// ├─ TryStopDeleteService
// ├─ RemoveInstallFolder - <-- Here
// └─ RemoveFiles
// 3. InstallValidate
// 4. InstallFiles
// 5. InstallExecute
// 6. InstallFinalize
UINT __stdcall RemoveInstallFolder(
__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
DWORD er = ERROR_SUCCESS;
int nResult = 0;
LPWSTR installFolder = NULL;
LPWSTR pwz = NULL;
LPWSTR pwzData = NULL;
WCHAR runtimeBroker[1024] = { 0, };
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
ExitOnFailure(hr, "Failed to initialize");
@@ -58,24 +204,23 @@ UINT __stdcall RemoveInstallFolder(
hr = WcaReadStringFromCaData(&pwz, &installFolder);
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
StringCchPrintfW(runtimeBroker, sizeof(runtimeBroker) / sizeof(runtimeBroker[0]), L"%ls\\RuntimeBroker_rustdesk.exe", installFolder);
SHFILEOPSTRUCTW fileOp;
ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT));
fileOp.wFunc = FO_DELETE;
fileOp.pFrom = runtimeBroker;
fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT;
nResult = SHFileOperationW(&fileOp);
if (nResult == 0)
{
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has been deleted.", runtimeBroker);
if (installFolder == NULL || installFolder[0] == L'\0') {
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
goto LExit;
}
else
{
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", runtimeBroker, nResult);
if (PathIsRootW(installFolder)) {
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
goto LExit;
}
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
RecursiveDelete(installFolder);
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
LExit:
ReleaseStr(pwzData);
@@ -109,9 +254,12 @@ bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInfo
{
if (pebUpp.CommandLine.Length > 0)
{
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length);
// Allocate extra space for null terminator
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length + sizeof(WCHAR));
if (commandLine != NULL)
{
// Initialize all bytes to zero for safety
memset(commandLine, 0, pebUpp.CommandLine.Length + sizeof(WCHAR));
if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer,
commandLine, pebUpp.CommandLine.Length, &dwBytesRead))
{

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.5
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.5
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.5
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -33,7 +33,7 @@ use crate::{
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
kcp_stream::KcpStream,
secure_tcp,
ui_interface::{get_builtin_option, use_texture_render},
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
ui_session_interface::{InvokeUiSession, Session},
};
#[cfg(feature = "unix-file-copy-paste")]
@@ -2625,6 +2625,20 @@ impl LoginConfigHandler {
} else {
(my_id, self.id.clone())
};
let mut avatar = get_builtin_option(keys::OPTION_AVATAR);
if avatar.is_empty() {
avatar = serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option(
"user_info",
))
.ok()
.and_then(|x| {
x.get("avatar")
.and_then(|x| x.as_str())
.map(|x| x.trim().to_owned())
})
.unwrap_or_default();
}
avatar = resolve_avatar_url(avatar);
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
if display_name.is_empty() {
display_name =
@@ -2684,6 +2698,7 @@ impl LoginConfigHandler {
})
.into(),
hwid,
avatar,
..Default::default()
};
match self.conn_type {

View File

@@ -39,7 +39,7 @@ use hbb_common::{
use crate::{
hbbs_http::{create_http_client_async, get_url_for_tls},
ui_interface::{get_option, set_option},
ui_interface::{get_option, is_installed, set_option},
};
#[derive(Debug, Eq, PartialEq)]

View File

@@ -187,7 +187,10 @@ pub fn core_main() -> Option<Vec<String>> {
}
#[cfg(windows)]
hbb_common::config::PeerConfig::preload_peers();
{
crate::platform::try_remove_temp_update_files();
hbb_common::config::PeerConfig::preload_peers();
}
std::thread::spawn(move || crate::start_server(false, no_server));
} else {
#[cfg(windows)]
@@ -202,17 +205,24 @@ pub fn core_main() -> Option<Vec<String>> {
if config::is_disable_installation() {
return None;
}
let res = platform::update_me(false);
let text = match res {
Ok(_) => translate("Update successfully!".to_string()),
Err(err) => {
log::error!("Failed with error: {err}");
translate("Update failed!".to_string())
let text = match crate::platform::prepare_custom_client_update() {
Err(e) => {
log::error!("Error preparing custom client update: {}", e);
"Update failed!".to_string()
}
Ok(false) => "Update failed!".to_string(),
Ok(true) => match platform::update_me(false) {
Ok(_) => "Update successfully!".to_string(),
Err(err) => {
log::error!("Failed with error: {err}");
"Update failed!".to_string()
}
},
};
Toast::new(Toast::POWERSHELL_APP_ID)
.title(&config::APP_NAME.read().unwrap())
.text1(&text)
.text1(&translate(text))
.sound(Some(Sound::Default))
.duration(Duration::Short)
.show()

View File

@@ -1101,6 +1101,10 @@ pub fn main_get_api_server() -> String {
get_api_server()
}
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
SyncReturn(resolve_avatar_url(avatar))
}
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
http_request(url, method, body, header)
}
@@ -2776,10 +2780,13 @@ pub fn main_get_common(key: String) -> String {
} else if key.starts_with("download-file-") {
let _version = key.replace("download-file-", "");
#[cfg(target_os = "windows")]
return match crate::platform::windows::is_msi_installed() {
Ok(true) => format!("rustdesk-{_version}-x86_64.msi"),
Ok(false) => format!("rustdesk-{_version}-x86_64.exe"),
Err(e) => {
return match (
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"),
(Err(e), _) => {
log::error!("Failed to check if is msi: {}", e);
format!("error:update-failed-check-msi-tip")
}
@@ -2876,30 +2883,17 @@ pub fn main_set_common(_key: String, _value: String) {
if let Some(f) = new_version_file.to_str() {
// 1.4.0 does not support "--update"
// But we can assume that the new version supports it.
#[cfg(target_os = "windows")]
if f.ends_with(".exe") {
if let Err(e) =
crate::platform::run_exe_in_cur_session(f, vec!["--update"], false)
{
log::error!("Failed to run the update exe: {}", e);
}
} else if f.ends_with(".msi") {
if let Err(e) = crate::platform::update_me_msi(f, false) {
log::error!("Failed to run the update msi: {}", e);
}
} else {
// unreachable!()
}
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "windows", target_os = "macos"))]
match crate::platform::update_to(f) {
Ok(_) => {
log::info!("Update successfully!");
log::info!("Update process is launched successfully!");
}
Err(e) => {
log::error!("Failed to update to new version, {}", e);
fs::remove_file(f).ok();
}
}
fs::remove_file(f).ok();
}
}
} else if _key == "extract-update-dmg" {

View File

@@ -17,6 +17,7 @@ lazy_static::lazy_static! {
const QUERY_INTERVAL_SECS: f32 = 1.0;
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
@@ -82,6 +83,8 @@ pub struct UserPayload {
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub avatar: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub note: Option<String>,
@@ -273,6 +276,7 @@ impl OidcSession {
serde_json::json!({
"name": auth_body.user.name,
"display_name": auth_body.user.display_name,
"avatar": auth_body.user.avatar,
"status": auth_body.user.status
})
.to_string(),

View File

@@ -53,8 +53,25 @@ pub fn download_file(
auto_del_dur: Option<Duration>,
) -> ResultType<String> {
let id = url.clone();
if DOWNLOADERS.lock().unwrap().contains_key(&id) {
return Ok(id);
// First pass: if a non-error downloader exists for this URL, reuse it.
// If an errored downloader exists, remove it so this call can retry.
let mut stale_path = None;
{
let mut downloaders = DOWNLOADERS.lock().unwrap();
if let Some(downloader) = downloaders.get(&id) {
if downloader.error.is_none() {
return Ok(id);
}
stale_path = downloader.path.clone();
downloaders.remove(&id);
}
}
if let Some(p) = stale_path {
if p.exists() {
if let Err(e) = std::fs::remove_file(&p) {
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
}
}
}
if let Some(path) = path.as_ref() {
@@ -75,8 +92,26 @@ pub fn download_file(
tx_cancel: tx,
finished: false,
};
let mut downloaders = DOWNLOADERS.lock().unwrap();
downloaders.insert(id.clone(), downloader);
// Second pass (atomic with insert) to avoid race with another concurrent caller.
let mut stale_path_after_check = None;
{
let mut downloaders = DOWNLOADERS.lock().unwrap();
if let Some(existing) = downloaders.get(&id) {
if existing.error.is_none() {
return Ok(id);
}
stale_path_after_check = existing.path.clone();
downloaders.remove(&id);
}
downloaders.insert(id.clone(), downloader);
}
if let Some(p) = stale_path_after_check {
if p.exists() {
if let Err(e) = std::fs::remove_file(&p) {
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
}
}
}
let id2 = id.clone();
std::thread::spawn(

View File

@@ -226,6 +226,7 @@ pub enum Data {
is_terminal: bool,
peer_id: String,
name: String,
avatar: String,
authorized: bool,
port_forward: String,
keyboard: bool,
@@ -1583,6 +1584,6 @@ mod test {
#[test]
fn verify_ffi_enum_data_size() {
println!("{}", std::mem::size_of::<Data>());
assert!(std::mem::size_of::<Data>() <= 96);
assert!(std::mem::size_of::<Data>() <= 120);
}
}

View File

@@ -736,8 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"),
("Changelog", "更新日志"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "传出会话期间保持屏幕常亮"),
("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"),
("Continue with {}", "使用 {} 登录"),
("Display Name", "显示名称"),
].iter().cloned().collect();

View File

@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
("Continue with {}", "Fortfahren mit {}"),
("Display Name", ""),
("Display Name", "Anzeigename"),
].iter().cloned().collect();
}

View File

@@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Κατάσταση"),
("Your Desktop", "Ο σταθμός εργασίας σας"),
("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."),
("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το ID και τον κωδικό πρόσβασης."),
("Password", "Κωδικός πρόσβασης"),
("Ready", "Έτοιμο"),
("Established", "Συνδέθηκε"),
@@ -19,16 +19,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recent sessions", "Πρόσφατες συνεδρίες"),
("Address book", "Βιβλίο διευθύνσεων"),
("Confirmation", "Επιβεβαίωση"),
("TCP tunneling", "TCP tunneling"),
("TCP tunneling", "Σήραγγα TCP"),
("Remove", "Κατάργηση"),
("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"),
("Refresh random password", "Ανανέωση τυχαίου κωδικού πρόσβασης"),
("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"),
("Enable keyboard/mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"),
("Enable clipboard", "Ενεργοποίηση προχείρου"),
("Enable file transfer", "Ενεργοποίηση μεταφοράς αρχείων"),
("Enable TCP tunneling", "Ενεργοποίηση TCP tunneling"),
("Enable TCP tunneling", "Ενεργοποίηση σήραγγας TCP"),
("IP Whitelisting", "Λίστα επιτρεπόμενων IP"),
("ID/Relay Server", "Διακομιστής ID/Αναμετάδοσης"),
("ID/Relay Server", "ID/Διακομιστής Αναμετάδοσης"),
("Import server config", "Εισαγωγή διαμόρφωσης διακομιστή"),
("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"),
("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"),
@@ -36,14 +36,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"),
("Clipboard is empty", "Το πρόχειρο είναι κενό"),
("Stop service", "Διακοπή υπηρεσίας"),
("Change ID", "Αλλαγή αναγνωριστικού ID"),
("Change ID", "Αλλαγή του ID σας"),
("Your new ID", "Το νέο σας ID"),
("length %min% to %max%", "μέγεθος από %min% έως %max%"),
("starts with a letter", "ξεκινά με γράμμα"),
("allowed characters", "επιτρεπόμενοι χαρακτήρες"),
("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9, - (παύλα) και _ (κάτω παύλα). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."),
("Website", "Ιστότοπος"),
("About", "Πληροφορίες"),
("About", "Σχετικά"),
("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"),
("Privacy Statement", "Πολιτική απορρήτου"),
("Mute", "Σίγαση"),
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input", "Είσοδος ήχου"),
("Enhancements", "Βελτιώσεις"),
("Hardware Codec", "Κωδικοποιητής υλικού"),
("Adaptive bitrate", "Adaptive bitrate"),
("Adaptive bitrate", "Προσαρμοστικός ρυθμός μετάδοσης bit"),
("ID Server", "Διακομιστής ID"),
("Relay Server", "Διακομιστής αναμετάδοσης"),
("API Server", "Διακομιστής API"),
@@ -67,18 +67,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Skip", "Παράλειψη"),
("Close", "Κλείσιμο"),
("Retry", "Δοκίμασε ξανά"),
("OK", "ΟΚ"),
("OK", "Εντάξει"),
("Password Required", "Απαιτείται κωδικός πρόσβασης"),
("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"),
("Remember password", "Απομνημόνευση κωδικού πρόσβασης"),
("Wrong Password", "Λάθος κωδικός πρόσβασης"),
("Do you want to enter again?", "Επανασύνδεση;"),
("Do you want to enter again?", "Θέλετε να γίνει επανασύνδεση;"),
("Connection Error", "Σφάλμα σύνδεσης"),
("Error", "Σφάλμα"),
("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"),
("Connecting...", "Σύνδεση..."),
("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."),
("Please try 1 minute later", "Παρακαλώ ξαναδοκιμάστε σε 1 λεπτό"),
("Please try 1 minute later", "Παρακαλώ δοκιμάστε ξανά σε 1 λεπτό"),
("Login Error", "Σφάλμα εισόδου"),
("Successful", "Επιτυχής"),
("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."),
@@ -101,10 +101,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Select All", "Επιλογή όλων"),
("Unselect All", "Κατάργηση επιλογής όλων"),
("Empty Directory", "Κενός φάκελος"),
("Not an empty directory", "Ο φάκελος δεν είναι κενός"),
("Not an empty directory", "Η διαδρομή δεν είναι κενή"),
("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"),
("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κενό φάκελο;"),
("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτού του φακέλου;"),
("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την κενή διαδρομή;"),
("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτής της διαδρομής;"),
("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"),
("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"),
("Deleting", "Διαγραφή"),
@@ -133,8 +133,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"),
("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"),
("Refresh", "Ανανέωση"),
("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"),
("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"),
("ID does not exist", "Το ID αυτό δεν υπάρχει"),
("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με τον διακομιστή"),
("Please try later", "Παρακαλώ δοκιμάστε αργότερα"),
("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"),
("Key mismatch", "Μη έγκυρο κλειδί"),
@@ -146,17 +146,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Set Password", "Ορίστε κωδικό πρόσβασης"),
("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"),
("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"),
("Click to upgrade", "Αναβάθμιση τώρα"),
("Click to upgrade", "Κάντε κλίκ για αναβάθμιση τώρα"),
("Configure", "Διαμόρφωση"),
("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."),
("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."),
("config_acc", "Για να ελέγξετε την επιφάνεια εργασίας σας από απόσταση, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Προσβασιμότητας\"."),
("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στην επιφάνεια εργασίας σας, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή οθόνης\"."),
("Installing ...", "Γίνεται εγκατάσταση ..."),
("Install", "Εγκατάσταση"),
("Installation", "Η εγκατάσταση"),
("Installation Path", "Διαδρομή εγκατάστασης"),
("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"),
("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"),
("agreement_tip", "Με την εγκατάσταση αποδέχεστε την άδεια χρήσης"),
("agreement_tip", "Με την εγκατάσταση, αποδέχεστε την άδεια χρήσης"),
("Accept and Install", "Αποδοχή και εγκατάσταση"),
("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"),
("Generating ...", "Δημιουργία ..."),
@@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Local Port", "Τοπική θύρα"),
("Local Address", "Τοπική διεύθυνση"),
("Change Local Port", "Αλλαγή τοπικής θύρας"),
("setup_server_tip", "Για πιο γρήγορη σύνδεση, ρυθμίστε τον δικό σας διακομιστή σύνδεσης"),
("Too short, at least 6 characters.", "Πολύ μικρό, τουλάχιστον 6 χαρακτήρες."),
("setup_server_tip", "Για πιο γρήγορη σύνδεση, παρακαλούμε να ρυθμίστε τον δικό σας διακομιστή σύνδεσης"),
("Too short, at least 6 characters.", "Πολύ μικρό, χρειάζεται τουλάχιστον 6 χαρακτήρες."),
("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."),
("Permissions", "Άδειες"),
("Accept", "Αποδοχή"),
@@ -183,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"),
("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"),
("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"),
("Enter Remote ID", "Εισαγωγή απομακρυσμένου ID"),
("Enter Remote ID", "Εισαγωγή του απομακρυσμένου ID"),
("Enter your password", "Εισάγετε τον κωδικό σας"),
("Logging in...", "Γίνεται σύνδεση..."),
("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"),
@@ -200,35 +200,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"),
("Reboot required", "Απαιτείται επανεκκίνηση"),
("Unsupported display server", "Μη υποστηριζόμενος διακομιστής εμφάνισης "),
("x11 expected", "απαιτείται X11"),
("x11 expected", "αναμένεται X11"),
("Port", "Θύρα"),
("Settings", "Ρυθμίσεις"),
("Username", "Όνομα χρήστη"),
("Invalid port", "Μη έγκυρη θύρα"),
("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"),
("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"),
("Closed manually by the peer", "Τερματίστηκε από τον απομακρυσμένο σταθμό"),
("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης διαμόρφωσης"),
("Run without install", "Εκτέλεση χωρίς εγκατάσταση"),
("Connect via relay", "Πραγματοποίηση σύνδεση μέσω αναμεταδότη"),
("Always connect via relay", "Σύνδεση πάντα μέσω αναμεταδότη"),
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"),
("Connect via relay", "Σύνδεση μέσω αναμεταδότη"),
("Always connect via relay", "Να γίνεται σύνδεση πάντα μέσω αναμεταδότη"),
("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων να έχουν πρόσβαση σε εμένα"),
("Login", "Σύνδεση"),
("Verify", "Επαλήθευση"),
("Remember me", "Να με θυμάσαι"),
("Trust this device", "Εμπιστεύομαι αυτή την συσκευή"),
("Trust this device", "Να εμπιστεύομαι αυτή την συσκευή"),
("Verification code", "Κωδικός επαλήθευσης"),
("verification_tip", "Εντοπίστηκε νέα συσκευή και εστάλη ένας κωδικός επαλήθευσης στην καταχωρισμένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνδεθείτε ξανά."),
("verification_tip", "Ένας κωδικός επαλήθευσης έχει σταλεί στην καταχωρημένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνεχίσετε τη σύνδεση."),
("Logout", "Αποσύνδεση"),
("Tags", "Ετικέτες"),
("Search ID", "Αναζήτηση ID"),
("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"),
("Add ID", "Προσθήκη αναγνωριστικού ID"),
("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, κενό ή νέα γραμμή"),
("Add ID", "Προσθήκη ID"),
("Add Tag", "Προσθήκη ετικέτας"),
("Unselect all tags", "Κατάργηση επιλογής όλων των ετικετών"),
("Unselect all tags", "Αποεπιλογή όλων των ετικετών"),
("Network error", "Σφάλμα δικτύου"),
("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"),
("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"),
("Wrong credentials", "Λάθος διαπιστευτήρια"),
("The verification code is incorrect or has expired", ""),
("The verification code is incorrect or has expired", "Ο κωδικός επαλήθευσης είναι λανθασμένος ή έχει λήξει"),
("Edit Tag", "Επεξεργασία ετικέτας"),
("Forget Password", "Διαγραφή απομνημονευμένου κωδικού"),
("Favorites", "Αγαπημένα"),
@@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Socks5 Proxy", "Διαμεσολαβητής Socks5"),
("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"),
("Discovered", "Ανακαλύφθηκαν"),
("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"),
("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος."),
("Remote ID", "Απομακρυσμένο ID"),
("Paste", "Επικόλληση"),
("Paste here?", "Επικόλληση εδώ;"),
@@ -262,28 +262,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Pinch to Zoom", "Τσίμπημα για ζουμ"),
("Canvas Zoom", "Ζουμ σε καμβά"),
("Reset canvas", "Επαναφορά καμβά"),
("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"),
("No permission of file transfer", "Δεν υπάρχει άδεια για την μεταφορά αρχείων"),
("Note", "Σημείωση"),
("Connection", "Σύνδεση"),
("Share screen", "Κοινή χρήση οθόνης"),
("Chat", "Κουβέντα"),
("Total", "Σύνολο"),
("items", "στοιχεία"),
("Selected", "Επιλεγμένο"),
("Screen Capture", "Αποτύπωση οθόνης"),
("Selected", "Επιλεγμένα"),
("Screen Capture", "Καταγραφή οθόνης"),
("Input Control", "Έλεγχος εισόδου"),
("Audio Capture", "Εγγραφή ήχου"),
("Do you accept?", "Δέχεσαι;"),
("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"),
("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"),
("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισόδου για Android;"),
("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."),
("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."),
("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."),
("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."),
("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."),
("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."),
("android_start_service_tip", ""),
("android_permission_may_not_change_tip", ""),
("android_input_permission_tip2", "Παρακαλούμε να μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."),
("android_new_connection_tip", "Έχει ληφθεί νέο αίτημα ελέγχου, το οποίο θέλει να ελέγξει την τρέχουσα συσκευή σας."),
("android_service_will_start_tip", "Η ενεργοποίηση της \"Καταγραφής οθόνης\" θα ξεκινήσει αυτόματα την υπηρεσία, επιτρέποντας σε άλλες συσκευές να ζητήσουν σύνδεση με τη συσκευή σας."),
("android_stop_service_tip", "Το κλείσιμο της υπηρεσίας αυτής θα κλείσει αυτόματα όλες τις υπάρχουσες συνδέσεις."),
("android_version_audio_tip", "Η τρέχουσα έκδοση Android δεν υποστηρίζει εγγραφή ήχου, αναβαθμίστε σε Android 10 ή νεότερη έκδοση."),
("android_start_service_tip", "Πατήστε [Έναρξη υπηρεσίας] ή ενεργοποιήστε την άδεια [Καταγραφή οθόνης] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."),
("android_permission_may_not_change_tip", "Τα δικαιώματα για τις καθιερωμένες συνδέσεις δεν μπορούν να αλλάξουν άμεσα μέχρι να επανασυνδεθούν."),
("Account", "Λογαριασμός"),
("Overwrite", "Αντικατάσταση"),
("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"),
@@ -293,14 +293,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Succeeded", "Επιτυχής"),
("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"),
("Unsupported", "Δεν υποστηρίζεται"),
("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"),
("Peer denied", "Ο απομακρυσμένος σταθμός έχει απορριφθεί"),
("Please install plugins", "Παρακαλώ εγκαταστήστε τα πρόσθετα"),
("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"),
("Failed to turn off", "Αποτυχία απενεργοποίησης"),
("Turned off", "Απενεργοποιημένο"),
("Language", "Γλώσσα"),
("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"),
("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"),
("Keep RustDesk background service", "Διατήρηση της υπηρεσίας παρασκηνίου του RustDesk"),
("Ignore Battery Optimizations", "Αγνόηση βελτιστοποιήσεων μπαταρίας"),
("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"),
("Start on boot", "Έναρξη κατά την εκκίνηση"),
("Start the screen sharing service on boot, requires special permissions", "Η έναρξη της υπηρεσίας κοινής χρήσης οθόνης κατά την εκκίνηση, απαιτεί ειδικά δικαιώματα"),
@@ -315,11 +315,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Restart remote device", "Επανεκκίνηση απομακρυσμένης συσκευής"),
("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"),
("Restarting remote device", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής"),
("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."),
("remote_restarting_tip", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής. Κλείστε αυτό το πλαίσιο μηνύματος και επανασυνδεθείτε με τον μόνιμο κωδικό πρόσβασης μετά από λίγο."),
("Copied", "Αντιγράφηκε"),
("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"),
("Fullscreen", "Πλήρης οθόνη"),
("Mobile Actions", "Mobile Actions"),
("Mobile Actions", "Ενέργειες για κινητά"),
("Select Monitor", "Επιλογή οθόνης"),
("Control Actions", "Ενέργειες ελέγχου"),
("Display Settings", "Ρυθμίσεις οθόνης"),
@@ -347,7 +347,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable audio", "Ενεργοποίηση ήχου"),
("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"),
("Server", "Διακομιστής"),
("Direct IP Access", "Πρόσβαση με χρήση IP"),
("Direct IP Access", "Άμεση πρόσβαση IP"),
("Proxy", "Διαμεσολαβητής"),
("Apply", "Εφαρμογή"),
("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"),
@@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Pin Toolbar", "Καρφίτσωμα γραμμής εργαλείων"),
("Unpin Toolbar", "Ξεκαρφίτσωμα γραμμής εργαλείων"),
("Recording", "Εγγραφή"),
("Directory", "Φάκελος εγγραφών"),
("Directory", "Διαδρομή"),
("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"),
("Automatically record outgoing sessions", "Αυτόματη εγγραφή εξερχόμενων συνεδριών"),
("Change", "Αλλαγή"),
@@ -373,23 +373,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."),
("Disconnected", "Αποσυνδέθηκε"),
("Other", "Άλλα"),
("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"),
("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσουν πολλαπλές καρτέλες"),
("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"),
("Full Access", "Πλήρης πρόσβαση"),
("Screen Share", "Κοινή χρήση οθόνης"),
("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."),
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
("JumpLink", "Προβολή"),
("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."),
("JumpLink", "Σύνδεσμος μετάβασης"),
("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."),
("Show RustDesk", "Εμφάνιση RustDesk"),
("Show RustDesk", "Εμφάνιση του RustDesk"),
("This PC", "Αυτός ο υπολογιστής"),
("or", "ή"),
("Elevate", "Ανύψωση"),
("Zoom cursor", "ρσορας μεγέθυνσης"),
("Zoom cursor", "Δρομέας ζουμ"),
("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"),
("Accept sessions via click", "Αποδοχή συνεδριών με κλικ"),
("Accept sessions via both", "Αποδοχή συνεδριών και με τα δύο"),
("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."),
("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα της συνεδρίας σας..."),
("One-time Password", "Κωδικός μίας χρήσης"),
("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"),
("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"),
@@ -398,27 +398,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"),
("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."),
("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"),
("Skipped", "Παράλειψη"),
("Add to address book", "Προσθήκη στο Βιβλίο Διευθύνσεων"),
("Skipped", "Παραλήφθηκε"),
("Add to address book", "Προσθήκη στο βιβλίο διευθύνσεων"),
("Group", "Ομάδα"),
("Search", "Αναζήτηση"),
("Closed manually by web console", "Κλειστό χειροκίνητα από την κονσόλα web"),
("Closed manually by web console", "Κλείσιμο χειροκίνητα από την κονσόλα ιστού"),
("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"),
("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"),
("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."),
("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"),
("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"),
("config_microphone", "Ρύθμιση μικροφώνου"),
("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"),
("software_render_tip", "Εάν χρησιμοποιείτε κάρτα γραφικών της Nvidia σε Linux και το παράθυρο απομακρυσμένης πρόσβασης κλείνει αμέσως μετά τη σύνδεση, η μετάβαση στο πρόγραμμα οδήγησης της Nouveau ανοιχτού κώδικα και η επιλογή χρήσης απόδοσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση του λογισμικού."),
("Always use software rendering", "Να χρησιμοποιείτε πάντα η απόδοση λογισμικού"),
("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με το πληκτρολόγιο, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Παρακολούθηση εισόδου\"."),
("config_microphone", "Για να μιλήσετε εξ αποστάσεως, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή ήχου\"."),
("request_elevation_tip", "Μπορείτε επίσης να ζητήσετε ανύψωση εάν υπάρχει κάποιος στην απομακρυσμένη πλευρά."),
("Wait", "Περιμένετε"),
("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"),
("Elevation Error", "Σφάλμα ανύψωσης"),
("Ask the remote user for authentication", "Ζητήστε από τον απομακρυσμένο χρήστη έλεγχο ταυτότητας"),
("Choose this if the remote account is administrator", "Επιλέξτε αυτό εάν ο απομακρυσμένος λογαριασμός είναι διαχειριστής"),
("Transmit the username and password of administrator", "Αποστολή του ονόματος χρήστη και του κωδικού πρόσβασης του διαχειριστή"),
("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο OK στο παράθυρο UAC όπου εκτελείται το RustDesk."),
("Request Elevation", "Αίτημα ανύψωσης δικαιωμάτων χρήστη"),
("wait_accept_uac_tip", "Περιμένετε να αποδεχτεί ο απομακρυσμένος χρήστης το παράθυρο διαλόγου UAC."),
("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"),
("Transmit the username and password of administrator", "Μεταδώστε το όνομα χρήστη και τον κωδικό πρόσβασης του διαχειριστή"),
("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο πλήκτρο Εντάξει στο παράθυρο UAC όπου εκτελείται το RustDesk."),
("Request Elevation", "Αίτημα ανύψωσης"),
("wait_accept_uac_tip", "Περιμένετε μέχρι ο απομακρυσμένος χρήστης να αποδεχτεί το παράθυρο διαλόγου UAC."),
("Elevate successfully", "Επιτυχής ανύψωση"),
("uppercase", "κεφαλαία γράμματα"),
("lowercase", "πεζά γράμματα"),
("digit", "αριθμός"),
@@ -427,7 +427,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Weak", "Αδύναμο"),
("Medium", "Μέτριο"),
("Strong", "Δυνατό"),
("Switch Sides", "Εναλλαγή πλευράς"),
("Switch Sides", "Αλλαγή πλευρών"),
("Please confirm if you want to share your desktop?", "Παρακαλώ επιβεβαιώστε αν επιθυμείτε την κοινή χρήση της επιφάνειας εργασίας;"),
("Display", "Εμφάνιση"),
("Default View Style", "Προκαθορισμένος τρόπος εμφάνισης"),
@@ -441,11 +441,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Φωνητική κλήση"),
("Text chat", "Συνομιλία κειμένου"),
("Stop voice call", "Διακοπή φωνητικής κλήσης"),
("relay_hint_tip", "Εάν δεν είναι δυνατή η απευθείας σύνδεση, μπορείτε να δοκιμάσετε να συνδεθείτε μέσω διακομιστή αναμετάδοσης"),
("relay_hint_tip", "Ενδέχεται να μην είναι δυνατή η απευθείας σύνδεση: μπορείτε να δοκιμάσετε να συνδεθείτε μέσω αναμετάδοσης. Επιπλέον, εάν θέλετε να χρησιμοποιήσετε την αναμετάδοση στην πρώτη σας προσπάθεια, μπορείτε να προσθέσετε την \"/r\" κατάληξη στο ID ή να επιλέξετε την επιλογή \"Πάντα σύνδεση μέσω αναμετάδοσης\" στην κάρτα πρόσφατων συνεδριών, εάν υπάρχει."),
("Reconnect", "Επανασύνδεση"),
("Codec", "Κωδικοποίηση"),
("Resolution", "Ανάλυση"),
("No transfers in progress", "Δεν υπάρχει μεταφορά σε εξέλιξη"),
("No transfers in progress", "Δεν υπάρχουν μεταφορές σε εξέλιξη"),
("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"),
("RDP Settings", "Ρυθμίσεις RDP"),
("Sort by", "Ταξινόμηση κατά"),
@@ -454,35 +454,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Minimize", "Ελαχιστοποίηση"),
("Maximize", "Μεγιστοποίηση"),
("Your Device", "Η συσκευή σας"),
("empty_recent_tip", "Δεν υπάρχουν πρόσφατες συνεδρίες!\nΔοκιμάστε να ξεκινήσετε μια νέα."),
("empty_favorite_tip", "Δεν υπάρχουν ακόμη αγαπημένες συνδέσεις;\nΑφού πραγματοποιήσετε σύνδεση με κάποιο απομακρυσμένο σταθμό, μπορείτε να τον προσθέσετε στα αγαπημένα σας!"),
("empty_lan_tip", "Δεν έχουμε ανακαλυφθεί ακόμη απομακρυσμένοι σταθμοί."),
("empty_address_book_tip", "Φαίνεται ότι αυτή τη στιγμή δεν υπάρχουν αγαπημένες συνδέσεις στο βιβλίο διευθύνσεών σας."),
("empty_recent_tip", "Ωχ, δεν υπάρχουν πρόσφατες συνεδρίες!\nΏρα να προγραμματίσετε μια νέα."),
("empty_favorite_tip", "Δεν έχετε ακόμα αγαπημένους απομακρυσμένους σταθμούς;\nΑς βρούμε κάποιον για να συνδεθούμε και ας τον προσθέσουμε στα αγαπημένα σας!"),
("empty_lan_tip", "Ωχ όχι, φαίνεται ότι δεν έχουμε ανακαλύψει ακόμη κανέναν απομακρυσμένο σταθμό."),
("empty_address_book_tip", "Ω, Αγαπητέ/ή μου, φαίνεται ότι αυτήν τη στιγμή δεν υπάρχουν απομακρυσμένοι σταθμοί στο βιβλίο διευθύνσεών σας."),
("Empty Username", "Κενό όνομα χρήστη"),
("Empty Password", "Κενός κωδικός πρόσβασης"),
("Me", "Εγώ"),
("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."),
("identical_file_tip", "Αυτό το αρχείο είναι πανομοιότυπο με αυτό του απομακρυσμένου σταθμού."),
("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"),
("View Mode", "Λειτουργία προβολής"),
("login_linux_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."),
("login_linux_tip", "Πρέπει να συνδεθείτε σε έναν απομακρυσμένο λογαριασμό Linux για να ενεργοποιήσετε μια συνεδρία επιφάνειας εργασίας X"),
("verify_rustdesk_password_tip", "Επιβεβαιώστε τον κωδικό του RustDesk"),
("remember_account_tip", "Απομνημόνευση αυτού του λογαριασμού"),
("os_account_desk_tip", "Αυτός ο λογαριασμός θα χρησιμοποιηθεί για την είσοδο και διαχείριση του απομακρυσμένου λειτουργικού συστήματος"),
("os_account_desk_tip", "Αυτός ο λογαριασμός χρησιμοποιείται για σύνδεση στο απομακρυσμένο λειτουργικό σύστημα και ενεργοποίηση της συνεδρίας επιφάνειας εργασίας σε headless"),
("OS Account", "Λογαριασμός λειτουργικού συστήματος"),
("another_user_login_title_tip", "Υπάρχει ήδη άλλος συνδεδεμένος χρήστης"),
("another_user_login_text_tip", "Αποσύνδεση"),
("xorg_not_found_title_tip", "Δεν βρέθηκε το Xorg"),
("xorg_not_found_text_tip", "Παρακαλώ εγκαταστήστε το Xorg"),
("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμη επιφάνεια εργασίας"),
("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμο περιβάλλον επιφάνειας εργασίας"),
("no_desktop_text_tip", "Παρακαλώ εγκαταστήστε το περιβάλλον GNOME"),
("No need to elevate", "Δεν χρειάζονται αυξημένα δικαιώματα"),
("No need to elevate", "Δεν χρειάζεται ανύψωση"),
("System Sound", "Ήχος συστήματος"),
("Default", "Προκαθορισμένο"),
("New RDP", "Νέα απομακρυσμένη σύνδεση"),
("Fingerprint", ""),
("Copy Fingerprint", ""),
("no fingerprints", ""),
("Select a peer", "Επιλέξτε σταθμό"),
("New RDP", "Νέα RDP"),
("Fingerprint", "Δακτυλικό αποτύπωμα"),
("Copy Fingerprint", "Αντιγραφή δακτυλικού αποτυπώματος"),
("no fingerprints", "χωρίς δακτυλικά αποτυπώματα"),
("Select a peer", "Επιλέξτε έναν σταθμό"),
("Select peers", "Επιλέξτε σταθμούς"),
("Plugins", "Επεκτάσεις"),
("Uninstall", "Κατάργηση εγκατάστασης"),
@@ -493,10 +493,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("resolution_original_tip", "Αρχική ανάλυση"),
("resolution_fit_local_tip", "Προσαρμογή στην τοπική ανάλυση"),
("resolution_custom_tip", "Προσαρμοσμένη ανάλυση"),
("Collapse toolbar", "Εμφάνιση γραμμής εργαλείων"),
("Accept and Elevate", "Αποδοχή με αυξημένα δικαιώματα"),
("accept_and_elevate_btn_tooltip", "Αποδοχή της σύνδεσης με αυξημένα δικαιώματα χρήστη"),
("clipboard_wait_response_timeout_tip", "Έληξε ο χρόνος αναμονής για την ανταπόκριση της αντιγραφής"),
("Collapse toolbar", "Σύμπτυξη γραμμής εργαλείων"),
("Accept and Elevate", "Αποδοχή και ανύψωση"),
("accept_and_elevate_btn_tooltip", "Αποδεχτείτε τη σύνδεση και ανυψώστε τα δικαιώματα UAC."),
("clipboard_wait_response_timeout_tip", "Λήξη χρονικού ορίου αναμονής για απάντηση αντιγραφής."),
("Incoming connection", "Εισερχόμενη σύνδεση"),
("Outgoing connection", "Εξερχόμενη σύνδεση"),
("Exit", "Έξοδος"),
@@ -505,7 +505,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Service", "Υπηρεσία"),
("Start", "Έναρξη"),
("Stop", "Διακοπή"),
("exceed_max_devices", "Υπέρβαση μέγιστου ορίου αποθηκευμένων συνδέσεων"),
("exceed_max_devices", "Έχετε φτάσει τον μέγιστο αριθμό διαχειριζόμενων συσκευών."),
("Sync with recent sessions", "Συγχρονισμός των πρόσφατων συνεδριών"),
("Sort tags", "Ταξινόμηση ετικετών"),
("Open connection in new tab", "Άνοιγμα σύνδεσης σε νέα καρτέλα"),
@@ -514,14 +514,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Already exists", "Υπάρχει ήδη"),
("Change Password", "Αλλαγή κωδικού"),
("Refresh Password", "Ανανέωση κωδικού"),
("ID", ""),
("ID", "ID"),
("Grid View", "Προβολή σε πλακίδια"),
("List View", "Προβολή σε λίστα"),
("Select", "Επιλογή"),
("Toggle Tags", "Εναλλαγή ετικετών"),
("pull_ab_failed_tip", "Αποτυχία ανανέωσης βιβλίου διευθύνσεων"),
("push_ab_failed_tip", "Αποτυχία συγχρονισμού βιβλίου διευθύνσεων"),
("synced_peer_readded_tip", "Οι συσκευές των τρεχουσών συνεδριών θα συγχρονιστούν με το βιβλίο διευθύνσεων"),
("pull_ab_failed_tip", "Η ανανέωση του βιβλίου διευθύνσεων απέτυχε"),
("push_ab_failed_tip", "Αποτυχία συγχρονισμού του βιβλίου διευθύνσεων με τον διακομιστή"),
("synced_peer_readded_tip", "Οι συσκευές που υπήρχαν στις πρόσφατες συνεδρίες θα συγχρονιστούν ξανά με το βιβλίο διευθύνσεων."),
("Change Color", "Αλλαγή χρώματος"),
("Primary Color", "Κυρίως χρώμα"),
("HSV Color", "Χρώμα HSV"),
@@ -536,31 +536,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("I Agree", "Συμφωνώ"),
("Decline", "Διαφωνώ"),
("Timeout in minutes", "Τέλος χρόνου σε λεπτά"),
("auto_disconnect_option_tip", "Αυτόματη αποσύνδεση απομακρυσμένης συνεδρίας έπειτα από την πάροδο του χρονικού ορίου αδράνειας "),
("auto_disconnect_option_tip", "Αυτόματο κλείσιμο εισερχόμενων συνεδριών σε περίπτωση αδράνειας χρήστη"),
("Connection failed due to inactivity", "Η σύνδεση τερματίστηκε έπειτα από την πάροδο του χρόνου αδράνειας"),
("Check for software update on startup", "Έλεγχος για ενημερώσεις κατα την εκκίνηση"),
("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε τον RustDesk Server Pro στην έκδοση {} ή νεότερη!"),
("Check for software update on startup", "Έλεγχος για ενημερώσεις κατά την εκκίνηση"),
("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε το RustDesk Server Pro στην έκδοση {} ή νεότερη!"),
("pull_group_failed_tip", "Αποτυχία ανανέωσης της ομάδας"),
("Filter by intersection", ""),
("Filter by intersection", "Φιλτράρισμα κατά διασταύρωση"),
("Remove wallpaper during incoming sessions", "Αφαίρεση εικόνας φόντου στις εισερχόμενες συνδέσεις"),
("Test", "Δοκιμή"),
("display_is_plugged_out_msg", "Η οθόνη έχει αποσυνδεθεί, επιστρέψτε στην κύρια οθόνη προβολής"),
("display_is_plugged_out_msg", "Η οθόνη είναι αποσυνδεδεμένη από την πρίζα, μεταβείτε στην πρώτη οθόνη."),
("No displays", "Δεν υπάρχουν οθόνες"),
("Open in new window", "Άνοιγμα σε νέο παράθυρο"),
("Show displays as individual windows", "Εμφάνιση οθονών σε ξεχωριστά παράθυρα"),
("Use all my displays for the remote session", "Χρήση όλων των οθονών της απομακρυσμένης σύνδεσης"),
("selinux_tip", "Έχετε ενεργοποιημένο το SELinux, το οποίο πιθανόν εμποδίζει την ορθή λειτουργία του RustDesk."),
("selinux_tip", "Το SELinux είναι ενεργοποιημένο στη συσκευή σας, κάτι που ενδέχεται να εμποδίσει την σωστή λειτουργία του RustDesk ως ελεγχόμενης πλευράς."),
("Change view", "Αλλαγή απεικόνισης"),
("Big tiles", "Μεγάλα εικονίδια"),
("Small tiles", "Μικρά εικονίδια"),
("List", "Λίστα"),
("Virtual display", "Εινονική οθόνη"),
("Plug out all", "Αποσύνδεση όλων"),
("True color (4:4:4)", ""),
("True color (4:4:4)", "Αληθινό χρώμα (4:4:4)"),
("Enable blocking user input", "Ενεργοποίηση αποκλεισμού χειρισμού από τον χρήστη"),
("id_input_tip", "Μπορείτε να εισάγετε ενα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (<domain>:<port>).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"<id>@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."),
("privacy_mode_impl_mag_tip", "Προφύλαξη Οθόνης"),
("privacy_mode_impl_virtual_display_tip", "Εικονική Οθόνη"),
("id_input_tip", "Μπορείτε να εισάγετε ένα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (<domain>:<port>).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"<id>@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."),
("privacy_mode_impl_mag_tip", "Λειτουργία 1"),
("privacy_mode_impl_virtual_display_tip", "Λειτουργία 2"),
("Enter privacy mode", "Ενεργοποίηση λειτουργίας απορρήτου"),
("Exit privacy mode", "Διακοπή λειτουργίας απορρήτου"),
("idd_not_support_under_win10_2004_tip", "Το πρόγραμμα οδήγησης έμμεσης οθόνης δεν υποστηρίζεται. Απαιτείτε λειτουργικό σύστημα Windows 10 έκδοση 2004 ή νεότερο."),
@@ -570,26 +570,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("swap-left-right-mouse", "Εναλλαγή αριστερό-δεξί κουμπί του ποντικιού"),
("2FA code", "κωδικός 2FA"),
("More", "Περισσότερα"),
("enable-2fa-title", "Ενεργοποίηση Πιστοποίησης Δύο Παραγόντων"),
("enable-2fa-desc", "Ρυθμίστε τον έλεγχο ταυτότητας τώρα. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως Authy, Microsoft ή Google Authenticator στο τηλέφωνο ή στην επιφάνεια εργασίας σας.Σαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."),
("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι ο κωδικός και οι ρυθμίσεις τοπικής ώρας είναι σωστές"),
("enable-2fa-title", "Ενεργοποίηση πιστοποίησης δύο παραγόντων"),
("enable-2fa-desc", "Παρακαλούμε να ρυθμίστε τώρα τον έλεγχο ταυτότητας. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως το Authy, το Microsoft ή το Google Authenticator στο τηλέφωνο ή τον υπολογιστή σας.\n\nΣαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."),
("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι οι ρυθμίσεις κωδικού και τοπικής ώρας είναι σωστές."),
("enter-2fa-title", "Έλεγχος ταυτότητας δύο παραγόντων"),
("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι εως 6 χαρακτήρες"),
("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι έως 6 χαρακτήρες"),
("2FA code must be 6 digits.", "Ο κωδικός 2FA πρέπει να είναι 6ψήφιος."),
("Multiple Windows sessions found", ""),
("Multiple Windows sessions found", "Βρέθηκαν πολλές συνεδρίες των Windows"),
("Please select the session you want to connect to", "Επιλέξτε τη συνεδρία στην οποία θέλετε να συνδεθείτε"),
("powered_by_me", "Με την υποστήριξη της RustDesk"),
("outgoing_only_desk_tip", ""),
("preset_password_warning", "προειδοποίηση προκαθορισμένου κωδικού πρόσβασης"),
("powered_by_me", "Με την υποστήριξη του RustDesk"),
("outgoing_only_desk_tip", "Αυτή είναι μια προσαρμοσμένη έκδοση.\nΜπορείτε να συνδεθείτε με άλλες συσκευές, αλλά άλλες συσκευές δεν μπορούν να συνδεθούν με τη δική σας συσκευή."),
("preset_password_warning", "Αυτή η προσαρμοσμένη έκδοση συνοδεύεται από έναν προκαθορισμένο κωδικό πρόσβασης. Όποιος γνωρίζει αυτόν τον κωδικό πρόσβασης θα μπορούσε να αποκτήσει τον πλήρη έλεγχο της συσκευής σας. Εάν δεν το περιμένατε αυτό, απεγκαταστήστε αμέσως το λογισμικό."),
("Security Alert", "Ειδοποίηση ασφαλείας"),
("My address book", "Το βιβλίο διευθύνσεών μου"),
("Personal", "Προσωπικό"),
("Owner", "Ιδιοκτήτης"),
("Set shared password", "Ορίστε κοινόχρηστο κωδικό πρόσβασης"),
("Set shared password", "Ορίστε έναν κοινόχρηστο κωδικό πρόσβασης"),
("Exist in", "Υπάρχει στο"),
("Read-only", "Μόνο για ανάγνωση"),
("Read/Write", "Ανάγνωση/Εγγραφή"),
("Full Control", "Πλήρης Έλεγχος"),
("Full Control", "Πλήρης έλεγχος"),
("share_warning_tip", "Τα παραπάνω πεδία είναι κοινόχρηστα και ορατά σε άλλους."),
("Everyone", "Όλοι"),
("ab_web_console_tip", "Περισσότερα στην κονσόλα web"),
@@ -597,18 +597,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάρχουν φυσικές οθόνες, δεν χρειάζεται να χρησιμοποιήσετε τη λειτουργία απορρήτου."),
("Follow remote cursor", "Παρακολούθηση απομακρυσμένου κέρσορα"),
("Follow remote window focus", "Παρακολούθηση απομακρυσμένου ενεργού παραθύρου"),
("default_proxy_tip", "Προκαθορισμένο πρωτόκολλο Socks5 στην πόρτα 1080"),
("default_proxy_tip", "Το προεπιλεγμένο πρωτόκολλο και η θύρα είναι Socks5 και 1080"),
("no_audio_input_device_tip", "Δεν βρέθηκε συσκευή εισόδου ήχου."),
("Incoming", "Εισερχόμενη"),
("Outgoing", "Εξερχόμενη"),
("Clear Wayland screen selection", ""),
("clear_Wayland_screen_selection_tip", ""),
("confirm_clear_Wayland_screen_selection_tip", ""),
("android_new_voice_call_tip", ""),
("texture_render_tip", ""),
("Use texture rendering", ""),
("Floating window", ""),
("floating_window_tip", ""),
("Clear Wayland screen selection", "Εκκαθάριση επιλογής οθόνης Wayland"),
("clear_Wayland_screen_selection_tip", "Αφού διαγράψετε την επιλογή οθόνης, μπορείτε να επιλέξετε ξανά την οθόνη για κοινή χρήση."),
("confirm_clear_Wayland_screen_selection_tip", "Είστε βέβαιοι ότι θέλετε να διαγράψετε την επιλογή οθόνης Wayland;"),
("android_new_voice_call_tip", "Ελήφθη ένα νέο αίτημα φωνητικής κλήσης. Εάν το αποδεχτείτε, ο ήχος θα μεταβεί σε φωνητική επικοινωνία."),
("texture_render_tip", "Χρησιμοποιήστε την απόδοση υφής για να κάνετε τις εικόνες πιο ομαλές. Μπορείτε να δοκιμάσετε να απενεργοποιήσετε αυτήν την επιλογή εάν αντιμετωπίσετε προβλήματα απόδοσης."),
("Use texture rendering", "Χρήση απόδοσης υφής"),
("Floating window", "Πλωτό παράθυρο"),
("floating_window_tip", "Βοηθά στη διατήρηση της υπηρεσίας παρασκηνίου RustDesk"),
("Keep screen on", "Διατήρηση οθόνης Ανοιχτή"),
("Never", "Ποτέ"),
("During controlled", "Κατα την διάρκεια απομακρυσμένου ελέγχου"),
@@ -618,8 +618,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Apps", "Εφαρμογές"),
("Volume up", "Αύξηση έντασης"),
("Volume down", "Μείωση έντασης"),
("Power", ""),
("Telegram bot", ""),
("Power", "Ενέργεια"),
("Telegram bot", "Telegram bot"),
("enable-bot-tip", "Εάν ενεργοποιήσετε αυτήν τη δυνατότητα, μπορείτε να λάβετε τον κωδικό 2FA από το bot σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."),
("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακριτικό αφού ολοκληρώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουργήσατε. Στείλτε ένα μήνυμα που αρχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεργοποιήσετε."),
("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"),
@@ -639,11 +639,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "Γονικός φάκελος"),
("Resume", "Συνέχεια"),
("Invalid file name", "Μη έγκυρο όνομα αρχείου"),
("one-way-file-transfer-tip", ""),
("one-way-file-transfer-tip", "Η μονόδρομη μεταφορά αρχείων είναι ενεργοποιημένη στην ελεγχόμενη πλευρά."),
("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"),
("Authenticate", "Πιστοποίηση"),
("web_id_input_tip", ""),
("Download", ""),
("web_id_input_tip", "Μπορείτε να εισαγάγετε ένα ID στον ίδιο διακομιστή, η άμεση πρόσβαση IP δεν υποστηρίζεται στον web client.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε άλλον διακομιστή, παρακαλούμε να προσθέστε τη διεύθυνση διακομιστή (<id>@<server_address>?key=<key_value>), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε δημόσιο διακομιστή, παρακαλούμε να εισαγάγετε \"<id>@public\". Το κλειδί δεν είναι απαραίτητο για δημόσιο διακομιστή."),
("Download", "Λήψη"),
("Upload folder", "Μεταφόρτωση φακέλου"),
("Upload files", "Μεταφόρτωση αρχείων"),
("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"),
@@ -652,93 +652,93 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
("Accessible devices", "Προσβάσιμες συσκευές"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"),
("d3d_render_tip", ""),
("Use D3D rendering", ""),
("Printer", ""),
("printer-os-requirement-tip", ""),
("printer-requires-installed-{}-client-tip", ""),
("printer-{}-not-installed-tip", ""),
("printer-{}-ready-tip", ""),
("Install {} Printer", ""),
("Outgoing Print Jobs", ""),
("Incoming Print Jobs", ""),
("Incoming Print Job", ""),
("use-the-default-printer-tip", ""),
("use-the-selected-printer-tip", ""),
("auto-print-tip", ""),
("print-incoming-job-confirm-tip", ""),
("remote-printing-disallowed-tile-tip", ""),
("remote-printing-disallowed-text-tip", ""),
("save-settings-tip", ""),
("dont-show-again-tip", ""),
("Take screenshot", ""),
("Taking screenshot", ""),
("screenshot-merged-screen-not-supported-tip", ""),
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
("websocket_tip", ""),
("Use WebSocket", ""),
("Trackpad speed", ""),
("Default trackpad speed", ""),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("d3d_render_tip", "Όταν είναι ενεργοποιημένη η απόδοση D3D, η οθόνη του τηλεχειριστηρίου ενδέχεται να είναι μαύρη σε ορισμένα μηχανήματα."),
("Use D3D rendering", "Χρήση απόδοσης D3D"),
("Printer", "Εκτυπωτής"),
("printer-os-requirement-tip", "Η λειτουργία εξερχόμενης εκτύπωσης του εκτυπωτή απαιτεί Windows 10 ή νεότερη έκδοση."),
("printer-requires-installed-{}-client-tip", "Για να χρησιμοποιήσετε την απομακρυσμένη εκτύπωση, πρέπει να εγκατασταθεί το {} σε αυτήν τη συσκευή."),
("printer-{}-not-installed-tip", "Ο εκτυπωτής {} δεν είναι εγκατεστημένος."),
("printer-{}-ready-tip", "Ο εκτυπωτής {} είναι εγκατεστημένος και έτοιμος για χρήση."),
("Install {} Printer", "Εγκατάσταση εκτυπωτή {}"),
("Outgoing Print Jobs", "Εξερχόμενες εργασίες εκτύπωσης"),
("Incoming Print Jobs", "Εισερχόμενες εργασίες εκτύπωσης"),
("Incoming Print Job", "Εισερχόμενη εργασία εκτύπωσης"),
("use-the-default-printer-tip", "Χρήση του προεπιλεγμένου εκτυπωτή"),
("use-the-selected-printer-tip", "Χρήση του επιλεγμένου εκτυπωτή"),
("auto-print-tip", "Εκτυπώστε αυτόματα χρησιμοποιώντας τον επιλεγμένο εκτυπωτή."),
("print-incoming-job-confirm-tip", "Λάβατε μια εργασία εκτύπωσης από απόσταση. Θέλετε να την εκτελέσετε από την πλευρά σας;"),
("remote-printing-disallowed-tile-tip", "Η απομακρυσμένη εκτύπωση δεν επιτρέπεται"),
("remote-printing-disallowed-text-tip", "Οι ρυθμίσεις δικαιωμάτων της ελεγχόμενης πλευράς απαγορεύουν την Απομακρυσμένη Εκτύπωση."),
("save-settings-tip", "Αποθήκευση ρυθμίσεων"),
("dont-show-again-tip", "Να μην εμφανιστεί ξανά αυτό"),
("Take screenshot", "Λήψη στιγμιότυπου οθόνης"),
("Taking screenshot", "Γίνεται λήψη στιγμιότυπου οθόνης"),
("screenshot-merged-screen-not-supported-tip", "Η συγχώνευση στιγμιότυπων οθόνης από πολλές οθόνες δεν υποστηρίζεται προς το παρόν. Αλλάξτε σε μία μόνο οθόνη και δοκιμάστε ξανά."),
("screenshot-action-tip", "Επιλέξτε πώς θα συνεχίσετε με το στιγμιότυπο οθόνης."),
("Save as", "Αποθήκευση ως"),
("Copy to clipboard", "Αντιγραφή στο πρόχειρο"),
("Enable remote printer", "Ενεργοποίηση απομακρυσμένου εκτυπωτή"),
("Downloading {}", "Γίνεται Λήψη {}"),
("{} Update", "{} Ενημέρωση"),
("{}-to-update-tip", "Το {} θα κλείσει τώρα και θα εγκαταστήσει τη νέα έκδοση."),
("download-new-version-failed-tip", "Η λήψη απέτυχε. Μπορείτε να δοκιμάσετε ξανά ή να κάνετε κλικ στο κουμπί \"Λήψη\" για να κάνετε λήψη από τη σελίδα έκδοσης και να κάνετε αναβάθμιση χειροκίνητα."),
("Auto update", "Αυτόματη ενημέρωση"),
("update-failed-check-msi-tip", "Η μέθοδος εγκατάστασης απέτυχε. Κάντε κλικ στο κουμπί \"Λήψη\" για λήψη από τη σελίδα έκδοσης και κάντε χειροκίνητα την αναβάθμιση."),
("websocket_tip", "Όταν χρησιμοποιείτε το WebSocket, υποστηρίζονται μόνο συνδέσεις αναμετάδοσης."),
("Use WebSocket", "Χρήση WebSocket"),
("Trackpad speed", "Ταχύτητα trackpad"),
("Default trackpad speed", "Προεπιλεγμένη ταχύτητα trackpad"),
("Numeric one-time password", "Αριθμητικός κωδικός πρόσβασης μίας χρήσης"),
("Enable IPv6 P2P connection", "Ενεργοποίηση σύνδεσης IPv6 P2P"),
("Enable UDP hole punching", "Ενεργοποίηση διάτρησης οπών UDP"),
("View camera", "Προβολή κάμερας"),
("Enable camera", ""),
("No cameras", ""),
("view_camera_unsupported_tip", ""),
("Terminal", ""),
("Enable terminal", ""),
("New tab", ""),
("Keep terminal sessions on disconnect", ""),
("Terminal (Run as administrator)", ""),
("terminal-admin-login-tip", ""),
("Failed to get user token.", ""),
("Incorrect username or password.", ""),
("The user is not an administrator.", ""),
("Failed to check if the user is an administrator.", ""),
("Supported only in the installed version.", ""),
("elevation_username_tip", ""),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Scale custom", ""),
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
("Allow insecure TLS fallback", ""),
("allow-insecure-tls-fallback-tip", ""),
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Enable camera", "Ενεργοποίηση κάμερας"),
("No cameras", "Δεν υπάρχουν κάμερες"),
("view_camera_unsupported_tip", "Η τηλεχειριστήριο δεν υποστηρίζει την προβολή της κάμερας."),
("Terminal", "Τερματικό"),
("Enable terminal", "Ενεργοποίηση τερματικού"),
("New tab", "Νέα καρτέλα"),
("Keep terminal sessions on disconnect", "Διατήρηση περιόδων λειτουργίας τερματικού κατά την αποσύνδεση"),
("Terminal (Run as administrator)", "Τερματικό (Εκτέλεση ως διαχειριστής)"),
("terminal-admin-login-tip", "Παρακαλώ εισάγετε το όνομα χρήστη και τον κωδικό πρόσβασης διαχειριστή της ελεγχόμενης πλευράς."),
("Failed to get user token.", "Αποτυχία λήψης διακριτικού χρήστη."),
("Incorrect username or password.", "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης."),
("The user is not an administrator.", "Ο χρήστης δεν είναι διαχειριστής."),
("Failed to check if the user is an administrator.", "Αποτυχία ελέγχου εάν ο χρήστης είναι διαχειριστής."),
("Supported only in the installed version.", "Υποστηρίζεται μόνο στην εγκατεστημένη έκδοση."),
("elevation_username_tip", "Εισαγάγετε όνομα χρήστη ή τομέα\\όνομα χρήστη"),
("Preparing for installation ...", "Προετοιμασία για εγκατάσταση..."),
("Show my cursor", "Εμφάνιση του κέρσορα μου"),
("Scale custom", "Προσαρμοσμένη κλίμακα"),
("Custom scale slider", "Ρυθμιστικό προσαρμοσμένης κλίμακας"),
("Decrease", "Μείωση"),
("Increase", "Αύξηση"),
("Show virtual mouse", "Εμφάνιση εικονικού ποντικιού"),
("Virtual mouse size", "Μέγεθος εικονικού ποντικιού"),
("Small", "Μικρό"),
("Large", "Μεγάλο"),
("Show virtual joystick", "Εμφάνιση εικονικού joystick"),
("Edit note", "Επεξεργασία σημείωσης"),
("Alias", "Ψευδώνυμο"),
("ScrollEdge", "Άκρη κύλισης"),
("Allow insecure TLS fallback", "Να επιτρέπεται η μη ασφαλής εφεδρική λειτουργία TLS"),
("allow-insecure-tls-fallback-tip", "Από προεπιλογή, το RustDesk επαληθεύει το πιστοποιητικό διακομιστή για πρωτόκολλα που χρησιμοποιούν TLS.\nΜε ενεργοποιημένη αυτήν την επιλογή, το RustDesk θα παρακάμψει το βήμα επαλήθευσης και θα προχωρήσει σε περίπτωση αποτυχίας επαλήθευσης."),
("Disable UDP", "Απενεργοποίηση UDP"),
("disable-udp-tip", "Ελέγχει εάν θα χρησιμοποιείται μόνο TCP.\nΌταν είναι ενεργοποιημένη αυτή η επιλογή, το RustDesk δεν θα χρησιμοποιεί πλέον το UDP 21116, αλλά θα χρησιμοποιείται το TCP 21116."),
("server-oss-not-support-tip", "ΣΗΜΕΙΩΣΗ: Το OSS του διακομιστή RustDesk δεν περιλαμβάνει αυτήν τη λειτουργία."),
("input note here", "εισάγετε σημείωση εδώ"),
("note-at-conn-end-tip", "Ζητήστε σημείωση στο τέλος της σύνδεσης"),
("Show terminal extra keys", "Εμφάνιση επιπλέον κλειδιών τερματικού"),
("Relative mouse mode", "Σχετική λειτουργία ποντικιού"),
("rel-mouse-not-supported-peer-tip", "Η λειτουργία σχετικού ποντικιού δεν υποστηρίζεται από τον συνδεδεμένο ομότιμο υπολογιστή."),
("rel-mouse-not-ready-tip", "Η λειτουργία σχετικού ποντικιού δεν είναι ακόμη έτοιμη. Δοκιμάστε ξανά."),
("rel-mouse-lock-failed-tip", "Αποτυχία κλειδώματος δρομέα. Η λειτουργία σχετικού ποντικιού έχει απενεργοποιηθεί."),
("rel-mouse-exit-{}-tip", "Πιέστε {} για έξοδο."),
("rel-mouse-permission-lost-tip", "Η άδεια πληκτρολογίου ανακλήθηκε. Η λειτουργία σχετικού ποντικιού απενεργοποιήθηκε."),
("Changelog", "Αρχείο αλλαγών"),
("keep-awake-during-outgoing-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια εξερχόμενων συνεδριών"),
("keep-awake-during-incoming-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια των εισερχόμενων συνεδριών"),
("Continue with {}", "Συνέχεια με {}"),
("Display Name", ""),
("Display Name", "Εμφανιζόμενο όνομα"),
].iter().cloned().collect();
}

View File

@@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Set permanent password", "Définir le mot de passe permanent"),
("Enable remote restart", "Activer le redémarrage à distance"),
("Restart remote device", "Redémarrer lappareil distant"),
("Are you sure you want to restart", "Voulez-vous vraiment redémarrer lappareil ?"),
("Are you sure you want to restart", "Voulez-vous vraiment redémarrer"),
("Restarting remote device", "Redémarrage de lappareil distant"),
("remote_restarting_tip", "L'appareil distant redémarre ; veuillez fermer cette boîte de dialogue et vous reconnecter en utilisant le mot de passe permanent dans quelques instants"),
("Copied", "Copié"),
@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Maintenir lécran allumé lors des sessions sortantes"),
("keep-awake-during-incoming-sessions-label", "Maintenir lécran allumé lors des sessions entrantes"),
("Continue with {}", "Continuer avec {}"),
("Display Name", ""),
("Display Name", "Nom daffichage"),
].iter().cloned().collect();
}

View File

@@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Password", "Jelszó"),
("Ready", "Kész"),
("Established", "Létrejött"),
("connecting_status", "Kapcsolódás folyamatban"),
("connecting_status", "Kapcsolódás folyamatban ..."),
("Enable service", "Szolgáltatás engedélyezése"),
("Start service", "Szolgáltatás indítása"),
("Service is running", "Szolgáltatás aktív"),
@@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable file transfer", "Fájlátvitel engedélyezése"),
("Enable TCP tunneling", "TCP-alagút engedélyezése"),
("IP Whitelisting", "IP engedélyezési lista"),
("ID/Relay Server", "Azonosító-/Továbbító-kiszolgáló"),
("ID/Relay Server", "ID/Továbbító-kiszolgáló"),
("Import server config", "Kiszolgáló-konfiguráció importálása"),
("Export Server Config", "Kiszolgáló-konfiguráció exportálása"),
("Import server configuration successfully", "Kiszolgáló-konfiguráció sikeresen importálva"),
@@ -54,7 +54,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enhancements", "Fejlesztések"),
("Hardware Codec", "Hardveres kodek"),
("Adaptive bitrate", "Adaptív bitráta"),
("ID Server", "Azonosító-kiszolgáló"),
("ID Server", "ID-kiszolgáló"),
("Relay Server", "Továbbító-kiszolgáló"),
("API Server", "API-kiszolgáló"),
("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."),
@@ -76,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Connection Error", "Kapcsolódási hiba"),
("Error", "Hiba"),
("Reset by the peer", "A kapcsolatot a másik fél lezárta."),
("Connecting...", "Kapcsolódás"),
("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet"),
("Connecting...", "Kapcsolódás..."),
("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet ..."),
("Please try 1 minute later", "Próbálja meg 1 perc múlva"),
("Login Error", "Bejelentkezési hiba"),
("Successful", "Sikeres"),
("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra"),
("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra..."),
("Name", "Név"),
("Type", "Típus"),
("Modified", "Módosított"),
@@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Optimize reaction time", "Gyorsan reagáló"),
("Custom", "Egyéni"),
("Show remote cursor", "Távoli kurzor megjelenítése"),
("Show quality monitor", "Kapcsolat minőségének megjelenítése"),
("Show quality monitor", "Kijelző minőségének ellenőrzése"),
("Disable clipboard", "Közös vágólap kikapcsolása"),
("Lock after session end", "Távoli fiók zárolása a munkamenet végén"),
("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del billentyűzetkombinációt"),
@@ -150,8 +150,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Configure", "Beállítás"),
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
("Installing ...", "Telepítés"),
("Install", "Telepítés"),
("Installing ...", "Telepítés ..."),
("Install", "Telepítse"),
("Installation", "Telepítés"),
("Installation Path", "Telepítési útvonal"),
("Create start menu shortcuts", "Start menü parancsikonok létrehozása"),
@@ -159,10 +159,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licenc szerződés."),
("Accept and Install", "Elfogadás és telepítés"),
("End-user license agreement", "Végfelhasználói licenc szerződés"),
("Generating ...", "Előállítás…"),
("Generating ...", "Létrehozás ..."),
("Your installation is lower version.", "A telepített verzió alacsonyabb."),
("not_close_tcp_tip", "Ne zárja be ezt az ablakot, amíg TCP-alagutat használ"),
("Listening ...", "Figyelés"),
("Listening ...", "Figyelés ..."),
("Remote Host", "Távoli kiszolgáló"),
("Remote Port", "Távoli port"),
("Action", "Indítás"),
@@ -177,7 +177,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accept", "Elfogadás"),
("Dismiss", "Elutasítás"),
("Disconnect", "Kapcsolat bontása"),
("Enable file copy and paste", "Fájlmásolás és -beillesztés engedélyezése"),
("Enable file copy and paste", "Fájlmásolás és beillesztés engedélyezése"),
("Connected", "Kapcsolódva"),
("Direct and encrypted connection", "Közvetlen, és titkosított kapcsolat"),
("Relayed and encrypted connection", "Továbbított, és titkosított kapcsolat"),
@@ -185,7 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Relayed and unencrypted connection", "Továbbított, és nem titkosított kapcsolat"),
("Enter Remote ID", "Távoli számítógép azonosítója"),
("Enter your password", "Adja meg a jelszavát"),
("Logging in...", "Belépés folyamatban"),
("Logging in...", "Belépés folyamatban..."),
("Enable RDP session sharing", "RDP-munkamenet-megosztás engedélyezése"),
("Auto Login", "Automatikus bejelentkezés"),
("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"),
@@ -219,7 +219,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("verification_tip", "A regisztrált e-mail-címre egy ellenőrző kód lesz elküldve. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."),
("Logout", "Kilépés"),
("Tags", "Címkék"),
("Search ID", "Azonosító keresése"),
("Search ID", "Azonosító keresése..."),
("whitelist_sep", "A címeket vesszővel, pontosvesszővel, szóközzel vagy új sorral kell elválasztani"),
("Add ID", "Azonosító hozzáadása"),
("Add Tag", "Címke hozzáadása"),
@@ -258,10 +258,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Three-Finger vertically", "Három ujj függőlegesen"),
("Mouse Wheel", "Egérgörgő"),
("Two-Finger Move", "Kétujjas mozgatás"),
("Canvas Move", "Vászon mozgatása"),
("Canvas Move", "Nézet módosítása"),
("Pinch to Zoom", "Kétujjas nagyítás"),
("Canvas Zoom", "Vászon nagyítása"),
("Reset canvas", "Vászon visszaállítása"),
("Canvas Zoom", "Nézet nagyítása"),
("Reset canvas", "Nézet visszaállítása"),
("No permission of file transfer", "Nincs engedély a fájlátvitelre"),
("Note", "Megjegyzés"),
("Connection", "Kapcsolat"),
@@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable remote restart", "Távoli újraindítás engedélyezése"),
("Restart remote device", "Távoli eszköz újraindítása"),
("Are you sure you want to restart", "Biztosan újra szeretné indítani?"),
("Restarting remote device", "Távoli eszköz újraindítása"),
("Restarting remote device", "Távoli eszköz újraindítása..."),
("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, kapcsolódjon újra az állandó jelszavával"),
("Copied", "Másolva"),
("Exit Fullscreen", "Kilépés teljes képernyős módból"),
@@ -369,12 +369,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Deny LAN discovery", "Felfedezés tiltása"),
("Write a message", "Üzenet írása"),
("Prompt", "Kérés"),
("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére"),
("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére..."),
("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."),
("Disconnected", "Kapcsolat bontva"),
("Other", "Egyéb"),
("Confirm before closing multiple tabs", "Biztosan bezárja az összes lapot?"),
("Keyboard Settings", "Billentyűzet-beállítások"),
("Keyboard Settings", "Billentyűzetbeállítások"),
("Full Access", "Teljes hozzáférés"),
("Screen Share", "Képernyőmegosztás"),
("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."),
@@ -389,7 +389,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accept sessions via password", "Munkamenetek elfogadása jelszóval"),
("Accept sessions via click", "Munkamenetek elfogadása kattintással"),
("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"),
("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét"),
("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."),
("One-time Password", "Egyszer használatos jelszó"),
("Use one-time password", "Használjon ideiglenes jelszót"),
("One-time password length", "Egyszer használatos jelszó hossza"),
@@ -447,13 +447,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Resolution", "Felbontás"),
("No transfers in progress", "Nincs folyamatban átvitel"),
("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"),
("RDP Settings", "RDP-beállítások"),
("RDP Settings", "RDP beállítások"),
("Sort by", "Rendezés"),
("New Connection", "Új kapcsolat"),
("Restore", "Visszaállítás"),
("Minimize", "Minimalizálás"),
("Maximize", "Maximalizálás"),
("Your Device", "Saját eszköz"),
("Your Device", "Az én eszközöm"),
("empty_recent_tip", "Nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."),
("empty_favorite_tip", "Még nincs kedvenc távoli állomása?\nHagyja, hogy találjunk valakit, akivel kapcsolatba tud lépni, és adja hozzá a kedvencekhez!"),
("empty_lan_tip", "Úgy tűnik, még nem adott hozzá egyetlen távoli helyszínt sem."),
@@ -468,7 +468,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"),
("remember_account_tip", "Emlékezzen erre a fiókra"),
("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."),
("OS Account", "OS-fiók"),
("OS Account", "OS fiók"),
("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."),
("another_user_login_text_tip", "Különálló"),
("xorg_not_found_title_tip", "Xorg nem található."),
@@ -568,7 +568,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input_source_2_tip", "2. bemeneti forrás"),
("Swap control-command key", "Vezérlő- és parancsgombok cseréje"),
("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"),
("2FA code", "2FA-kód"),
("2FA code", "2FA kód"),
("More", "Továbbiak"),
("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"),
("enable-2fa-desc", "Állítsa be a hitelesítőt. Használhat egy hitelesítő alkalmazást, például az Aegis, Authy, a Microsoft- vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nOlvassa be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."),
@@ -647,13 +647,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Upload folder", "Mappa feltöltése"),
("Upload files", "Fájlok feltöltése"),
("Clipboard is synchronized", "A vágólap szinkronizálva van"),
("Update client clipboard", "Kliens vágólapjának frissítése"),
("Update client clipboard", "Az ügyfél vágólapjának frissítése"),
("Untagged", "Címkézetlen"),
("new-version-of-{}-tip", "A(z) {} új verziója"),
("Accessible devices", "Hozzáférhető eszközök"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"),
("d3d_render_tip", "D3D-leképezés"),
("Use D3D rendering", "D3D-leképezés használata"),
("d3d_render_tip", "D3D leképezés"),
("Use D3D rendering", "D3D leképezés használata"),
("Printer", "Nyomtató"),
("printer-os-requirement-tip", "Nyomtató operációs rendszerének minimális rendszerkövetelménye"),
("printer-requires-installed-{}-client-tip", "A nyomtatóhoz szükséges a(z) {} kliens telepítése"),
@@ -672,7 +672,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("save-settings-tip", "Beállítások mentése"),
("dont-show-again-tip", "Ne jelenítse meg újra"),
("Take screenshot", "Képernyőkép készítése"),
("Taking screenshot", "Képernyőkép készítése"),
("Taking screenshot", "Képernyőkép készítése..."),
("screenshot-merged-screen-not-supported-tip", "Egyesített képernyőről nem támogatott a képernyőkép készítése"),
("screenshot-action-tip", "Képernyőkép-művelet"),
("Save as", "Mentés másként"),
@@ -680,7 +680,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable remote printer", "Távoli nyomtatók engedélyezése"),
("Downloading {}", "{} letöltése"),
("{} Update", "{} frissítés"),
("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."),
("{}-to-update-tip", "{} bezárása és az új verzió telepítése."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("Auto update", "Automatikus frissítés"),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
@@ -707,7 +707,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."),
("Supported only in the installed version.", "Csak a telepített változatban támogatott."),
("elevation_username_tip", "Felhasználónév vagy tartománynév megadása"),
("Preparing for installation ...", "Felkészülés a telepítésre"),
("Preparing for installation ...", "Felkészülés a telepítésre ..."),
("Show my cursor", "Kurzor megjelenítése"),
("Scale custom", "Egyéni méretarány"),
("Custom scale slider", "Egyéni méretarány-csúszka"),
@@ -733,12 +733,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."),
("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."),
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."),
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."),
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a következő gombot: {}"),
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."),
("Changelog", "Változáslista"),
("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"),
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
("Continue with {}", "Folytatás a következővel: {}"),
("Display Name", ""),
("Continue with {}", "Folytatás ezzel: {}"),
("Display Name", "Kijelző név"),
].iter().cloned().collect();
}

View File

@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"),
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
("Continue with {}", "Continua con {}"),
("Display Name", ""),
("Display Name", "Visualizza nome"),
].iter().cloned().collect();
}

View File

@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
("Continue with {}", "{}(으)로 계속"),
("Display Name", ""),
("Display Name", "표시 이름"),
].iter().cloned().collect();
}

View File

@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."),
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
("Continue with {}", "Ga verder met {}"),
("Display Name", ""),
("Display Name", "Naam Weergeven"),
].iter().cloned().collect();
}

View File

@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"),
("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"),
("Continue with {}", "Продолжить с {}"),
("Display Name", ""),
("Display Name", "Отображаемое имя"),
].iter().cloned().collect();
}

View File

@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranıık tutun"),
("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranıık tutun"),
("Continue with {}", "{} ile devam et"),
("Display Name", ""),
("Display Name", "Görünen Ad"),
].iter().cloned().collect();
}

View File

@@ -42,9 +42,16 @@ static PRIVILEGES_SCRIPTS_DIR: Dir =
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
static mut LATEST_SEED: i32 = 0;
// Using a fixed temporary directory for updates is preferable to
// using one that includes the custom client name.
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate";
#[inline]
fn get_update_temp_dir() -> PathBuf {
let euid = unsafe { hbb_common::libc::geteuid() };
Path::new("/tmp").join(format!(".rustdeskupdate-{}", euid))
}
#[inline]
fn get_update_temp_dir_string() -> String {
get_update_temp_dir().to_string_lossy().into_owned()
}
/// Global mutex to serialize CoreGraphics cursor operations.
/// This prevents race conditions between cursor visibility (hide depth tracking)
@@ -279,24 +286,12 @@ fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync
Err(e) => {
log::error!("run osascript failed: {}", e);
}
Ok(status) if !status.success() => {
log::warn!("run osascript failed with status: {}", status);
}
_ => {
let installed = std::path::Path::new(&agent_plist_file).exists();
log::info!("Agent file {} installed: {}", &agent_plist_file, installed);
if installed {
// Unload first, or load may not work if already loaded.
// We hope that the load operation can immediately trigger a start.
std::process::Command::new("launchctl")
.args(&["unload", "-w", &agent_plist_file])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
let status = std::process::Command::new("launchctl")
.args(&["load", "-w", &agent_plist_file])
.status();
log::info!("launch server, status: {:?}", &status);
}
}
}
};
@@ -415,7 +410,9 @@ pub fn set_cursor_pos(x: i32, y: i32) -> bool {
let _guard = match CG_CURSOR_MUTEX.try_lock() {
Ok(guard) => guard,
Err(std::sync::TryLockError::WouldBlock) => {
log::error!("[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!");
log::error!(
"[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!"
);
debug_assert!(false, "Re-entrant call to set_cursor_pos detected");
return false;
}
@@ -822,7 +819,8 @@ pub fn quit_gui() {
#[inline]
pub fn try_remove_temp_update_dir(dir: Option<&str>) {
let target_path = Path::new(dir.unwrap_or(UPDATE_TEMP_DIR));
let target_path_buf = dir.map(PathBuf::from).unwrap_or_else(get_update_temp_dir);
let target_path = target_path_buf.as_path();
if target_path.exists() {
std::fs::remove_dir_all(target_path).ok();
}
@@ -851,32 +849,34 @@ pub fn update_me() -> ResultType<()> {
if is_installed_daemon && !is_service_stopped {
let agent = format!("{}_server.plist", crate::get_full_name());
let agent_plist_file = format!("/Library/LaunchAgents/{}", agent);
std::process::Command::new("launchctl")
.args(&["unload", "-w", &agent_plist_file])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.ok();
update_daemon_agent(agent_plist_file, app_dir, true);
} else {
// `kill -9` may not work without "administrator privileges"
let update_body = format!(
r#"
do shell script "
pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{app_name}.app' && ditto '{app_dir}' '/Applications/{app_name}.app' && chown -R {user}:staff '/Applications/{app_name}.app' && xattr -r -d com.apple.quarantine '/Applications/{app_name}.app'
" with prompt "{app_name} wants to update itself" with administrator privileges
"#,
app_name = app_name,
pid = std::process::id(),
app_dir = app_dir,
user = get_active_username()
);
match Command::new("osascript")
let update_body = r#"
on run {app_name, cur_pid, app_dir, user_name}
set app_bundle to "/Applications/" & app_name & ".app"
set app_bundle_q to quoted form of app_bundle
set app_dir_q to quoted form of app_dir
set user_name_q to quoted form of user_name
set check_source to "test -d " & app_dir_q & " || exit 1;"
set kill_others to "pids=$(pgrep -x '" & app_name & "' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;"
set copy_files to "rm -rf " & app_bundle_q & " && ditto " & app_dir_q & " " & app_bundle_q & " && chown -R " & user_name_q & ":staff " & app_bundle_q & " && (xattr -r -d com.apple.quarantine " & app_bundle_q & " || true);"
set sh to "set -e;" & check_source & kill_others & copy_files
do shell script sh with prompt app_name & " wants to update itself" with administrator privileges
end run
"#;
let active_user = get_active_username();
let status = Command::new("osascript")
.arg("-e")
.arg(update_body)
.status()
{
.arg(app_name.to_string())
.arg(std::process::id().to_string())
.arg(app_dir)
.arg(active_user)
.status();
match status {
Ok(status) if !status.success() => {
log::error!("osascript execution failed with status: {}", status);
}
@@ -897,25 +897,28 @@ pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{
}
pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> {
let update_temp_dir = get_update_temp_dir_string();
println!("Starting update from DMG: {}", dmg_path);
extract_dmg(dmg_path, UPDATE_TEMP_DIR)?;
extract_dmg(dmg_path, &update_temp_dir)?;
println!("DMG extracted");
update_extracted(UPDATE_TEMP_DIR)?;
update_extracted(&update_temp_dir)?;
println!("Update process started");
Ok(())
}
pub fn update_to(_file: &str) -> ResultType<()> {
update_extracted(UPDATE_TEMP_DIR)?;
let update_temp_dir = get_update_temp_dir_string();
update_extracted(&update_temp_dir)?;
Ok(())
}
pub fn extract_update_dmg(file: &str) {
let update_temp_dir = get_update_temp_dir_string();
let mut evt: HashMap<&str, String> =
HashMap::from([("name", "extract-update-dmg".to_string())]);
match extract_dmg(file, UPDATE_TEMP_DIR) {
match extract_dmg(file, &update_temp_dir) {
Ok(_) => {
log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR);
log::info!("Extracted dmg file to {}", update_temp_dir);
}
Err(e) => {
evt.insert("err", e.to_string());

View File

@@ -1,18 +1,26 @@
on run {daemon_file, agent_file, user, cur_pid, source_dir}
set unload_service to "launchctl unload -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist || true;"
set agent_plist to "/Library/LaunchAgents/com.carriez.RustDesk_server.plist"
set daemon_plist to "/Library/LaunchDaemons/com.carriez.RustDesk_service.plist"
set app_bundle to "/Applications/RustDesk.app"
set kill_others to "pgrep -x 'RustDesk' | grep -v " & cur_pid & " | xargs kill -9;"
set check_source to "test -d " & quoted form of source_dir & " || exit 1;"
set resolve_uid to "uid=$(id -u " & quoted form of user & " 2>/dev/null || true);"
set unload_agent to "if [ -n \"$uid\" ]; then launchctl bootout gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootout user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl unload -w " & quoted form of agent_plist & " || true; else launchctl unload -w " & quoted form of agent_plist & " || true; fi;"
set unload_service to "launchctl unload -w " & daemon_plist & " || true;"
set kill_others to "pids=$(pgrep -x 'RustDesk' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;"
set copy_files to "rm -rf /Applications/RustDesk.app && ditto " & source_dir & " /Applications/RustDesk.app && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app;"
set copy_files to "(rm -rf " & quoted form of app_bundle & " && ditto " & quoted form of source_dir & " " & quoted form of app_bundle & " && chown -R " & quoted form of user & ":staff " & quoted form of app_bundle & " && (xattr -r -d com.apple.quarantine " & quoted form of app_bundle & " || true)) || exit 1;"
set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;"
set write_daemon_plist to "echo " & quoted form of daemon_file & " > " & daemon_plist & " && chown root:wheel " & daemon_plist & ";"
set write_agent_plist to "echo " & quoted form of agent_file & " > " & agent_plist & " && chown root:wheel " & agent_plist & ";"
set load_service to "launchctl load -w " & daemon_plist & ";"
set agent_label_cmd to "agent_label=$(basename " & quoted form of agent_plist & " .plist);"
set bootstrap_agent to "if [ -n \"$uid\" ]; then launchctl bootstrap gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootstrap user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl load -w " & quoted form of agent_plist & " || true; else launchctl load -w " & quoted form of agent_plist & " || true; fi;"
set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;"
set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent
set sh2 to "echo " & quoted form of agent_file & " > /Library/LaunchAgents/com.carriez.RustDesk_server.plist && chown root:wheel /Library/LaunchAgents/com.carriez.RustDesk_server.plist;"
set sh3 to "launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;"
set sh to unload_service & kill_others & copy_files & sh1 & sh2 & sh3
set sh to "set -e;" & check_source & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent
do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges
end run

View File

@@ -580,6 +580,31 @@ extern "C"
return rdp_or_console;
}
BOOL is_session_locked(BOOL include_rdp)
{
DWORD session_id = get_current_session(include_rdp);
if (session_id == 0xFFFFFFFF) {
return FALSE;
}
PWTSINFOEXW pInfo = NULL;
DWORD bytes = 0;
BOOL locked = FALSE;
if (WTSQuerySessionInformationW(
WTS_CURRENT_SERVER_HANDLE,
session_id,
WTSSessionInfoEx,
(LPWSTR *)&pInfo,
&bytes)) {
if (pInfo && pInfo->Level == 1) {
locked = (pInfo->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK);
}
if (pInfo) {
WTSFreeMemory(pInfo);
}
}
return locked;
}
uint32_t get_active_user(PWSTR bufin, uint32_t nin, BOOL rdp)
{
uint32_t nout = 0;

View File

@@ -107,9 +107,9 @@ pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
let center_x = rect.left + (rect.right - rect.left) / 2;
let center_y = rect.top + (rect.bottom - rect.top) / 2;
center_x >= display.x
&& center_x <= display.x + display.width
&& center_x < display.x + display.width
&& center_y >= display.y
&& center_y <= display.y + display.height
&& center_y < display.y + display.height
})
}
}
@@ -153,11 +153,7 @@ pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool {
};
if result == FALSE {
let err = GetLastError();
log::warn!(
"ClipCursor failed: rect={:?}, error_code={}",
rect,
err
);
log::warn!("ClipCursor failed: rect={:?}, error_code={}", rect, err);
return false;
}
true
@@ -527,6 +523,7 @@ const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
extern "C" {
fn get_current_session(rdp: BOOL) -> DWORD;
fn is_session_locked(include_rdp: BOOL) -> BOOL;
fn LaunchProcessWin(
cmd: *const u16,
session_id: DWORD,
@@ -756,15 +753,37 @@ pub fn run_as_user(arg: Vec<&str>) -> ResultType<Option<std::process::Child>> {
run_exe_in_cur_session(std::env::current_exe()?.to_str().unwrap_or(""), arg, false)
}
pub fn run_exe_direct(
exe: &str,
arg: Vec<&str>,
show: bool,
) -> ResultType<Option<std::process::Child>> {
let mut cmd = std::process::Command::new(exe);
for a in arg {
cmd.arg(a);
}
if !show {
cmd.creation_flags(CREATE_NO_WINDOW);
}
match cmd.spawn() {
Ok(child) => Ok(Some(child)),
Err(e) => bail!("Failed to start process: {}", e),
}
}
pub fn run_exe_in_cur_session(
exe: &str,
arg: Vec<&str>,
show: bool,
) -> ResultType<Option<std::process::Child>> {
let Some(session_id) = get_current_process_session_id() else {
bail!("Failed to get current process session id");
};
run_exe_in_session(exe, arg, session_id, show)
if is_root() {
let Some(session_id) = get_current_process_session_id() else {
bail!("Failed to get current process session id");
};
run_exe_in_session(exe, arg, session_id, show)
} else {
run_exe_direct(exe, arg, show)
}
}
pub fn run_exe_in_session(
@@ -1129,6 +1148,10 @@ pub fn is_prelogin() -> bool {
username.is_empty() || username == "SYSTEM"
}
pub fn is_locked() -> bool {
unsafe { is_session_locked(share_rdp()) == TRUE }
}
// `is_logon_ui()` is regardless of multiple sessions now.
// It only check if "LogonUI.exe" exists.
//
@@ -1326,6 +1349,38 @@ pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> ResultType<String>
))
}
#[inline]
pub fn rename_exe_cmd(src_exe: &str, path: &str) -> ResultType<String> {
let src_exe_filename = PathBuf::from(src_exe)
.file_name()
.ok_or(anyhow!("Can't get file name of {src_exe}"))?
.to_string_lossy()
.to_string();
let app_name = crate::get_app_name().to_lowercase();
if src_exe_filename.to_lowercase() == format!("{app_name}.exe") {
Ok("".to_owned())
} else {
Ok(format!(
"
move /Y \"{path}\\{src_exe_filename}\" \"{path}\\{app_name}.exe\"
",
))
}
}
#[inline]
pub fn remove_meta_toml_cmd(is_msi: bool, path: &str) -> String {
if is_msi && crate::is_custom_client() {
format!(
"
del /F /Q \"{path}\\meta.toml\"
",
)
} else {
"".to_owned()
}
}
fn get_after_install(
exe: &str,
reg_value_start_menu_shortcuts: Option<String>,
@@ -1412,7 +1467,11 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res
}
let app_name = crate::get_app_name();
let current_exe = std::env::current_exe()?;
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
let cur_exe = current_exe.to_str().unwrap_or("").to_owned();
let shortcut_icon_location = get_shortcut_icon_location(&cur_exe);
let mk_shortcut = write_cmds(
format!(
"
@@ -1421,6 +1480,7 @@ sLinkFile = \"{tmp_path}\\{app_name}.lnk\"
Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = \"{exe}\"
{shortcut_icon_location}
oLink.Save
"
),
@@ -1477,8 +1537,13 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
reg_value_printer = "1".to_owned();
}
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
let size = meta.len() / 1024;
let meta = std::fs::symlink_metadata(&current_exe)?;
let mut size = meta.len() / 1024;
if let Some(parent_dir) = current_exe.parent() {
if let Some(d) = parent_dir.to_str() {
size = get_directory_size_kb(d);
}
}
// https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa
// https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10
// https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html
@@ -1531,7 +1596,7 @@ chcp 65001
md \"{path}\"
{copy_exe}
reg add {subkey} /f
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{display_icon}\"
reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\"
reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\"
@@ -1555,6 +1620,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
{install_remote_printer}
{sleep}
",
display_icon = get_custom_icon(&cur_exe).unwrap_or(exe.to_string()),
version = crate::VERSION.replace("-", "."),
build_date = crate::BUILD_DATE,
after_install = get_after_install(
@@ -1790,6 +1856,163 @@ fn get_reg_of(subkey: &str, name: &str) -> String {
"".to_owned()
}
fn get_public_base_dir() -> PathBuf {
if let Ok(allusersprofile) = std::env::var("ALLUSERSPROFILE") {
let path = PathBuf::from(&allusersprofile);
if path.exists() {
return path;
}
}
if let Ok(public) = std::env::var("PUBLIC") {
let path = PathBuf::from(public).join("Documents");
if path.exists() {
return path;
}
}
let program_data_dir = PathBuf::from("C:\\ProgramData");
if program_data_dir.exists() {
return program_data_dir;
}
std::env::temp_dir()
}
#[inline]
pub fn get_custom_client_staging_dir() -> PathBuf {
get_public_base_dir()
.join("RustDesk")
.join("RustDeskCustomClientStaging")
}
/// Removes the custom client staging directory.
///
/// Current behavior: intentionally a no-op (does not delete).
///
/// Rationale
/// - The staging directory only contains a small `custom.txt`, leaving it is harmless.
/// - Deleting directories under a public location (e.g., C:\\ProgramData\\RustDesk) is
/// susceptible to TOCTOU attacks if an unprivileged user can replace the path with a
/// symlink/junction between checks and deletion.
///
/// Future work:
/// - Use the files (if needed) in the installation directory instead of a public location.
/// This directory only contains a small `custom.txt` file.
/// - Pass the custom client name directly via command line
/// or environment variable during update installation. Then no staging directory is needed.
#[inline]
pub fn remove_custom_client_staging_dir(staging_dir: &Path) -> ResultType<bool> {
if !staging_dir.exists() {
return Ok(false);
}
// First explicitly removes `custom.txt` to ensure stale config is never replayed,
// even if the subsequent directory removal fails.
//
// `std::fs::remove_file` on a symlink removes the symlink itself, not the target,
// so this is safe even in a TOCTOU race.
let custom_txt_path = staging_dir.join("custom.txt");
if custom_txt_path.exists() {
allow_err!(std::fs::remove_file(&custom_txt_path));
}
// Intentionally not deleting. See the function docs for rationale.
log::debug!(
"Skip deleting staging directory {:?} (intentional to avoid TOCTOU)",
staging_dir
);
Ok(false)
}
// Prepare custom client update by copying staged custom.txt to current directory and loading it.
// Returns:
// 1. Ok(true) if preparation was successful or no staging directory exists.
// 2. Ok(false) if custom.txt file exists but has invalid contents or fails security checks
// (e.g., is a symlink or has invalid contents).
// 3. Err if any unexpected error occurs during file operations.
pub fn prepare_custom_client_update() -> ResultType<bool> {
let custom_client_staging_dir = get_custom_client_staging_dir();
let current_exe = std::env::current_exe()?;
let current_exe_dir = current_exe
.parent()
.ok_or(anyhow!("Cannot get parent directory of current exe"))?;
let staging_dir = custom_client_staging_dir.clone();
let clear_staging_on_exit = crate::SimpleCallOnReturn {
b: true,
f: Box::new(
move || match remove_custom_client_staging_dir(&staging_dir) {
Ok(existed) => {
if existed {
log::info!("Custom client staging directory removed successfully.");
}
}
Err(e) => {
log::error!(
"Failed to remove custom client staging directory {:?}: {}",
staging_dir,
e
);
}
},
),
};
if custom_client_staging_dir.exists() {
let custom_txt_path = custom_client_staging_dir.join("custom.txt");
if !custom_txt_path.exists() {
return Ok(true);
}
let metadata = std::fs::symlink_metadata(&custom_txt_path)?;
if metadata.is_symlink() {
log::error!(
"custom.txt is a symlink. Refusing to load custom client for security reasons."
);
drop(clear_staging_on_exit);
return Ok(false);
}
if metadata.is_file() {
// Copy custom.txt to current directory
let local_custom_file_path = current_exe_dir.join("custom.txt");
log::debug!(
"Copying staged custom file from {:?} to {:?}",
custom_txt_path,
local_custom_file_path
);
// No need to check symlink before copying.
// `load_custom_client()` will fail if the file is not valid.
fs::copy(&custom_txt_path, &local_custom_file_path)?;
log::info!("Staged custom client file copied to current directory.");
// Load custom client
let is_custom_file_exists =
local_custom_file_path.exists() && local_custom_file_path.is_file();
crate::load_custom_client();
// Remove the copied custom.txt file
allow_err!(fs::remove_file(&local_custom_file_path));
// Check if loaded successfully
if is_custom_file_exists && !crate::common::is_custom_client() {
// The custom.txt file existed, but its contents are invalid.
log::error!("Failed to load custom client from custom.txt.");
drop(clear_staging_on_exit);
// ERROR_INVALID_DATA
return Ok(false);
}
} else {
log::info!("No custom client files found in staging directory.");
}
} else {
log::info!(
"Custom client staging directory {:?} does not exist.",
custom_client_staging_dir
);
}
Ok(true)
}
pub fn get_license_from_exe_name() -> ResultType<CustomServer> {
let mut exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
// if defined portable appname entry, replace original executable name with it.
@@ -1898,12 +2121,48 @@ unsafe fn set_default_dll_directories() -> bool {
true
}
fn get_custom_icon(exe: &str) -> Option<String> {
if crate::is_custom_client() {
if let Some(p) = PathBuf::from(exe).parent() {
let alter_icon_path = p.join("data\\flutter_assets\\assets\\icon.ico");
if alter_icon_path.exists() {
// Verify that the icon is not a symlink for security
if let Ok(metadata) = std::fs::symlink_metadata(&alter_icon_path) {
if metadata.is_symlink() {
log::warn!(
"Custom icon at {:?} is a symlink, refusing to use it.",
alter_icon_path
);
return None;
}
if metadata.is_file() {
return Some(alter_icon_path.to_string_lossy().to_string());
}
}
}
}
}
None
}
#[inline]
fn get_shortcut_icon_location(exe: &str) -> String {
if exe.is_empty() {
return "".to_owned();
}
get_custom_icon(exe)
.map(|p| format!("oLink.IconLocation = \"{}\"", p))
.unwrap_or_default()
}
pub fn create_shortcut(id: &str) -> ResultType<()> {
let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
// https://github.com/rustdesk/rustdesk/issues/13735
// Replace ':' with '_' for filename since ':' is not allowed in Windows filenames
// https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384
let filename = id.replace(':', "_");
let shortcut_icon_location = get_shortcut_icon_location(&exe);
let shortcut = write_cmds(
format!(
"
@@ -1914,6 +2173,7 @@ sLinkFile = objFSO.BuildPath(strDesktop, \"{filename}.lnk\")
Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = \"{exe}\"
oLink.Arguments = \"--connect {id}\"
{shortcut_icon_location}
oLink.Save
"
),
@@ -2719,6 +2979,44 @@ if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
std::process::exit(0);
}
/// Calculate the total size of a directory in KB
/// Does not follow symlinks to prevent directory traversal attacks.
fn get_directory_size_kb(path: &str) -> u64 {
let mut total_size = 0u64;
let mut stack = vec![PathBuf::from(path)];
while let Some(current_path) = stack.pop() {
let entries = match std::fs::read_dir(&current_path) {
Ok(entries) => entries,
Err(_) => continue,
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let metadata = match std::fs::symlink_metadata(entry.path()) {
Ok(metadata) => metadata,
Err(_) => continue,
};
if metadata.is_symlink() {
continue;
}
if metadata.is_dir() {
stack.push(entry.path());
} else {
total_size = total_size.saturating_add(metadata.len());
}
}
}
total_size / 1024
}
pub fn update_me(debug: bool) -> ResultType<()> {
let app_name = crate::get_app_name();
let src_exe = std::env::current_exe()?.to_string_lossy().to_string();
@@ -2759,12 +3057,35 @@ pub fn update_me(debug: bool) -> ResultType<()> {
if versions.len() > 2 {
version_build = versions[2];
}
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
let size = meta.len() / 1024;
let version = crate::VERSION.replace("-", ".");
let size = get_directory_size_kb(&path);
let build_date = crate::BUILD_DATE;
let display_icon = get_custom_icon(&exe).unwrap_or(exe.to_string());
let reg_cmd = format!(
"
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
let is_msi = is_msi_installed().ok();
fn get_reg_cmd(
subkey: &str,
is_msi: Option<bool>,
display_icon: &str,
version: &str,
build_date: &str,
version_major: &str,
version_minor: &str,
version_build: &str,
size: u64,
) -> String {
let reg_display_icon = if is_msi.unwrap_or(false) {
"".to_string()
} else {
format!(
"reg add {} /f /v DisplayIcon /t REG_SZ /d \"{}\"",
subkey, display_icon
)
};
format!(
"
{reg_display_icon}
reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\"
@@ -2772,10 +3093,39 @@ reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major}
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor}
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
",
version = crate::VERSION.replace("-", "."),
build_date = crate::BUILD_DATE,
);
"
)
}
let reg_cmd = {
let reg_cmd_main = get_reg_cmd(
&subkey,
is_msi,
&display_icon,
&version,
&build_date,
&version_major,
&version_minor,
&version_build,
size,
);
let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) {
get_reg_cmd(
&reg_msi_key,
is_msi,
&display_icon,
&version,
&build_date,
&version_major,
&version_minor,
&version_build,
size,
)
} else {
"".to_owned()
};
format!("{}{}", reg_cmd_main, reg_cmd_msi)
};
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
let restore_service_cmd = if is_service_running {
@@ -2815,6 +3165,8 @@ sc stop {app_name}
taskkill /F /IM {app_name}.exe{filter}
{reg_cmd}
{copy_exe}
{rename_exe}
{remove_meta_toml}
{restore_service_cmd}
{uninstall_printer_cmd}
{install_printer_cmd}
@@ -2822,43 +3174,106 @@ taskkill /F /IM {app_name}.exe{filter}
",
app_name = app_name,
copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?,
rename_exe = rename_exe_cmd(&src_exe, &path)?,
remove_meta_toml = remove_meta_toml_cmd(is_msi.unwrap_or(true), &path),
sleep = if debug { "timeout 300" } else { "" },
);
let _restore_session_guard = crate::common::SimpleCallOnReturn {
b: true,
f: Box::new(move || {
let is_root = is_root();
if tray_sessions.is_empty() {
log::info!("No tray process found.");
} else {
log::info!(
"Try to restore the tray process..., sessions: {:?}",
&tray_sessions
);
// When not running as root, only spawn once since run_exe_direct
// doesn't target specific sessions.
let mut spawned_non_root_tray = false;
for s in tray_sessions.clone().into_iter() {
if s != 0 {
// We need to check if is_root here because if `update_me()` is called from
// the main window running with administrator permission,
// `run_exe_in_session()` will fail with error 1314 ("A required privilege is
// not held by the client").
//
// This issue primarily affects the MSI-installed version running in Administrator
// session during testing, but we check permissions here to be safe.
if is_root {
allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true));
} else if !spawned_non_root_tray {
// Only spawn once for non-root since run_exe_direct doesn't take session parameter
allow_err!(run_exe_direct(&exe, vec!["--tray"], false));
spawned_non_root_tray = true;
}
}
}
}
if main_window_sessions.is_empty() {
log::info!("No main window process found.");
} else {
log::info!("Try to restore the main window process...");
std::thread::sleep(std::time::Duration::from_millis(2000));
// When not running as root, only spawn once since run_exe_direct
// doesn't target specific sessions.
let mut spawned_non_root_main = false;
for s in main_window_sessions.clone().into_iter() {
if s != 0 {
if is_root {
allow_err!(run_exe_in_session(&exe, vec![], s, true));
} else if !spawned_non_root_main {
// Only spawn once for non-root since run_exe_direct doesn't take session parameter
allow_err!(run_exe_direct(&exe, vec![], false));
spawned_non_root_main = true;
}
}
}
}
std::thread::sleep(std::time::Duration::from_millis(300));
}),
};
run_cmds(cmds, debug, "update")?;
std::thread::sleep(std::time::Duration::from_millis(2000));
if tray_sessions.is_empty() {
log::info!("No tray process found.");
} else {
log::info!("Try to restore the tray process...");
log::info!(
"Try to restore the tray process..., sessions: {:?}",
&tray_sessions
);
for s in tray_sessions {
if s != 0 {
allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true));
}
}
}
if main_window_sessions.is_empty() {
log::info!("No main window process found.");
} else {
log::info!("Try to restore the main window process...");
std::thread::sleep(std::time::Duration::from_millis(2000));
for s in main_window_sessions {
if s != 0 {
allow_err!(run_exe_in_session(&exe, vec![], s, true));
}
}
}
std::thread::sleep(std::time::Duration::from_millis(300));
log::info!("Update completed.");
Ok(())
}
fn get_reg_msi_key(subkey: &str, is_msi: Option<bool>) -> Option<String> {
// Only proceed if it's a custom client and MSI is installed.
// `is_msi.unwrap_or(true)` is intentional: subsequent code validates the registry,
// hence no early return is required upon MSI detection failure.
if !(crate::common::is_custom_client() && is_msi.unwrap_or(true)) {
return None;
}
// Get the uninstall string from registry
let uninstall_string = get_reg_of(subkey, "UninstallString");
if uninstall_string.is_empty() {
return None;
}
// Find the product code (GUID) in the uninstall string
// Handle both quoted and unquoted GUIDs: /X {GUID} or /X "{GUID}"
let start = uninstall_string.rfind('{')?;
let end = uninstall_string.rfind('}')?;
if start >= end {
return None;
}
let product_code = &uninstall_string[start..=end];
// Build the MSI registry key path
let pos = subkey.rfind('\\')?;
let reg_msi_key = format!("{}{}", &subkey[..=pos], product_code);
Some(reg_msi_key)
}
// Double confirm the process name
fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
let name = name.to_lowercase();
@@ -2880,6 +3295,109 @@ fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
Ok(())
}
pub fn handle_custom_client_staging_dir_before_update(
custom_client_staging_dir: &PathBuf,
) -> ResultType<()> {
let Some(current_exe_dir) = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
else {
bail!("Failed to get current exe directory");
};
// Clean up existing staging directory
if custom_client_staging_dir.exists() {
log::debug!(
"Removing existing custom client staging directory: {:?}",
custom_client_staging_dir
);
if let Err(e) = remove_custom_client_staging_dir(custom_client_staging_dir) {
bail!(
"Failed to remove existing custom client staging directory {:?}: {}",
custom_client_staging_dir,
e
);
}
}
let src_path = current_exe_dir.join("custom.txt");
if src_path.exists() {
// Verify that custom.txt is not a symlink before copying
let metadata = match std::fs::symlink_metadata(&src_path) {
Ok(m) => m,
Err(e) => {
bail!(
"Failed to read metadata for custom.txt at {:?}: {}",
src_path,
e
);
}
};
if metadata.is_symlink() {
allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir));
bail!(
"custom.txt at {:?} is a symlink, refusing to stage for security reasons.",
src_path
);
}
if metadata.is_file() {
if !custom_client_staging_dir.exists() {
if let Err(e) = std::fs::create_dir_all(custom_client_staging_dir) {
bail!("Failed to create parent directory {:?} when staging custom client files: {}", custom_client_staging_dir, e);
}
}
let dst_path = custom_client_staging_dir.join("custom.txt");
if let Err(e) = std::fs::copy(&src_path, &dst_path) {
allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir));
bail!(
"Failed to copy custom txt from {:?} to {:?}: {}",
src_path,
dst_path,
e
);
}
} else {
log::warn!(
"custom.txt at {:?} is not a regular file, skipping.",
src_path
);
}
} else {
log::info!("No custom txt found to stage for update.");
}
Ok(())
}
// Used for auto update and manual update in the main window.
pub fn update_to(file: &str) -> ResultType<()> {
if file.ends_with(".exe") {
let custom_client_staging_dir = get_custom_client_staging_dir();
if crate::is_custom_client() {
handle_custom_client_staging_dir_before_update(&custom_client_staging_dir)?;
} else {
// Clean up any residual staging directory from previous custom client
allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir));
}
if !run_uac(file, "--update")? {
bail!(
"Failed to run the update exe with UAC, error: {:?}",
std::io::Error::last_os_error()
);
}
} else if file.ends_with(".msi") {
if let Err(e) = update_me_msi(file, false) {
bail!("Failed to run the update msi: {}", e);
}
} else {
// unreachable!()
bail!("Unsupported update file format: {}", file);
}
Ok(())
}
// Don't launch tray app when running with `\qn`.
// 1. Because `/qn` requires administrator permission and the tray app should be launched with user permission.
// Or launching the main window from the tray app will cause the main window to be launched with administrator permission.
@@ -2900,6 +3418,7 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
}
pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType<String> {
let shortcut_icon_location = get_shortcut_icon_location(exe);
Ok(write_cmds(
format!(
"
@@ -2909,6 +3428,7 @@ sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\"
Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = \"{exe}\"
oLink.Arguments = \"--tray\"
{shortcut_icon_location}
oLink.Save
",
app_name = crate::get_app_name(),
@@ -2971,6 +3491,44 @@ fn run_after_run_cmds(silent: bool) {
std::thread::sleep(std::time::Duration::from_millis(300));
}
#[inline]
pub fn try_remove_temp_update_files() {
let temp_dir = std::env::temp_dir();
let Ok(entries) = std::fs::read_dir(&temp_dir) else {
log::debug!("Failed to read temp directory: {:?}", temp_dir);
return;
};
let one_hour = std::time::Duration::from_secs(60 * 60);
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
// Match files like rustdesk-*.msi or rustdesk-*.exe
if file_name.starts_with("rustdesk-")
&& (file_name.ends_with(".msi") || file_name.ends_with(".exe"))
{
// Skip files modified within the last hour to avoid deleting files being downloaded
if let Ok(metadata) = std::fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
if let Ok(elapsed) = modified.elapsed() {
if elapsed < one_hour {
continue;
}
}
}
}
if let Err(e) = std::fs::remove_file(&path) {
log::debug!("Failed to remove temp update file {:?}: {}", path, e);
} else {
log::info!("Removed temp update file: {:?}", path);
}
}
}
}
}
}
#[inline]
pub fn try_kill_broker() {
allow_err!(std::process::Command::new("cmd")
@@ -3146,7 +3704,8 @@ pub fn is_x64() -> bool {
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
log::info!("try kill rustdesk main window process");
let app_name = crate::get_app_name().to_lowercase();
log::info!("try kill main window process");
use hbb_common::sysinfo::System;
let mut sys = System::new();
sys.refresh_processes();
@@ -3155,7 +3714,6 @@ pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> {
.map(|x| x.user_id())
.unwrap_or_default();
let my_pid = std::process::id();
let app_name = crate::get_app_name().to_lowercase();
if app_name.is_empty() {
bail!("app name is empty");
}

View File

@@ -54,7 +54,7 @@ pub async fn listen(
remote_host: String,
remote_port: i32,
) -> ResultType<()> {
let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?;
let listener = tcp::new_listener(format!("127.0.0.1:{}", port), true).await?;
let addr = listener.local_addr()?;
log::info!("listening on port {:?}", addr);
let is_rdp = port == 0;

View File

@@ -66,7 +66,7 @@ impl RendezvousMediator {
}
crate::hbbs_http::sync::start();
#[cfg(target_os = "windows")]
if crate::platform::is_installed() && crate::is_server() && !crate::is_custom_client() {
if crate::platform::is_installed() && crate::is_server() {
crate::updater::start_auto_update();
}
check_zombie();

View File

@@ -560,7 +560,9 @@ impl Connection {
match data {
ipc::Data::Authorize => {
conn.require_2fa.take();
conn.send_logon_response().await;
if !conn.send_logon_response_and_keep_alive().await {
break;
}
if conn.port_forward_socket.is_some() {
break;
}
@@ -1338,9 +1340,66 @@ impl Connection {
crate::post_request(url, v.to_string(), "").await
}
async fn send_logon_response(&mut self) {
fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) {
let mut is_rdp = false;
if pf.host == "RDP" && pf.port == 0 {
pf.host = "localhost".to_owned();
pf.port = 3389;
is_rdp = true;
}
if pf.host.is_empty() {
pf.host = "localhost".to_owned();
}
(format!("{}:{}", pf.host, pf.port), is_rdp)
}
async fn connect_port_forward_if_needed(&mut self) -> bool {
if self.port_forward_socket.is_some() {
return true;
}
let Some(login_request::Union::PortForward(pf)) = self.lr.union.as_ref() else {
return true;
};
let mut pf = pf.clone();
let (mut addr, is_rdp) = Self::normalize_port_forward_target(&mut pf);
self.port_forward_address = addr.clone();
match timeout(3000, TcpStream::connect(&addr)).await {
Ok(Ok(sock)) => {
self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new()));
true
}
Ok(Err(e)) => {
log::warn!("Port forward connect failed for {}: {}", addr, e);
if is_rdp {
addr = "RDP".to_owned();
}
self.send_login_error(format!(
"Failed to access remote {}. Please make sure it is reachable/open.",
addr
))
.await;
false
}
Err(e) => {
log::warn!("Port forward connect timed out for {}: {}", addr, e);
if is_rdp {
addr = "RDP".to_owned();
}
self.send_login_error(format!(
"Failed to access remote {}. Please make sure it is reachable/open.",
addr
))
.await;
false
}
}
}
// Returns whether this connection should be kept alive.
// `true` does not necessarily mean authorization succeeded (e.g. REQUIRE_2FA case).
async fn send_logon_response_and_keep_alive(&mut self) -> bool {
if self.authorized {
return;
return true;
}
if self.require_2fa.is_some() && !self.is_recent_session(true) && !self.from_switch {
self.require_2fa.as_ref().map(|totp| {
@@ -1371,7 +1430,11 @@ impl Connection {
}
});
self.send_login_error(crate::client::REQUIRE_2FA).await;
return;
// Keep the connection alive so the client can continue with 2FA.
return true;
}
if !self.connect_port_forward_if_needed().await {
return false;
}
self.authorized = true;
let (conn_type, auth_conn_type) = if self.file_transfer.is_some() {
@@ -1494,7 +1557,7 @@ impl Connection {
res.set_peer_info(pi);
msg_out.set_login_response(res);
self.send(msg_out).await;
return;
return true;
}
#[cfg(target_os = "linux")]
if self.is_remote() {
@@ -1517,7 +1580,7 @@ impl Connection {
let mut msg_out = Message::new();
msg_out.set_login_response(res);
self.send(msg_out).await;
return;
return true;
}
}
#[allow(unused_mut)]
@@ -1671,6 +1734,7 @@ impl Connection {
self.try_sub_monitor_services();
}
}
true
}
fn try_sub_camera_displays(&mut self) {
@@ -1813,6 +1877,7 @@ impl Connection {
port_forward: self.port_forward_address.clone(),
peer_id,
name,
avatar: self.lr.avatar.clone(),
authorized,
keyboard: self.keyboard,
clipboard: self.clipboard,
@@ -2178,33 +2243,8 @@ impl Connection {
sleep(1.).await;
return false;
}
let mut is_rdp = false;
if pf.host == "RDP" && pf.port == 0 {
pf.host = "localhost".to_owned();
pf.port = 3389;
is_rdp = true;
}
if pf.host.is_empty() {
pf.host = "localhost".to_owned();
}
let mut addr = format!("{}:{}", pf.host, pf.port);
self.port_forward_address = addr.clone();
match timeout(3000, TcpStream::connect(&addr)).await {
Ok(Ok(sock)) => {
self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new()));
}
_ => {
if is_rdp {
addr = "RDP".to_owned();
}
self.send_login_error(format!(
"Failed to access remote {}, please make sure if it is open",
addr
))
.await;
return false;
}
}
let (addr, _is_rdp) = Self::normalize_port_forward_target(&mut pf);
self.port_forward_address = addr;
}
_ => {
if !self.check_privacy_mode_on().await {
@@ -2232,11 +2272,10 @@ impl Connection {
// https://github.com/rustdesk/rustdesk-server-pro/discussions/646
// `is_logon` is used to check login with `OPTION_ALLOW_LOGON_SCREEN_PASSWORD` == "Y".
// `is_logon_ui()` is used on Windows, because there's no good way to detect `is_locked()`.
// Detecting `is_logon_ui()` (if `LogonUI.exe` running) is a workaround.
// `is_logon_ui()` is a fallback for logon UI detection on Windows.
#[cfg(target_os = "windows")]
let is_logon = || {
crate::platform::is_prelogin() || {
crate::platform::is_prelogin() || crate::platform::is_locked() || {
match crate::platform::is_logon_ui() {
Ok(result) => result,
Err(e) => {
@@ -2275,7 +2314,9 @@ impl Connection {
if err_msg.is_empty() {
#[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await;
self.send_logon_response().await;
if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized);
} else {
self.send_login_error(err_msg).await;
@@ -2311,7 +2352,9 @@ impl Connection {
if err_msg.is_empty() {
#[cfg(target_os = "linux")]
self.linux_headless_handle.wait_desktop_cm_ready().await;
self.send_logon_response().await;
if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(lr.my_id, lr.my_name, self.authorized);
} else {
self.send_login_error(err_msg).await;
@@ -2329,7 +2372,9 @@ impl Connection {
self.update_failure(failure, true, 1);
self.require_2fa.take();
raii::AuthedConnID::set_session_2fa(self.session_key());
self.send_logon_response().await;
if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(
self.lr.my_id.to_owned(),
self.lr.my_name.to_owned(),
@@ -2380,7 +2425,9 @@ impl Connection {
if let Some((_instant, uuid_old)) = uuid_old {
if uuid == uuid_old {
self.from_switch = true;
self.send_logon_response().await;
if !self.send_logon_response_and_keep_alive().await {
return false;
}
self.try_start_cm(
lr.my_id.clone(),
lr.my_name.clone(),
@@ -5346,9 +5393,8 @@ mod raii {
}
pub fn check_wake_lock_on_setting_changed() {
let current = config::Config::get_bool_option(
keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS,
);
let current =
config::Config::get_bool_option(keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS);
let cached = *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap();
if cached != Some(current) {
Self::check_wake_lock();

View File

@@ -809,7 +809,7 @@ fn record_key_is_control_key(record_key: u64) -> bool {
#[inline]
fn record_key_is_chr(record_key: u64) -> bool {
record_key < KEY_CHAR_START
record_key >= KEY_CHAR_START
}
#[inline]
@@ -1513,6 +1513,27 @@ fn get_control_key_value(key_event: &KeyEvent) -> i32 {
}
}
#[inline]
fn has_hotkey_modifiers(key_event: &KeyEvent) -> bool {
key_event.modifiers.iter().any(|ck| {
let v = ck.value();
v == ControlKey::Control.value()
|| v == ControlKey::RControl.value()
|| v == ControlKey::Meta.value()
|| v == ControlKey::RWin.value()
|| {
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
v == ControlKey::Alt.value() || v == ControlKey::RAlt.value()
}
#[cfg(target_os = "macos")]
{
false
}
}
})
}
fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) {
let ck_value = get_control_key_value(key_event);
fix_modifiers(&key_event.modifiers[..], en, ck_value);
@@ -1572,7 +1593,7 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en)
}
fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
// On Wayland with uinput mode, use clipboard for character input
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
@@ -1587,6 +1608,16 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
}
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
if !_hotkey {
if down {
if let Ok(chr) = char::try_from(chr) {
en.key_sequence(&chr.to_string());
}
}
return;
}
let key = char_value_to_key(chr);
if down {
@@ -1856,7 +1887,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) {
let record_key = chr as u64 + KEY_CHAR_START;
record_pressed_key(KeysDown::EnigoKey(record_key), down);
process_chr(&mut en, chr, down)
process_chr(&mut en, chr, down, has_hotkey_modifiers(evt))
}
Some(key_event::Union::Unicode(chr)) => {
// Same as Chr: release Shift for Unicode input

View File

@@ -30,8 +30,54 @@ const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal
const MAX_BUFFER_LINES: usize = 10000;
const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services
const SERVICE_IDLE_TIMEOUT: Duration = Duration::from_secs(3600); // 1 hour idle timeout
const CHANNEL_BUFFER_SIZE: usize = 100; // Number of messages to buffer in channel
const CHANNEL_BUFFER_SIZE: usize = 500; // Channel buffer size. Max per-message size ~4KB (reader buffer), so worst case ~500*4KB ≈ 2MB/terminal. Increased from 100 to reduce data loss during disconnects.
const COMPRESS_THRESHOLD: usize = 512; // Compress terminal data larger than this
// Default max bytes for reconnection buffer replay.
const DEFAULT_RECONNECT_BUFFER_BYTES: usize = 8 * 1024;
const MAX_SIGWINCH_PHASE_ATTEMPTS: u8 = 3; // Max attempts per SIGWINCH phase before giving up
/// Two-phase SIGWINCH trigger for TUI app redraw on reconnection.
///
/// Why two phases? A single resize-then-restore done back-to-back is too fast:
/// by the time the TUI app handles the asynchronous SIGWINCH signal and calls
/// `ioctl(TIOCGWINSZ)`, the PTY size has already been restored to the original.
/// ncurses sees no size change and skips the full redraw.
///
/// Splitting across two `read_outputs()` calls (~30ms apart) ensures the app
/// sees a real size change on each SIGWINCH, forcing a complete redraw.
#[derive(Debug, Clone)]
enum SigwinchPhase {
/// No SIGWINCH needed.
Idle,
/// Phase 1: Resize PTY to temp dimensions (rows±1). The app handles SIGWINCH
/// and redraws at the temporary size.
TempResize { retries: u8 },
/// Phase 2: Restore PTY to correct dimensions. The app handles SIGWINCH,
/// detects the size change, and performs a full redraw at the correct size.
Restore { retries: u8 },
}
/// Which resize to perform in the two-phase SIGWINCH sequence.
enum SigwinchAction {
/// Phase 1: resize to temp dimensions (rows±1) to trigger SIGWINCH with a visible size change.
TempResize,
/// Phase 2: restore to correct dimensions to trigger SIGWINCH and force full redraw.
Restore,
}
/// Session state machine for terminal streaming.
#[derive(Debug)]
enum SessionState {
/// Session is closed, not streaming data to client.
Closed,
/// Session is active, streaming data to client.
/// pending_buffer: historical buffer to send before real-time data (set on reconnection).
/// sigwinch: two-phase SIGWINCH trigger state for TUI app redraw.
Active {
pending_buffer: Option<Vec<u8>>,
sigwinch: SigwinchPhase,
},
}
lazy_static::lazy_static! {
// Global registry of persistent terminal services indexed by service_id
@@ -433,22 +479,103 @@ impl OutputBuffer {
}
fn get_recent(&self, max_bytes: usize) -> Vec<u8> {
let mut result = Vec::new();
if max_bytes == 0 {
return Vec::new();
}
let mut chunks: Vec<&[u8]> = Vec::new();
let mut size = 0;
// Get recent lines up to max_bytes
// Collect whole chunks from newest to oldest, preserving chronological continuity.
// If the newest chunk alone exceeds max_bytes, take its tail (truncation may split
// an ANSI escape, but the terminal will self-correct on subsequent output).
for line in self.lines.iter().rev() {
if size + line.len() > max_bytes {
if size == 0 && line.len() > max_bytes {
// Single oversized chunk: take the tail to preserve the most recent content.
// Align offset forward to a UTF-8 char boundary so that downstream
// clients (e.g. Dart) that decode the payload as UTF-8 text don't
// encounter split code points. The protobuf bytes field itself allows
// arbitrary bytes; this is a best-effort mitigation for client-side decoding.
let mut offset = line.len() - max_bytes;
// Skip at most 3 continuation bytes (UTF-8 max 4-byte sequence).
// Prevents runaway skipping on non-UTF-8 binary data.
let mut skipped = 0u8;
while skipped < 3
&& offset < line.len()
&& (line[offset] & 0b1100_0000) == 0b1000_0000
{
offset += 1;
skipped += 1;
}
// If we skipped past all remaining bytes (degenerate data), drop the
// chunk entirely rather than emitting a slice that decodes poorly on the client.
if offset < line.len() {
chunks.push(&line[offset..]);
size = line.len() - offset;
}
}
break;
}
size += line.len();
result.splice(0..0, line.iter().cloned());
chunks.push(line);
}
// Reverse to restore chronological order and concatenate
chunks.reverse();
let mut result = Vec::with_capacity(size);
for chunk in chunks {
result.extend_from_slice(chunk);
}
result
}
}
/// Try to send data through the output channel with rate-limited drop logging.
/// Returns `true` if the caller should break out of the read loop (channel disconnected).
fn try_send_output(
output_tx: &mpsc::SyncSender<Vec<u8>>,
data: Vec<u8>,
terminal_id: i32,
label: &str,
drop_count: &mut u64,
last_drop_warn: &mut Instant,
) -> bool {
match output_tx.try_send(data) {
Ok(_) => {
if *drop_count > 0 {
log::trace!(
"Terminal {}{} output channel recovered, dropped {} chunks since last report",
terminal_id,
label,
*drop_count
);
*drop_count = 0;
}
false
}
Err(mpsc::TrySendError::Full(_)) => {
*drop_count += 1;
if last_drop_warn.elapsed() >= Duration::from_secs(5) {
log::trace!(
"Terminal {}{} output channel full, dropped {} chunks in last {:?}",
terminal_id,
label,
*drop_count,
last_drop_warn.elapsed()
);
*drop_count = 0;
*last_drop_warn = Instant::now();
}
false
}
Err(mpsc::TrySendError::Disconnected(_)) => {
log::debug!("Terminal {}{} output channel disconnected", terminal_id, label);
true
}
}
}
pub struct TerminalSession {
pub created_at: Instant,
last_activity: Instant,
@@ -469,7 +596,8 @@ pub struct TerminalSession {
cols: u16,
// Track if we've already sent the closed message
closed_message_sent: bool,
is_opened: bool,
// Session state machine for reconnection handling
state: SessionState,
// Helper mode: PTY is managed by helper process, communication via message protocol
#[cfg(target_os = "windows")]
is_helper_mode: bool,
@@ -496,7 +624,7 @@ impl TerminalSession {
rows,
cols,
closed_message_sent: false,
is_opened: false,
state: SessionState::Closed,
#[cfg(target_os = "windows")]
is_helper_mode: false,
#[cfg(target_os = "windows")]
@@ -511,7 +639,7 @@ impl TerminalSession {
// This helper function is to ensure that the threads are joined before the child process is dropped.
// Though this is not strictly necessary on macOS.
fn stop(&mut self) {
self.is_opened = false;
self.state = SessionState::Closed;
self.exiting.store(true, Ordering::SeqCst);
// Drop the input channel to signal writer thread to exit
@@ -668,7 +796,9 @@ impl PersistentTerminalService {
(
session.rows,
session.cols,
session.output_buffer.get_recent(4096),
session
.output_buffer
.get_recent(DEFAULT_RECONNECT_BUFFER_BYTES),
)
})
}
@@ -683,7 +813,7 @@ impl PersistentTerminalService {
self.needs_session_sync = true;
for session in self.sessions.values() {
let mut session = session.lock().unwrap();
session.is_opened = false;
session.state = SessionState::Closed;
}
}
}
@@ -777,11 +907,55 @@ impl TerminalServiceProxy {
) -> Result<Option<TerminalResponse>> {
let mut response = TerminalResponse::new();
// When the client requests a terminal_id that doesn't exist but there are
// surviving persistent sessions, remap the lowest-ID session to the requested
// terminal_id. This handles the case where _nextTerminalId resets to 1 on
// reconnect but the server-side sessions have non-contiguous IDs (e.g. {2: htop}).
//
// The client's requested terminal_id may not match any surviving session ID
// (e.g. _nextTerminalId incremented beyond the surviving IDs). This remap is a
// one-time handle reassignment — only the first reconnect triggers it because
// needs_session_sync is cleared afterward. Remaining sessions are communicated
// back via `persistent_sessions` with their original server-side IDs.
if !service.sessions.contains_key(&open.terminal_id)
&& service.needs_session_sync
&& !service.sessions.is_empty()
{
if let Some(&lowest_id) = service.sessions.keys().min() {
log::info!(
"Remapping persistent session {} -> {} for reconnection",
lowest_id,
open.terminal_id
);
if let Some(session_arc) = service.sessions.remove(&lowest_id) {
service.sessions.insert(open.terminal_id, session_arc);
}
}
}
// Check if terminal already exists
if let Some(session_arc) = service.sessions.get(&open.terminal_id) {
// Reconnect to existing terminal
let mut session = session_arc.lock().unwrap();
session.is_opened = true;
// Directly enter Active state with pending buffer for immediate streaming.
// Historical buffer is sent first by read_outputs(), then real-time data follows.
// No overlap: pending_buffer comes from output_buffer (pre-disconnect history),
// while received_data in read_outputs() comes from the channel (post-reconnect).
// During disconnect, the run loop (sp.ok()) exits so read_outputs() stops being
// called; output_buffer is not updated, and channel data may be lost if it fills up.
let buffer = session
.output_buffer
.get_recent(DEFAULT_RECONNECT_BUFFER_BYTES);
let has_pending = !buffer.is_empty();
session.state = SessionState::Active {
pending_buffer: if has_pending { Some(buffer) } else { None },
// Always trigger two-phase SIGWINCH on reconnect to force TUI app redraw,
// regardless of whether there's pending buffer data. This avoids edge cases
// where buffer is empty but a TUI app (top/htop) still needs a full redraw.
sigwinch: SigwinchPhase::TempResize {
retries: MAX_SIGWINCH_PHASE_ATTEMPTS,
},
};
let mut opened = TerminalOpened::new();
opened.terminal_id = open.terminal_id;
opened.success = true;
@@ -803,13 +977,6 @@ impl TerminalServiceProxy {
}
response.set_opened(opened);
// Send buffered output
let buffer = session.output_buffer.get_recent(4096);
if !buffer.is_empty() {
// We'll need to send this separately or extend the protocol
// For now, just acknowledge the reconnection
}
return Ok(Some(response));
}
@@ -824,7 +991,7 @@ impl TerminalServiceProxy {
// Create new terminal session
log::info!(
"Creating new terminal {} for service: {}",
"Creating new terminal {} for service {}",
open.terminal_id,
service.service_id
);
@@ -919,6 +1086,9 @@ impl TerminalServiceProxy {
let reader_thread = thread::spawn(move || {
let mut reader = reader;
let mut buf = vec![0u8; 4096];
let mut drop_count: u64 = 0;
// Initialize to > 5s ago so the first drop triggers a warning immediately.
let mut last_drop_warn = Instant::now() - Duration::from_secs(6);
loop {
match reader.read(&mut buf) {
Ok(0) => {
@@ -932,19 +1102,22 @@ impl TerminalServiceProxy {
break;
}
let data = buf[..n].to_vec();
// Try to send, if channel is full, drop the data
match output_tx.try_send(data) {
Ok(_) => {}
Err(mpsc::TrySendError::Full(_)) => {
log::debug!(
"Terminal {} output channel full, dropping data",
terminal_id
);
}
Err(mpsc::TrySendError::Disconnected(_)) => {
log::debug!("Terminal {} output channel disconnected", terminal_id);
break;
}
// Use try_send to avoid blocking the reader thread when channel is full.
// During disconnect, the run loop (sp.ok()) stops and read_outputs() is
// no longer called, so the channel won't be drained. Blocking send would
// deadlock the reader thread in that case.
// Note: data produced during disconnect may be lost if channel fills up,
// since output_buffer is only updated in read_outputs(). The buffer will
// contain history from before the disconnect, not data produced after it.
if try_send_output(
&output_tx,
data,
terminal_id,
"",
&mut drop_count,
&mut last_drop_warn,
) {
break;
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
@@ -970,7 +1143,10 @@ impl TerminalServiceProxy {
session.output_rx = Some(output_rx);
session.reader_thread = Some(reader_thread);
session.writer_thread = Some(writer_thread);
session.is_opened = true;
session.state = SessionState::Active {
pending_buffer: None,
sigwinch: SigwinchPhase::Idle,
};
let mut opened = TerminalOpened::new();
opened.terminal_id = open.terminal_id;
@@ -1132,6 +1308,9 @@ impl TerminalServiceProxy {
let terminal_id = open.terminal_id;
let reader_thread = thread::spawn(move || {
let mut buf = vec![0u8; 4096];
let mut drop_count: u64 = 0;
// Initialize to > 5s ago so the first drop triggers a warning immediately.
let mut last_drop_warn = Instant::now() - Duration::from_secs(6);
loop {
match output_pipe.read(&mut buf) {
Ok(0) => {
@@ -1144,18 +1323,16 @@ impl TerminalServiceProxy {
break;
}
let data = buf[..n].to_vec();
match output_tx.try_send(data) {
Ok(_) => {}
Err(mpsc::TrySendError::Full(_)) => {
log::debug!(
"Terminal {} output channel full, dropping data",
terminal_id
);
}
Err(mpsc::TrySendError::Disconnected(_)) => {
log::debug!("Terminal {} output channel disconnected", terminal_id);
break;
}
// Use try_send to avoid blocking the reader thread (same as direct PTY mode)
if try_send_output(
&output_tx,
data,
terminal_id,
" (helper)",
&mut drop_count,
&mut last_drop_warn,
) {
break;
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
@@ -1185,7 +1362,10 @@ impl TerminalServiceProxy {
session.output_rx = Some(output_rx);
session.reader_thread = Some(reader_thread);
session.writer_thread = Some(writer_thread);
session.is_opened = true;
session.state = SessionState::Active {
pending_buffer: None,
sigwinch: SigwinchPhase::Idle,
};
session.is_helper_mode = true;
session.helper_process_handle = Some(SendableHandle::new(helper_raw_handle));
@@ -1227,6 +1407,11 @@ impl TerminalServiceProxy {
session.rows = resize.rows as u16;
session.cols = resize.cols as u16;
// Note: we do NOT clear the sigwinch phase here. The server-side two-phase
// SIGWINCH mechanism in read_outputs() is self-contained (temp resize → restore
// across two polling cycles), so client resize is purely a dimension sync and
// doesn't affect it.
// Windows: handle helper mode vs direct PTY mode
#[cfg(target_os = "windows")]
{
@@ -1332,6 +1517,116 @@ impl TerminalServiceProxy {
}
}
/// Perform a single PTY resize as part of the two-phase SIGWINCH sequence.
/// Returns true if the resize succeeded.
///
/// Takes individual field references to avoid borrowing the entire TerminalSession,
/// which would conflict with the mutable borrow of session.state in read_outputs().
fn do_sigwinch_resize(
terminal_id: i32,
rows: u16,
cols: u16,
pty_pair: &Option<portable_pty::PtyPair>,
input_tx: &Option<SyncSender<Vec<u8>>>,
_is_helper_mode: bool,
action: &SigwinchAction,
) -> bool {
// Skip if dimensions are not initialized (shouldn't happen on reconnect,
// but guard against it to avoid resizing to nonsensical values).
if rows == 0 || cols == 0 {
return false;
}
let target_rows = match action {
SigwinchAction::TempResize => {
// For very small terminals (≤2 rows), subtracting 1 would result in an unusable
// size (0 or 1 row), so we add 1 instead. Either direction triggers SIGWINCH.
if rows > 2 {
rows.saturating_sub(1)
} else {
rows.saturating_add(1)
}
}
SigwinchAction::Restore => rows,
};
let phase_name = match action {
SigwinchAction::TempResize => "temp resize",
SigwinchAction::Restore => "restore",
};
#[cfg(target_os = "windows")]
let use_helper = _is_helper_mode;
#[cfg(not(target_os = "windows"))]
let use_helper = false;
if use_helper {
#[cfg(target_os = "windows")]
{
let input_tx = match input_tx {
Some(tx) => tx,
None => return false,
};
let msg = encode_resize_message(target_rows, cols);
if let Err(e) = input_tx.try_send(msg) {
log::warn!(
"Terminal {} SIGWINCH {} via helper failed: {}",
terminal_id,
phase_name,
e
);
return false;
}
true
}
#[cfg(not(target_os = "windows"))]
{
let _ = (input_tx, phase_name);
false
}
} else if let Some(pty_pair) = pty_pair {
if let Err(e) = pty_pair.master.resize(PtySize {
rows: target_rows,
cols,
pixel_width: 0,
pixel_height: 0,
}) {
log::warn!(
"Terminal {} SIGWINCH {} failed: {}",
terminal_id,
phase_name,
e
);
return false;
}
true
} else {
false
}
}
/// Helper to create a TerminalResponse with optional compression.
fn create_terminal_data_response(terminal_id: i32, data: Vec<u8>) -> TerminalResponse {
let mut response = TerminalResponse::new();
let mut terminal_data = TerminalData::new();
terminal_data.terminal_id = terminal_id;
if data.len() > COMPRESS_THRESHOLD {
let compressed = compress::compress(&data);
if compressed.len() < data.len() {
terminal_data.data = bytes::Bytes::from(compressed);
terminal_data.compressed = true;
} else {
terminal_data.data = bytes::Bytes::from(data);
}
} else {
terminal_data.data = bytes::Bytes::from(data);
}
response.set_data(terminal_data);
response
}
pub fn read_outputs(&self) -> Vec<TerminalResponse> {
let service = match get_service(&self.service_id) {
Some(s) => s,
@@ -1373,12 +1668,11 @@ impl TerminalServiceProxy {
closed_terminals.push(terminal_id);
}
if !session.is_opened {
// Skip the session if it is not opened.
continue;
}
// Read from output channel
// Always drain the output channel regardless of session state.
// When Active: data is sent to client. When Closed (within the same
// connection): data is buffered in output_buffer for reconnection replay.
// Note: during actual disconnect, the run loop exits and read_outputs()
// is not called, so channel data produced after disconnect may be lost.
let mut has_activity = false;
let mut received_data = Vec::new();
if let Some(output_rx) = &session.output_rx {
@@ -1389,37 +1683,111 @@ impl TerminalServiceProxy {
}
}
// Update buffer after reading
if has_activity {
session.update_activity();
}
// Update buffer (always buffer for reconnection support)
for data in &received_data {
session.output_buffer.append(data);
}
// Process received data for responses
for data in received_data {
let mut response = TerminalResponse::new();
let mut terminal_data = TerminalData::new();
terminal_data.terminal_id = terminal_id;
// Skip sending responses if session is not Active.
// Data is already buffered above and will be sent on next reconnection.
// Use a scoped block to limit the mutable borrow of session.state,
// so we can immutably borrow other session fields afterwards.
let sigwinch_action = {
let (pending_buffer, sigwinch) = match &mut session.state {
SessionState::Active {
pending_buffer,
sigwinch,
} => (pending_buffer, sigwinch),
_ => continue,
};
// Compress data if it exceeds threshold
if data.len() > COMPRESS_THRESHOLD {
let compressed = compress::compress(&data);
if compressed.len() < data.len() {
terminal_data.data = bytes::Bytes::from(compressed);
terminal_data.compressed = true;
} else {
// Compression didn't help, send uncompressed
terminal_data.data = bytes::Bytes::from(data);
// Send pending buffer response first (set on reconnection in handle_open).
// This ensures historical buffer is sent before any real-time data.
if let Some(buffer) = pending_buffer.take() {
if !buffer.is_empty() {
responses
.push(Self::create_terminal_data_response(terminal_id, buffer));
}
} else {
terminal_data.data = bytes::Bytes::from(data);
}
response.set_data(terminal_data);
responses.push(response);
// Two-phase SIGWINCH: see SigwinchPhase doc comments for rationale.
// Each phase is a single PTY resize, spaced ~30ms apart by the polling
// interval, ensuring the TUI app sees a real size change on each signal.
match sigwinch {
SigwinchPhase::TempResize { retries } => {
if *retries == 0 {
log::warn!(
"Terminal {} SIGWINCH phase 1 (temp resize) failed after {} attempts, giving up",
terminal_id, MAX_SIGWINCH_PHASE_ATTEMPTS
);
*sigwinch = SigwinchPhase::Idle;
None
} else {
*retries -= 1;
Some(SigwinchAction::TempResize)
}
}
SigwinchPhase::Restore { retries } => {
if *retries == 0 {
log::warn!(
"Terminal {} SIGWINCH phase 2 (restore) failed after {} attempts, giving up",
terminal_id, MAX_SIGWINCH_PHASE_ATTEMPTS
);
*sigwinch = SigwinchPhase::Idle;
None
} else {
*retries -= 1;
Some(SigwinchAction::Restore)
}
}
SigwinchPhase::Idle => None,
}
};
// Execute SIGWINCH resize outside the mutable borrow scope of session.state.
if let Some(action) = sigwinch_action {
#[cfg(target_os = "windows")]
let is_helper = session.is_helper_mode;
#[cfg(not(target_os = "windows"))]
let is_helper = false;
let resize_ok = Self::do_sigwinch_resize(
terminal_id,
session.rows,
session.cols,
&session.pty_pair,
&session.input_tx,
is_helper,
&action,
);
if let SessionState::Active { sigwinch, .. } = &mut session.state {
match action {
SigwinchAction::TempResize => {
if resize_ok {
// Phase 1 succeeded — advance to phase 2 (restore).
*sigwinch = SigwinchPhase::Restore {
retries: MAX_SIGWINCH_PHASE_ATTEMPTS,
};
}
// If failed, retries already decremented; will retry phase 1.
}
SigwinchAction::Restore => {
if resize_ok {
// Phase 2 succeeded — SIGWINCH sequence complete.
*sigwinch = SigwinchPhase::Idle;
}
// If failed, retries already decremented; will retry phase 2.
}
}
}
}
if has_activity {
session.update_activity();
// Send real-time data after historical buffer
for data in received_data {
responses.push(Self::create_terminal_data_response(terminal_id, data));
}
}
}

View File

@@ -12,7 +12,11 @@
include "common.tis";
var p = view.parameters;
view.refresh = function() {
var draft_input = $(input);
var draft = draft_input ? (draft_input.value || "") : "";
$(body).content(<ChatBox msgs={p.msgs} callback={p.callback} />);
var next_input = $(input);
if (next_input) next_input.value = draft;
view.focus = $(input);
}
function self.closing() {

View File

@@ -57,6 +57,11 @@ div.icon {
font-weight: bold;
}
img.icon {
size: 96px;
border-radius: 8px;
}
div.id {
@ELLIPSIS;
color: color(green-blue);

View File

@@ -28,6 +28,7 @@ impl InvokeUiCM for SciterHandler {
client.port_forward.clone(),
client.peer_id.clone(),
client.name.clone(),
client.avatar.clone(),
client.authorized,
client.keyboard,
client.clipboard,

View File

@@ -42,9 +42,11 @@ class Body: Reactor.Component
return <div .content style="size:*">
<div .left-panel>
<div .icon-and-id>
{c.avatar ?
<img .icon src={c.avatar} /> :
<div .icon style={"background: " + string2RGB(c.name, 1)}>
{c.name[0].toUpperCase()}
</div>
</div>}
<div>
<div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
<div .id>({c.peer_id})</div>
@@ -366,7 +368,7 @@ function bring_to_top(idx=-1) {
}
}
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
stdout.println("new connection #" + id + ": " + peer_id);
var conn;
connections.map(function(c) {
@@ -385,6 +387,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin
conn = {
id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id,
port_forward: port_forward,
avatar: avatar,
name: name, authorized: authorized, time: new Date(), now: new Date(),
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
audio: audio, file: file, restart: restart, recording: recording,

View File

@@ -824,7 +824,9 @@ class UpdateMe: Reactor.Component {
return <div .install-me>
<div>{translate('Status')}</div>
<div>There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.</div>
<div #install-me.link>{translate('Click to ' + update_or_download)}</div>
{is_custom_client
? <div style="font-size: 1em; font-weight: normal; text-align: left; padding-top: 1em;">{translate('Enable \"Auto update\" or contact your administrator for the latest version.')}</div>
: <div #install-me.link>{translate('Click to ' + update_or_download)}</div>}
<div #download-percent style="display:hidden; padding-top: 1em;" />
</div>;
}
@@ -1449,6 +1451,9 @@ function set_local_user_info(user) {
if (user.display_name) {
user_info.display_name = user.display_name;
}
if (user.avatar) {
user_info.avatar = user.avatar;
}
if (user.status) {
user_info.status = user.status;
}

View File

@@ -142,6 +142,14 @@ function resetWheel() {
}
var INERTIA_ACCELERATION = 30;
var WHEEL_ACCEL_VELOCITY_THRESHOLD = 5000;
var WHEEL_ACCEL_DT_FAST = 0.04;
var WHEEL_ACCEL_DT_MEDIUM = 0.08;
var WHEEL_ACCEL_VALUE_FAST = 3;
var WHEEL_ACCEL_VALUE_MEDIUM = 2;
// Wheel burst acceleration (empirical tuning).
// Applies only on fast, non-smooth wheel bursts to keep single-step scroll unchanged.
// Sciter uses seconds for dt, so velocity is in delta/sec.
// not good, precision not enough to simulate acceleration effect,
// seems have to use pixel based rather line based delta
@@ -237,12 +245,28 @@ function handler.onMouse(evt)
// mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"];
mask = 3;
{
var (dx, dy) = evt.wheelDeltas;
if (dx > 0) dx = 1;
else if (dx < 0) dx = -1;
if (dy > 0) dy = 1;
else if (dy < 0) dy = -1;
if (Math.abs(dx) > Math.abs(dy)) {
var now = getTime();
var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
var (raw_dx, raw_dy) = evt.wheelDeltas;
var dx = 0;
var dy = 0;
var abs_dx = Math.abs(raw_dx);
var abs_dy = Math.abs(raw_dy);
var dominant = abs_dx > abs_dy ? abs_dx : abs_dy;
var is_smooth = dominant < 1;
var accel = 1;
if (!is_smooth && dt > 0 && (is_win || is_linux) && get_peer_platform() == "Mac OS") {
var velocity = dominant / dt;
if (velocity >= WHEEL_ACCEL_VELOCITY_THRESHOLD) {
if (dt < WHEEL_ACCEL_DT_FAST) accel = WHEEL_ACCEL_VALUE_FAST;
else if (dt < WHEEL_ACCEL_DT_MEDIUM) accel = WHEEL_ACCEL_VALUE_MEDIUM;
}
}
if (raw_dx > 0) dx = accel;
else if (raw_dx < 0) dx = -accel;
if (raw_dy > 0) dy = accel;
else if (raw_dy < 0) dy = -accel;
if (abs_dx > abs_dy) {
dy = 0;
} else {
dx = 0;
@@ -253,8 +277,6 @@ function handler.onMouse(evt)
wheel_delta_y = acc_wheel_delta_y.toInteger();
acc_wheel_delta_x -= wheel_delta_x;
acc_wheel_delta_y -= wheel_delta_y;
var now = getTime();
var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
if (dt > 0) {
var vx = dx / dt;
var vy = dy / dt;
@@ -297,11 +319,13 @@ function handler.onMouse(evt)
entered = true;
stdout.println("enter");
handler.enter(handler.get_keyboard_mode());
last_wheel_time = 0;
return keyboard_enabled;
case Event.MOUSE_LEAVE:
entered = false;
stdout.println("leave");
handler.leave(handler.get_keyboard_mode());
last_wheel_time = 0;
if (is_left_down && get_peer_platform() == "Android") {
is_left_down = false;
handler.send_mouse((1 << 3) | 2, 0, 0, evt.altKey,

View File

@@ -134,6 +134,7 @@ pub struct Client {
pub is_terminal: bool,
pub port_forward: String,
pub name: String,
pub avatar: String,
pub peer_id: String,
pub keyboard: bool,
pub clipboard: bool,
@@ -220,6 +221,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
port_forward: String,
peer_id: String,
name: String,
avatar: String,
authorized: bool,
keyboard: bool,
clipboard: bool,
@@ -240,6 +242,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
is_terminal,
port_forward,
name: name.clone(),
avatar,
peer_id: peer_id.clone(),
keyboard,
clipboard,
@@ -500,9 +503,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
}
Ok(Some(data)) => {
match data {
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
log::debug!("conn_id: {}", id);
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
self.conn_id = id;
#[cfg(target_os = "windows")]
{
@@ -823,6 +826,7 @@ pub async fn start_listen<T: InvokeUiCM>(
port_forward,
peer_id,
name,
avatar,
authorized,
keyboard,
clipboard,
@@ -843,6 +847,7 @@ pub async fn start_listen<T: InvokeUiCM>(
port_forward,
peer_id,
name,
avatar,
authorized,
keyboard,
clipboard,

View File

@@ -245,7 +245,20 @@ pub fn get_builtin_option(key: &str) -> String {
#[inline]
pub fn set_local_option(key: String, value: String) {
LocalConfig::set_option(key.clone(), value.clone());
LocalConfig::set_option(key.clone(), value);
}
/// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL
/// by prepending the API server address.
pub fn resolve_avatar_url(avatar: String) -> String {
let avatar = avatar.trim().to_owned();
if avatar.starts_with('/') {
let api_server = get_api_server();
if !api_server.is_empty() {
return format!("{}{}", api_server.trim_end_matches('/'), avatar);
}
}
avatar
}
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]

View File

@@ -119,7 +119,7 @@ fn start_auto_update_check_(rx_msg: Receiver<UpdateMsg>) {
fn check_update(manually: bool) -> ResultType<()> {
#[cfg(target_os = "windows")]
let is_msi = crate::platform::is_msi_installed()?;
let update_msi = crate::platform::is_msi_installed()? && !crate::is_custom_client();
if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
return Ok(());
}
@@ -140,7 +140,7 @@ fn check_update(manually: bool) -> ResultType<()> {
"{}/rustdesk-{}-x86_64.{}",
download_url,
version,
if is_msi { "msi" } else { "exe" }
if update_msi { "msi" } else { "exe" }
)
} else {
format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version)
@@ -190,21 +190,21 @@ fn check_update(manually: bool) -> ResultType<()> {
// before the download, but not empty after the download.
if has_no_active_conns() {
#[cfg(target_os = "windows")]
update_new_version(is_msi, &version, &file_path);
update_new_version(update_msi, &version, &file_path);
}
}
Ok(())
}
#[cfg(target_os = "windows")]
fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
fn update_new_version(update_msi: bool, version: &str, file_path: &PathBuf) {
log::debug!(
"New version is downloaded, update begin, is msi: {is_msi}, version: {version}, file: {:?}",
"New version is downloaded, update begin, update msi: {update_msi}, version: {version}, file: {:?}",
file_path.to_str()
);
if let Some(p) = file_path.to_str() {
if let Some(session_id) = crate::platform::get_current_process_session_id() {
if is_msi {
if update_msi {
match crate::platform::update_me_msi(p, true) {
Ok(_) => {
log::debug!("New version \"{}\" updated.", version);
@@ -215,21 +215,57 @@ fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
version,
e
);
std::fs::remove_file(&file_path).ok();
}
}
} else {
match crate::platform::launch_privileged_process(
let custom_client_staging_dir = if crate::is_custom_client() {
let custom_client_staging_dir =
crate::platform::get_custom_client_staging_dir();
if let Err(e) = crate::platform::handle_custom_client_staging_dir_before_update(
&custom_client_staging_dir,
) {
log::error!(
"Failed to handle custom client staging dir before update: {}",
e
);
std::fs::remove_file(&file_path).ok();
return;
}
Some(custom_client_staging_dir)
} else {
// Clean up any residual staging directory from previous custom client
let staging_dir = crate::platform::get_custom_client_staging_dir();
hbb_common::allow_err!(crate::platform::remove_custom_client_staging_dir(
&staging_dir
));
None
};
let update_launched = match crate::platform::launch_privileged_process(
session_id,
&format!("{} --update", p),
) {
Ok(h) => {
if h.is_null() {
log::error!("Failed to update to the new version: {}", version);
false
} else {
log::debug!("New version \"{}\" is launched.", version);
true
}
}
Err(e) => {
log::error!("Failed to run the new version: {}", e);
false
}
};
if !update_launched {
if let Some(dir) = custom_client_staging_dir {
hbb_common::allow_err!(crate::platform::remove_custom_client_staging_dir(
&dir
));
}
std::fs::remove_file(&file_path).ok();
}
}
} else {
@@ -237,6 +273,7 @@ fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
"Failed to get the current process session id, Error {}",
std::io::Error::last_os_error()
);
std::fs::remove_file(&file_path).ok();
}
} else {
// unreachable!()