Compare commits

..

12 Commits

Author SHA1 Message Date
dependabot[bot]
716a4c9a11 Git submodule: Bump libs/hbb_common from 9043c15 to 822701e
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `9043c15` to `822701e`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](9043c15acc...822701e416)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 822701e416a60c97e6f3b23bb0bfa84a2177c57f
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 00:42:53 +00:00
Luke
6ad56075d6 Drag whole toolbar; snap to all four edges of the remote session window (#15051)
* Drag whole toolbar; snap to all four edges

Today the drag handle on the remote-session toolbar repositions only
the handle row -- the icons themselves stay centered at the top. This
change applies the position to the entire toolbar wrapper so dragging
the handle moves the whole thing, and extends snapping from top-only
to any of the four window edges.

When docked left/right the toolbar reflows vertically. A live ghost
preview shows where the toolbar will land while you drag, with a small
hysteresis bias to keep the preview from flickering near corners.

The legacy 'remote-menubar-drag-x' session option is read as a fallback
on first load so existing users keep their saved horizontal position;
new option keys are 'remote-menubar-edge' and 'remote-menubar-frac'.

Tested locally on Windows. macOS / Linux / web desktop use the same
shared widget with no platform-specific calls, but I did not verify
them.

* Load edge independently and clamp loaded fraction

Addresses CodeRabbit review on #15051: parse the saved edge regardless
of whether the new fraction option is present so a partial write of
frac doesn't reset the toolbar back to top, and clamp the loaded
fraction to the kOptionRemoteMenubarDragLeft/Right contract so a
corrupted or out-of-range saved value can't bypass the bounds until
the user drags again.

* Require edge activation zone to switch dock; preserve horizontal slide

Per review feedback on #15051: nearest-edge-wins made a low-intent
horizontal slide too easy to escalate into a high-impact orientation
change (vertical reflow on left/right dock). The default drag now
keeps the toolbar on its current dock edge and just updates the
fraction along that edge -- the prior horizontal-slide behavior.

An alternate edge is only previewed/committed when the cursor enters
its 32 px activation zone; once previewed, the cursor has to move
back 64 px before reverting (hysteresis at the zone boundary).

* Gate multi-edge docking behind a settings toggle; default = horizontal slide

Replaces the activation-zone approach with an explicit opt-in setting
in Settings -> Other ("Allow docking remote toolbar to any window
edge"). This addresses the concern that a low-intent horizontal drag
shouldn't be able to trigger a high-impact orientation change, while
still letting users who want multi-edge docking opt in cleanly.

Default (toggle off):
  - The original horizontal slide is preserved.
  - The bug fix from the first commit still applies: dragging the
    handle moves the whole toolbar, and the position persists across
    collapse/expand (no more re-center on re-open).
  - Draggable is axis-locked to horizontal so the feedback widget
    stays on the top line during drag.

Opt-in (toggle on):
  - Full nearest-edge wins with the live preview ghost and corner
    hysteresis; toolbar reflows vertically on left/right docks.
  - Draggable is unlocked for 2D drag.

Reads the option via mainGetLocalBoolOptionSync so the toolbar's
default state matches what the settings checkbox shows; the option
key uses the allow- prefix so unset defaults to off.

Takes effect on next session (setting is read at session init).

The setting key (allow-multi-edge-toolbar-dock) is read by the
existing local-options machinery and persists per-install without
needing to be registered in libs/hbb_common's KEYS_LOCAL_SETTINGS.
Can add that registration in a parallel hbb_common PR if preferred.

* Fix remote toolbar drag positioning & persistence

Align drag fraction calculation with the toolbar's actual travel range,
keep preview sizing stable during drag, and preserve legacy horizontal
position storage when multi-edge docking is disabled.

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

* Remote toolbar snap edges

1. Translations
2. Apply option to remote windows on changed

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

* fix: avoid remote toolbar docking jumps on setting reload

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

* Fix remote toolbar docking updates and drag sync

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

* refact: translation key

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

* feat(toolbar-snap-edges): test web

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

* Fix remote toolbar docking sync and vertical layout

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

* Fix remote toolbar monitor controls on side docks

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-05-24 19:08:45 +08:00
Maison da Silva
b81ae6c894 Translate various labels to Portuguese-BR (#15086)
Update
2026-05-22 18:36:15 +08:00
dependabot[bot]
546e9f1702 Git submodule: Bump libs/hbb_common from c8cbb6b to 9043c15 (#15067)
Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `c8cbb6b` to `9043c15`.
- [Release notes](https://github.com/rustdesk/hbb_common/releases)
- [Commits](c8cbb6be28...9043c15acc)

---
updated-dependencies:
- dependency-name: libs/hbb_common
  dependency-version: 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-21 09:07:43 +08:00
fufesou
bb51c6aa42 fix(ipc): cmdline, unit tests (#15069)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-18 17:03:04 +08:00
fufesou
78e8134ad5 fix(ipc): cmdline, use scope, deploy (#15068)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-18 16:52:22 +08:00
fufesou
bc2c36215d fix(ipc): scope active-user IPC routing to root CLI main requests (#15058)
* fix(ipc): scope active-user IPC routing to root CLI main requests

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

* fix(ipc): cmdline, comments fails close

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

* fix(ipc): cmdline, better check

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

* fix(ipc): cmdline, try active uid when no --server processes

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

* fix(ipc): cmdline, select active uid

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

* fix(ipc): remove unused import

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-05-18 16:32:46 +08:00
IronCodeStudios
377547fa11 scrap/wayland: insert videoconvert to fix screencast on COSMIC / DMA-BUF portals (#15063)
On Wayland compositors whose xdg-desktop-portal backend exposes screencast
frames as DMA-BUF buffers — notably xdg-desktop-portal-cosmic 0.1.0 on
Pop!_OS 24.04 / COSMIC — inbound screen capture fails. PipeWireRecorder
links pipewiresrc directly to an appsink whose caps only accept
video/x-raw BGRx/RGBx in system memory. That format set is too narrow for
the portal's buffer-type / modifier negotiation, which collapses with:

  pw.link: negotiating -> error no more output formats (-22)
  gstpipewiresrc: stream error: no more output formats
  gstbasesrc: streaming stopped, reason not-negotiated (-4)
  ERROR src/server/wayland.rs: Failed scrap Element failed to change its state

Inserting a videoconvert element between pipewiresrc and appsink widens
the negotiable format set to any system-memory video/x-raw format, giving
the portal room to settle on a format it can deliver via its SHM path.
videoconvert then converts to the BGRx/RGBx the appsink expects.

Verified on Pop!_OS 24.04 / COSMIC with gst-launch, before and after:

  # fails (current behaviour):
  gst-launch-1.0 pipewiresrc path=N ! video/x-raw,format=BGRx ! fakesink
  # works (with this change):
  gst-launch-1.0 pipewiresrc path=N ! videoconvert ! video/x-raw,format=BGRx ! fakesink

After the change, inbound connections capture and stream the desktop
normally and the "Failed scrap" error no longer occurs.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:02:23 +08:00
RustDesk
472c4fc03a --deploy, reuse the device token (#15035)
* --deploy, reuse the device token

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix review

* no id validation in deploy, so to keep the same behavior in udp register
pk

* Fix collapsed toolbar drag preview sizing

* Revert "Fix collapsed toolbar drag preview sizing"

This reverts commit 66e39abb74.

* remove too many logs

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-16 14:41:34 +08:00
rustdesk
9f8f726f12 fix compile 2026-05-15 17:30:59 +08:00
flusheDData
701a9c6cdc New terms added (#15036)
* Update es.rs

New terms added

* Update es.rs

New terms added

* Update Spanish translations for various strings

* Fix typo in Spanish translation for TLS fallback

* Add Spanish translations for various UI elements

* Update es.rs

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-05-15 15:31:25 +08:00
Alex Rijckaert
0d40cf2101 Update Dutch translations (#15024)
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-05-14 16:43:40 +08:00
61 changed files with 1179 additions and 192 deletions

View File

@@ -142,6 +142,10 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
const String kOptionCodecPreference = "codec-preference";
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
const String kOptionRemoteMenubarEdge = "remote-menubar-edge";
const String kOptionRemoteMenubarFraction = "remote-menubar-frac";
const String kOptionAllowMultiEdgeToolbarDock =
"allow-multi-edge-toolbar-dock";
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
const String kOptionRemoteMenubarState = "remoteMenubarState";
const String kOptionPeerSorting = "peer-sorting";

View File

@@ -488,6 +488,16 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
kOptionEnableConfirmClosingTabs,
isServer: false),
if (!bind.isIncomingOnly())
_OptionCheckBox(
context,
'allow-remote-toolbar-docking-any-edge',
kOptionAllowMultiEdgeToolbarDock,
isServer: false,
update: (_) {
reloadAllWindows();
},
),
_OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr),
if (!isWeb) wallpaper(),
if (!isWeb && !bind.isIncomingOnly()) ...[

View File

@@ -28,6 +28,220 @@ import './kb_layout_type_chooser.dart';
import 'package:flutter_hbb/utils/scale.dart';
import 'package:flutter_hbb/common/widgets/custom_scale_base.dart';
enum _ToolbarEdge { top, right, bottom, left }
_ToolbarEdge _parseToolbarEdge(String? s) {
switch (s) {
case 'right':
return _ToolbarEdge.right;
case 'bottom':
return _ToolbarEdge.bottom;
case 'left':
return _ToolbarEdge.left;
default:
return _ToolbarEdge.top;
}
}
String _toolbarEdgeToString(_ToolbarEdge e) {
switch (e) {
case _ToolbarEdge.top:
return 'top';
case _ToolbarEdge.right:
return 'right';
case _ToolbarEdge.bottom:
return 'bottom';
case _ToolbarEdge.left:
return 'left';
}
}
bool _isHorizontalEdge(_ToolbarEdge e) =>
e == _ToolbarEdge.top || e == _ToolbarEdge.bottom;
const _legacyRemoteMenubarDragX = 'remote-menubar-drag-x';
double _clampToolbarFraction(double fraction, double left, double right) {
if (fraction < left) fraction = left;
if (fraction > right) fraction = right;
return fraction;
}
Size _toolbarSizeForEdge(_ToolbarEdge edge, Size? measured) {
final isHorizontal = _isHorizontalEdge(edge);
final fallback = isHorizontal ? const Size(360, 40) : const Size(40, 360);
final size = measured ?? fallback;
final long = size.longestSide;
final short = size.shortestSide;
return Size(isHorizontal ? long : short, isHorizontal ? short : long);
}
Offset _toolbarOffsetForEdge({
required _ToolbarEdge edge,
required double fraction,
required Size parentSize,
required Size toolbarSize,
}) {
final xTravel = parentSize.width - toolbarSize.width;
final yTravel = parentSize.height - toolbarSize.height;
switch (edge) {
case _ToolbarEdge.top:
return Offset(xTravel * fraction, 0);
case _ToolbarEdge.bottom:
return Offset(xTravel * fraction, yTravel);
case _ToolbarEdge.left:
return Offset(0, yTravel * fraction);
case _ToolbarEdge.right:
return Offset(xTravel, yTravel * fraction);
}
}
double _fractionForAlignedDrag({
required double cursor,
required double grabOffset,
required double parentExtent,
required double toolbarExtent,
required double left,
required double right,
}) {
final travelExtent = parentExtent - toolbarExtent;
if (travelExtent <= 0) {
return _clampToolbarFraction(0.5, left, right);
}
return _clampToolbarFraction(
(cursor - grabOffset) / travelExtent, left, right);
}
({double left, double right}) _fractionBoundsForEdge(
_ToolbarEdge edge,
double left,
double right,
) {
return _isHorizontalEdge(edge)
? (left: left, right: right)
: (left: 0, right: 1);
}
String _toolbarRawFraction({
required bool multiEdgeEnabled,
required _ToolbarEdge edge,
required String? savedFraction,
required String? legacyFraction,
}) {
if (!multiEdgeEnabled) {
return (legacyFraction != null && legacyFraction.isNotEmpty)
? legacyFraction
: '0.5';
}
if (savedFraction != null && savedFraction.isNotEmpty) {
return savedFraction;
}
if (edge == _ToolbarEdge.top &&
legacyFraction != null &&
legacyFraction.isNotEmpty) {
return legacyFraction;
}
return '0.5';
}
// Returns the alignment for the wrapper Align that positions the entire
// toolbar against the given edge at the given fraction along that edge.
// Alignment uses [-1, 1] coordinates (0 = center).
Alignment _alignmentForEdge(_ToolbarEdge edge, double fraction) {
final f = fraction * 2 - 1;
switch (edge) {
case _ToolbarEdge.top:
return Alignment(f, -1);
case _ToolbarEdge.bottom:
return Alignment(f, 1);
case _ToolbarEdge.left:
return Alignment(-1, f);
case _ToolbarEdge.right:
return Alignment(1, f);
}
}
// The drag handle hangs off the side of the toolbar facing away from the
// docked edge, so the icons themselves sit flush against that edge.
BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) {
const r = Radius.circular(5);
switch (edge) {
case _ToolbarEdge.top:
return const BorderRadius.vertical(bottom: r);
case _ToolbarEdge.bottom:
return const BorderRadius.vertical(top: r);
case _ToolbarEdge.left:
return const BorderRadius.horizontal(right: r);
case _ToolbarEdge.right:
return const BorderRadius.horizontal(left: r);
}
}
int _monitorMenuQuarterTurns(_ToolbarEdge edge) {
switch (edge) {
case _ToolbarEdge.left:
return 1;
case _ToolbarEdge.right:
return 3;
case _ToolbarEdge.top:
case _ToolbarEdge.bottom:
return 0;
}
}
IconData _toolbarCollapseIcon(_ToolbarEdge edge, bool isCollapsed) {
switch (edge) {
case _ToolbarEdge.top:
return isCollapsed ? Icons.expand_more : Icons.expand_less;
case _ToolbarEdge.bottom:
return isCollapsed ? Icons.expand_less : Icons.expand_more;
case _ToolbarEdge.left:
return isCollapsed ? Icons.chevron_right : Icons.chevron_left;
case _ToolbarEdge.right:
return isCollapsed ? Icons.chevron_left : Icons.chevron_right;
}
}
class _ToolbarDockingOptions {
_ToolbarDockingOptions({
required this.edge,
required this.fraction,
required this.multiEdgeEnabled,
});
_ToolbarEdge edge;
double fraction;
bool multiEdgeEnabled;
}
final _toolbarDockingOptionsBySession = <String, _ToolbarDockingOptions>{};
String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString();
_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) =>
_toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)];
void _cacheToolbarDockingOptions({
required SessionID sessionId,
required _ToolbarEdge edge,
required double fraction,
required bool multiEdgeEnabled,
}) {
final key = _toolbarDockingCacheKey(sessionId);
final cached = _toolbarDockingOptionsBySession[key];
if (cached == null) {
_toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions(
edge: edge,
fraction: fraction,
multiEdgeEnabled: multiEdgeEnabled,
);
return;
}
cached.edge = edge;
cached.fraction = fraction;
cached.multiEdgeEnabled = multiEdgeEnabled;
}
class ToolbarState {
late RxBool _pin;
@@ -250,8 +464,26 @@ class RemoteToolbar extends StatefulWidget {
class _RemoteToolbarState extends State<RemoteToolbar> {
late Debouncer<int> _debouncerHide;
bool _isCursorOverImage = false;
final _fractionX = 0.5.obs;
final _fraction = 0.5.obs;
final _edge = _ToolbarEdge.top.obs;
final _dragging = false.obs;
// Live drag preview: where the toolbar would dock if the user dropped now.
final _previewEdge = Rxn<_ToolbarEdge>();
final _previewFraction = Rxn<double>();
// Measured size of the live toolbar, so the preview ghost matches reality
// (collapsed handle vs expanded toolbar). Updated after every layout pass.
final _toolbarSize = Rxn<Size>();
final _toolbarKey = GlobalKey(debugLabel: 'remote_toolbar_root');
// When false (default), the toolbar stays on the top edge and the drag
// handle just slides it horizontally — preserving long-standing UX while
// still fixing the bug where dragging only moved the handle. When true,
// the user has opted into multi-edge docking with nearest-edge snap.
// Kept in sync after settings-triggered rebuilds.
final _multiEdgeEnabled = false.obs;
final _dockingOptionsInitialized = false.obs;
bool _pendingDockingOptionSync = false;
int _dockingOptionSyncSerial = 0;
int _dragEpoch = 0;
int get windowId => stateGlobal.windowId;
@@ -273,16 +505,144 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
void _minimize() async =>
await WindowController.fromWindowId(windowId).minimize();
Future<void> _syncDockingOptions({required bool force}) async {
final syncSerial = ++_dockingOptionSyncSerial;
if (_dragging.isTrue) {
_deferDockingOptionsSync();
return;
}
final dragEpoch = _dragEpoch;
// Use the canonical helper so the option's documented default semantics
// apply (allow-* prefix => default false). Keeping it raw-string would
// diverge from how _OptionCheckBox displays the same key.
final multiEdgeEnabled =
mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock);
final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId);
if (cached == null && pi.isSet.isFalse) {
return;
}
final hadDockingOptions = cached != null;
final wasMultiEdgeEnabled =
cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value;
if (!force &&
hadDockingOptions &&
wasMultiEdgeEnabled == multiEdgeEnabled) {
_pendingDockingOptionSync = false;
return;
}
final savedFraction = await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction);
// Backward compat: legacy horizontal-only position.
final legacyFraction = await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX);
if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
var nextEdge = _edge.value;
var savedFractionForNextEdge = savedFraction;
var keepCurrentPosition = false;
if (!multiEdgeEnabled) {
nextEdge = _ToolbarEdge.top;
} else if (force || wasMultiEdgeEnabled || cached == null) {
final edgeStr = await bind.sessionGetOption(
sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge);
if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
nextEdge = _parseToolbarEdge(edgeStr);
} else {
// The setting changed from top-only to multi-edge while this toolbar is
// already visible. Keep its current position instead of jumping to the
// last saved multi-edge dock.
nextEdge = cached.edge;
savedFractionForNextEdge = cached.fraction.toString();
keepCurrentPosition = true;
}
final rawFraction = _toolbarRawFraction(
multiEdgeEnabled: multiEdgeEnabled,
edge: nextEdge,
savedFraction: savedFractionForNextEdge,
legacyFraction: legacyFraction,
);
// Clamp to the saved drag-bound contract so a corrupted or out-of-range
// saved value can't bypass it until the user drags again.
final dragLeft = double.tryParse(
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft)) ??
0.0;
final dragRight = double.tryParse(
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight)) ??
1.0;
final fractionBounds =
_fractionBoundsForEdge(nextEdge, dragLeft, dragRight);
final nextFraction = (double.tryParse(rawFraction) ?? 0.5)
.clamp(fractionBounds.left, fractionBounds.right)
.toDouble();
if (!mounted || syncSerial != _dockingOptionSyncSerial) return;
if (_dragging.isTrue || dragEpoch != _dragEpoch) {
_deferDockingOptionsSync();
return;
}
_edge.value = nextEdge;
_fraction.value = nextFraction;
_multiEdgeEnabled.value = multiEdgeEnabled;
_dockingOptionsInitialized.value = true;
_cacheToolbarDockingOptions(
sessionId: widget.ffi.sessionId,
edge: nextEdge,
fraction: nextFraction,
multiEdgeEnabled: multiEdgeEnabled,
);
_pendingDockingOptionSync = false;
if (!multiEdgeEnabled || keepCurrentPosition) {
bind.sessionPeerOption(
sessionId: widget.ffi.sessionId,
name: kOptionRemoteMenubarEdge,
value: _toolbarEdgeToString(nextEdge),
);
bind.sessionPeerOption(
sessionId: widget.ffi.sessionId,
name: kOptionRemoteMenubarFraction,
value: nextFraction.toString(),
);
}
}
void _deferDockingOptionsSync() {
_pendingDockingOptionSync = true;
if (_dragging.isFalse) {
_syncDockingOptionsAfterDragIfNeeded();
}
}
void _markToolbarDragEpoch() {
++_dragEpoch;
}
void _syncDockingOptionsAfterDragIfNeeded() {
if (!_pendingDockingOptionSync) return;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _syncDockingOptions(force: false);
});
}
@override
initState() {
super.initState();
final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId);
final multiEdgeEnabled =
mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock);
final shouldResetToTop =
cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled;
if (cached != null && !shouldResetToTop) {
_edge.value = cached.edge;
_fraction.value = cached.fraction;
_multiEdgeEnabled.value = multiEdgeEnabled;
_dockingOptionsInitialized.value = true;
}
WidgetsBinding.instance.addPostFrameCallback((_) async {
_fractionX.value = double.tryParse(await bind.sessionGetOption(
sessionId: widget.ffi.sessionId,
arg: 'remote-menubar-drag-x') ??
'0.5') ??
0.5;
await _syncDockingOptions(force: cached == null || shouldResetToTop);
// Initialize toolbar states (collapse, hide) from session options
widget.state.init(widget.ffi.sessionId);
});
@@ -303,6 +663,14 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
});
}
@override
void didUpdateWidget(covariant RemoteToolbar oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _syncDockingOptions(force: false);
});
}
_debouncerHideProc(int v) {
if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) {
collapse.value = true;
@@ -311,64 +679,130 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
@override
dispose() {
super.dispose();
++_dockingOptionSyncSerial;
widget.onEnterOrLeaveImageCleaner(identityHashCode(this));
super.dispose();
}
@override
Widget build(BuildContext context) {
return Obx(() {
// Wait for initialization to complete to prevent flickering
if (!widget.state.initialized.value) {
if (!widget.state.initialized.value ||
!_dockingOptionsInitialized.value) {
return const SizedBox.shrink();
}
// If toolbar is hidden, return empty widget
if (hide.value) {
return const SizedBox.shrink();
}
return Align(
alignment: Alignment.topCenter,
child: collapse.isFalse
? _buildToolbar(context)
: _buildDraggableCollapse(context),
final edge = _edge.value;
final isHorizontal = _isHorizontalEdge(edge);
// Measure the live toolbar after every layout so the preview ghost can
// match its actual footprint (collapsed handle vs expanded toolbar).
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_dragging.isTrue) return;
final ro = _toolbarKey.currentContext?.findRenderObject();
if (ro is RenderBox && ro.hasSize) {
final s = ro.size;
if (_toolbarSize.value != s) _toolbarSize.value = s;
}
});
final toolbar = Align(
alignment: _alignmentForEdge(edge, _fraction.value),
child: KeyedSubtree(
key: _toolbarKey,
child: collapse.isFalse
? _buildToolbar(context, edge, isHorizontal)
: _buildDraggableCollapse(context, edge, isHorizontal),
),
);
// Always return the Stack — even when not dragging — so the toolbar's
// position in the Element tree stays stable. Wrapping/unwrapping it
// mid-drag was killing the Draggable's gesture state.
return Stack(
fit: StackFit.expand,
children: [
IgnorePointer(
child: Obx(() {
final pe = _previewEdge.value;
final pf = _previewFraction.value;
if (!_dragging.isTrue || pe == null || pf == null) {
return const SizedBox.shrink();
}
return _buildDragPreview(context, pe, pf, _toolbarSize.value);
}),
),
toolbar,
],
);
});
}
Widget _buildDraggableCollapse(BuildContext context) {
Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge,
double fraction, Size? measured) {
final color = Theme.of(context).colorScheme.primary;
// Use the measured live toolbar size so collapsed vs expanded looks
// right. The current orientation may differ from the preview orientation
// (e.g. dragging a top-docked toolbar toward the left edge), so swap the
// long/short axes when previewing a different orientation.
final previewSize = _toolbarSizeForEdge(edge, measured);
return Align(
alignment: _alignmentForEdge(edge, fraction),
child: Container(
width: previewSize.width,
height: previewSize.height,
decoration: BoxDecoration(
color: color.withOpacity(0.10),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withOpacity(0.55), width: 1.5),
),
),
);
}
Widget _buildDraggableCollapse(
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
return Obx(() {
if (collapse.isFalse && _dragging.isFalse) {
triggerAutoHide();
}
final borderRadius = BorderRadius.vertical(
bottom: Radius.circular(5),
);
return Align(
alignment: FractionalOffset(_fractionX.value, 0),
child: Offstage(
offstage: _dragging.isTrue,
child: Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
final borderRadius = _collapseHandleBorderRadius(edge);
return Offstage(
offstage: _dragging.isTrue,
child: Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
borderRadius: borderRadius,
child: _DraggableShowHide(
id: widget.id,
sessionId: widget.ffi.sessionId,
dragging: _dragging,
fraction: _fraction,
edge: _edge,
previewEdge: _previewEdge,
previewFraction: _previewFraction,
toolbarSize: _toolbarSize,
markDragEpoch: _markToolbarDragEpoch,
syncDockingOptionsAfterDragIfNeeded:
_syncDockingOptionsAfterDragIfNeeded,
isHorizontal: isHorizontal,
multiEdgeEnabled: _multiEdgeEnabled.value,
toolbarState: widget.state,
setFullscreen: _setFullscreen,
setMinimize: _minimize,
borderRadius: borderRadius,
child: _DraggableShowHide(
id: widget.id,
sessionId: widget.ffi.sessionId,
dragging: _dragging,
fractionX: _fractionX,
toolbarState: widget.state,
setFullscreen: _setFullscreen,
setMinimize: _minimize,
borderRadius: borderRadius,
),
),
),
);
});
}
Widget _buildToolbar(BuildContext context) {
Widget _buildToolbar(
BuildContext context, _ToolbarEdge edge, bool isHorizontal) {
final List<Widget> toolbarItems = [];
toolbarItems.add(_PinMenu(state: widget.state));
if (!isWebDesktop) {
@@ -382,6 +816,7 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
return _MonitorMenu(
id: widget.id,
ffi: widget.ffi,
edge: edge,
setRemoteState: widget.setRemoteState);
} else {
return Offstage();
@@ -407,37 +842,53 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
if (!isWeb) toolbarItems.add(_RecordMenu());
toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi));
final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0));
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
borderRadius: toolbarBorderRadius,
color: Theme.of(context)
.menuBarTheme
.style
?.backgroundColor
?.resolve(MaterialState.values.toSet()),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Theme(
data: themeData(),
child: _ToolbarTheme.borderWrapper(
context,
Row(
children: [
SizedBox(width: _ToolbarTheme.buttonHMargin * 2),
...toolbarItems,
SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
],
),
toolbarBorderRadius),
),
),
// innerAxis: how the toolbar icons themselves flow.
// outerAxis: how the toolbar block and the handle stack against each other
// (perpendicular to the dock edge, so the handle hangs off the interior face).
final innerAxis = isHorizontal ? Axis.horizontal : Axis.vertical;
final outerAxis = isHorizontal ? Axis.vertical : Axis.horizontal;
final spacer = isHorizontal
? SizedBox(width: _ToolbarTheme.buttonHMargin * 2)
: SizedBox(height: _ToolbarTheme.buttonHMargin * 2);
final toolbarMaterial = Material(
elevation: _ToolbarTheme.elevation,
shadowColor: MyTheme.color(context).shadow,
borderRadius: toolbarBorderRadius,
color: Theme.of(context)
.menuBarTheme
.style
?.backgroundColor
?.resolve(MaterialState.values.toSet()),
child: SingleChildScrollView(
scrollDirection: innerAxis,
child: Theme(
data: themeData(),
child: _ToolbarTheme.borderWrapper(
context,
Flex(
direction: innerAxis,
mainAxisSize: MainAxisSize.min,
children: [
spacer,
...toolbarItems,
spacer,
],
),
toolbarBorderRadius),
),
_buildDraggableCollapse(context),
],
),
);
final handle = _buildDraggableCollapse(context, edge, isHorizontal);
// The handle hangs off the interior face of the toolbar (away from the
// docked edge), centered along that face by the Flex's default cross-axis
// alignment, so the icons themselves sit flush against the docked edge.
final children = (edge == _ToolbarEdge.top || edge == _ToolbarEdge.left)
? [toolbarMaterial, handle]
: [handle, toolbarMaterial];
return Flex(
direction: outerAxis,
mainAxisSize: MainAxisSize.min,
children: children,
);
}
@@ -516,11 +967,13 @@ class _MobileActionMenu extends StatelessWidget {
class _MonitorMenu extends StatelessWidget {
final String id;
final FFI ffi;
final _ToolbarEdge edge;
final Function(VoidCallback) setRemoteState;
const _MonitorMenu({
Key? key,
required this.id,
required this.ffi,
required this.edge,
required this.setRemoteState,
}) : super(key: key);
@@ -531,9 +984,17 @@ class _MonitorMenu extends StatelessWidget {
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
@override
Widget build(BuildContext context) => showMonitorsToolbar
? buildMultiMonitorMenu(context)
: Obx(() => buildMonitorMenu(context));
Widget build(BuildContext context) {
final child = showMonitorsToolbar
? buildMultiMonitorMenu(context)
: Obx(() => buildMonitorMenu(context));
final quarterTurns = _monitorMenuQuarterTurns(edge);
if (quarterTurns == 0) return child;
return RotatedBox(
quarterTurns: quarterTurns,
child: child,
);
}
Widget buildMonitorMenu(BuildContext context) {
final width = SimpleWrapper<double>(0);
@@ -665,7 +1126,8 @@ class _MonitorMenu extends StatelessWidget {
}
final scale = _ToolbarTheme.buttonSize / rect.height * 0.75;
final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5;
final height = rect.height * scale;
final startY = (_ToolbarTheme.buttonSize - height) * 0.5;
final startX = startY;
final children = <Widget>[];
@@ -708,7 +1170,7 @@ class _MonitorMenu extends StatelessWidget {
width.value = rect.width * scale + startX * 2;
return SizedBox(
width: width.value,
height: rect.height * scale + startY * 2,
height: height + startY * 2,
child: Stack(
children: children,
),
@@ -2519,7 +2981,18 @@ class RdoMenuButton<T> extends StatelessWidget {
class _DraggableShowHide extends StatefulWidget {
final String id;
final SessionID sessionId;
final RxDouble fractionX;
final RxDouble fraction;
final Rx<_ToolbarEdge> edge;
final Rxn<_ToolbarEdge> previewEdge;
final Rxn<double> previewFraction;
final Rxn<Size> toolbarSize;
final VoidCallback markDragEpoch;
final VoidCallback syncDockingOptionsAfterDragIfNeeded;
final bool isHorizontal;
// Whether multi-edge docking is enabled for this session (toggled in
// Settings -> Other). When false, the drag handle slides the toolbar
// horizontally on the top edge and never switches edges.
final bool multiEdgeEnabled;
final RxBool dragging;
final ToolbarState toolbarState;
final BorderRadius borderRadius;
@@ -2531,7 +3004,15 @@ class _DraggableShowHide extends StatefulWidget {
Key? key,
required this.id,
required this.sessionId,
required this.fractionX,
required this.fraction,
required this.edge,
required this.previewEdge,
required this.previewFraction,
required this.toolbarSize,
required this.markDragEpoch,
required this.syncDockingOptionsAfterDragIfNeeded,
required this.isHorizontal,
required this.multiEdgeEnabled,
required this.dragging,
required this.toolbarState,
required this.setFullscreen,
@@ -2544,10 +3025,12 @@ class _DraggableShowHide extends StatefulWidget {
}
class _DraggableShowHideState extends State<_DraggableShowHide> {
Offset position = Offset.zero;
Size size = Size.zero;
double left = 0.0;
double right = 1.0;
Offset? _lastPointerDown;
Offset? _dragGrabOffset;
double? _dragLongAxisGrabOffset;
Size? _dragToolbarSize;
RxBool get collapse => widget.toolbarState.collapse;
@@ -2573,41 +3056,174 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
}
}
// Bias applied to the currently-previewed edge so a drag hovering between
// two edges doesn't flicker. Only relevant when multi-edge is enabled.
static const double _switchHysteresisPx = 50.0;
_ToolbarEdge _nearestToolbarEdge(Offset cursor, Size mediaSize) {
if (!widget.multiEdgeEnabled) return widget.edge.value;
double rawDist(_ToolbarEdge e) {
switch (e) {
case _ToolbarEdge.top:
return cursor.dy;
case _ToolbarEdge.bottom:
return mediaSize.height - cursor.dy;
case _ToolbarEdge.left:
return cursor.dx;
case _ToolbarEdge.right:
return mediaSize.width - cursor.dx;
}
}
final previewed = widget.previewEdge.value;
var winner = widget.edge.value;
var best = double.infinity;
for (final e in _ToolbarEdge.values) {
final biased =
e == previewed ? rawDist(e) - _switchHysteresisPx : rawDist(e);
if (biased < best) {
best = biased;
winner = e;
}
}
return winner;
}
void _ensureDragGrabOffset(Offset cursor) {
if (_dragGrabOffset != null) return;
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
final toolbarSize =
_toolbarSizeForEdge(widget.edge.value, widget.toolbarSize.value);
_dragToolbarSize = toolbarSize;
final toolbarOffset = _toolbarOffsetForEdge(
edge: widget.edge.value,
fraction: widget.fraction.value,
parentSize: mediaSize,
toolbarSize: toolbarSize,
);
_dragGrabOffset = cursor - toolbarOffset;
_dragLongAxisGrabOffset = _isHorizontalEdge(widget.edge.value)
? _dragGrabOffset?.dx
: _dragGrabOffset?.dy;
}
double _dragGrabOffsetForEdge(_ToolbarEdge edge, Size toolbarSize) {
final offset = _dragLongAxisGrabOffset ?? 0;
final extent =
_isHorizontalEdge(edge) ? toolbarSize.width : toolbarSize.height;
return _clampToolbarFraction(offset, 0, extent);
}
void _updatePreview(Offset cursor) {
_ensureDragGrabOffset(cursor);
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
final winner = _nearestToolbarEdge(cursor, mediaSize);
widget.previewEdge.value = winner;
final toolbarSize = _toolbarSizeForEdge(winner, _dragToolbarSize);
final grabOffset = _dragGrabOffsetForEdge(winner, toolbarSize);
final double frac;
if (winner == _ToolbarEdge.top || winner == _ToolbarEdge.bottom) {
frac = _fractionForAlignedDrag(
cursor: cursor.dx,
grabOffset: grabOffset,
parentExtent: mediaSize.width,
toolbarExtent: toolbarSize.width,
left: left,
right: right,
);
} else {
final fractionBounds = _fractionBoundsForEdge(winner, left, right);
frac = _fractionForAlignedDrag(
cursor: cursor.dy,
grabOffset: grabOffset,
parentExtent: mediaSize.height,
toolbarExtent: toolbarSize.height,
left: fractionBounds.left,
right: fractionBounds.right,
);
}
widget.previewFraction.value = frac;
}
void _resetDragTracking() {
_lastPointerDown = null;
_dragGrabOffset = null;
_dragLongAxisGrabOffset = null;
_dragToolbarSize = null;
}
void _commitPreview() {
final newEdge = widget.previewEdge.value;
final frac = widget.previewFraction.value;
widget.previewEdge.value = null;
widget.previewFraction.value = null;
widget.dragging.value = false;
widget.markDragEpoch();
_resetDragTracking();
widget.syncDockingOptionsAfterDragIfNeeded();
if (newEdge == null || frac == null) return;
widget.edge.value = newEdge;
widget.fraction.value = frac;
_cacheToolbarDockingOptions(
sessionId: widget.sessionId,
edge: newEdge,
fraction: frac,
multiEdgeEnabled: widget.multiEdgeEnabled,
);
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: kOptionRemoteMenubarEdge,
value: _toolbarEdgeToString(newEdge),
);
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: kOptionRemoteMenubarFraction,
value: frac.toString(),
);
if (widget.multiEdgeEnabled) {
return;
}
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: _legacyRemoteMenubarDragX,
value: frac.toString(),
);
}
Widget _buildDraggable(BuildContext context) {
return Draggable(
axis: Axis.horizontal,
child: Icon(
Icons.drag_indicator,
size: 20,
color: MyTheme.color(context).drag_indicator,
return Listener(
onPointerDown: (event) => _lastPointerDown = event.position,
child: Draggable(
// When multi-edge docking is off the toolbar stays on the top edge,
// so lock the feedback to horizontal motion — otherwise the handle
// floats away from the top while dragging and the toolbar looks
// unmoored. When multi-edge is on we need 2D drag for snap-to-edge.
axis: widget.multiEdgeEnabled ? null : Axis.horizontal,
child: Icon(
widget.isHorizontal ? Icons.drag_indicator : Icons.drag_handle,
size: 20,
color: MyTheme.color(context).drag_indicator,
),
feedback: widget,
onDragStarted: () {
widget.markDragEpoch();
final pointerDown = _lastPointerDown;
if (pointerDown != null) {
_ensureDragGrabOffset(pointerDown);
}
widget.dragging.value = true;
// Seed the preview at the current docked edge/fraction so something
// shows the instant the drag begins, before the first onDragUpdate.
widget.previewEdge.value = widget.edge.value;
widget.previewFraction.value = widget.fraction.value;
},
onDragUpdate: (details) {
_updatePreview(details.globalPosition);
},
onDragEnd: (_) => _commitPreview(),
),
feedback: widget,
onDragStarted: (() {
final RenderObject? renderObj = context.findRenderObject();
if (renderObj != null) {
final RenderBox renderBox = renderObj as RenderBox;
size = renderBox.size;
position = renderBox.localToGlobal(Offset.zero);
}
widget.dragging.value = true;
}),
onDragEnd: (details) {
final mediaSize = MediaQueryData.fromView(View.of(context)).size;
widget.fractionX.value +=
(details.offset.dx - position.dx) / (mediaSize.width - size.width);
if (widget.fractionX.value < left) {
widget.fractionX.value = left;
}
if (widget.fractionX.value > right) {
widget.fractionX.value = right;
}
bind.sessionPeerOption(
sessionId: widget.sessionId,
name: 'remote-menubar-drag-x',
value: widget.fractionX.value.toString(),
);
widget.dragging.value = false;
},
);
}
@@ -2637,7 +3253,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
);
}
final child = Row(
final axis = widget.isHorizontal ? Axis.horizontal : Axis.vertical;
final child = Flex(
direction: axis,
mainAxisSize: MainAxisSize.min,
children: [
_buildDraggable(context),
@@ -2678,7 +3296,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
message: translate(
collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'),
child: Icon(
collapse.isFalse ? Icons.expand_less : Icons.expand_more,
_toolbarCollapseIcon(widget.edge.value, collapse.isTrue),
size: iconSize,
),
))),
@@ -2720,7 +3338,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
borderRadius: widget.borderRadius,
),
child: SizedBox(
height: 20,
height: widget.isHorizontal ? 20 : null,
width: widget.isHorizontal ? null : 20,
child: child,
),
),

View File

@@ -346,7 +346,7 @@ class InputModel {
/// which runs per-engine, so each isolate registers its own handler tied
/// to its own set of InputModels.
static void initSideButtonChannel() {
if (!Platform.isLinux) return;
if (!isLinux) return;
if (_sideButtonChannelInitialized) return;
_sideButtonChannelInitialized = true;

View File

@@ -276,12 +276,21 @@ impl PipeWireRecorder {
// see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982
src.set_property("always-copy", &true)?;
// COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink.
// xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream
// format set is too narrow (appsink only accepts BGRx/RGBx), producing
// "no more output formats" / not-negotiated (-4). videoconvert accepts any
// system-memory video/x-raw format, widening negotiation so the portal can
// settle on a format it can deliver via its SHM path.
let convert = gst::ElementFactory::make("videoconvert", None)?;
let sink = gst::ElementFactory::make("appsink", None)?;
sink.set_property("drop", &true)?;
sink.set_property("max-buffers", &1u32)?;
pipeline.add_many(&[&src, &sink])?;
src.link(&sink)?;
pipeline.add_many(&[&src, &convert, &sink])?;
src.link(&convert)?;
convert.link(&sink)?;
let appsink = sink
.dynamic_cast::<AppSink>()

View File

@@ -199,6 +199,20 @@ pub fn core_main() -> Option<Vec<String>> {
}
std::thread::spawn(move || crate::start_server(false, no_server));
} else {
#[cfg(any(target_os = "linux", target_os = "macos"))]
// Root CLI management commands must talk to the user `--server` main IPC.
// Example: `sudo rustdesk --option custom-rendezvous-server` should query the
// user's IPC instead of root's `/tmp/<app>-0/ipc`; `connect()` still limits this
// routing to empty-postfix main IPC only.
let _user_main_ipc_scope = if crate::platform::is_installed()
&& is_root()
&& is_user_main_ipc_scope_cli_command(&args)
{
Some(crate::ipc::UserMainIpcScope::new())
} else {
None
};
#[cfg(windows)]
{
use crate::platform;
@@ -938,6 +952,57 @@ fn is_root() -> bool {
crate::platform::is_root()
}
#[cfg(any(target_os = "linux", target_os = "macos", test))]
fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool {
matches!(
args.first().map(String::as_str),
Some("--password")
| Some("--set-unlock-pin")
| Some("--get-id")
| Some("--set-id")
| Some("--config")
| Some("--option")
| Some("--assign")
| Some("--deploy")
)
}
#[cfg(test)]
mod tests {
use super::*;
fn args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
#[test]
fn user_main_ipc_scope_cli_command_matches_management_commands_only() {
for command in [
"--password",
"--set-unlock-pin",
"--get-id",
"--set-id",
"--config",
"--option",
"--assign",
"--deploy",
] {
assert!(is_user_main_ipc_scope_cli_command(&args(&[command])));
}
for command in [
"--service",
"--server",
"--tray",
"--cm",
"--check-hwcodec-config",
"--connect",
] {
assert!(!is_user_main_ipc_scope_cli_command(&args(&[command])));
}
}
}
/// Check if the executable is a Quick Support version.
/// Note: This function must be kept in sync with `libs/portable/src/main.rs`.
#[cfg(windows)]

View File

@@ -33,25 +33,25 @@ use hbb_common::{
tokio_util::codec::Framed,
ResultType,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::authorize_service_scoped_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection;
#[cfg(windows)]
pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt;
#[cfg(windows)]
pub(crate) use ipc_auth::log_rejected_windows_ipc_connection;
#[cfg(target_os = "linux")]
pub(crate) use ipc_auth::{
active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
log_rejected_uinput_connection, peer_uid_from_fd,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection};
#[cfg(windows)]
use ipc_auth::{
authorize_windows_main_ipc_connection, portable_service_listener_security_attributes,
should_allow_everyone_create_on_windows,
};
#[cfg(target_os = "linux")]
pub(crate) use ipc_auth::{
ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid,
log_rejected_uinput_connection, peer_uid_from_fd,
};
#[cfg(target_os = "linux")]
use ipc_fs::terminal_count_candidate_uids;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use ipc_fs::{
@@ -63,6 +63,8 @@ use parity_tokio_ipc::{
};
use serde_derive::{Deserialize, Serialize};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::cell::Cell;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::unix::fs::PermissionsExt;
use std::{
collections::HashMap,
@@ -71,12 +73,47 @@ use std::{
// IPC actions here.
pub const IPC_ACTION_CLOSE: &str = "close";
#[cfg(target_os = "windows")]
const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000;
#[cfg(target_os = "windows")]
pub(crate) const IPC_TOKEN_LEN: usize = 64;
#[cfg(target_os = "windows")]
const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2;
#[cfg(target_os = "windows")]
const _: () = assert!(IPC_TOKEN_LEN % 2 == 0);
pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true);
#[cfg(any(target_os = "linux", target_os = "macos"))]
thread_local! {
static USE_USER_MAIN_IPC: Cell<bool> = Cell::new(false);
}
#[must_use = "bind this guard to a local variable to keep the IPC scope active"]
/// Thread-local guard for routing root main IPC to the active user on Linux/macOS.
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) struct UserMainIpcScope {
previous: bool,
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl UserMainIpcScope {
pub(crate) fn new() -> Self {
let previous = USE_USER_MAIN_IPC.with(|use_user_main| {
let previous = use_user_main.get();
use_user_main.set(true);
previous
});
Self { previous }
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
impl Drop for UserMainIpcScope {
fn drop(&mut self) {
USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.set(self.previous));
}
}
#[inline]
pub async fn connect_service(ms_timeout: u64) -> ResultType<ConnectionTmpl<ConnClient>> {
connect(ms_timeout, crate::POSTFIX_SERVICE).await
@@ -1112,11 +1149,7 @@ async fn handle(data: Data, stream: &mut Connection) {
};
}
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
let path = Config::ipc_path(postfix);
connect_with_path(ms_timeout, &path).await
}
#[cfg(target_os = "windows")]
pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
use hbb_common::rand::{rngs::OsRng, RngCore as _};
use std::fmt::Write as _;
@@ -1137,6 +1170,7 @@ pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
Ok(token)
}
#[cfg(target_os = "windows")]
pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool {
if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN {
return false;
@@ -1149,6 +1183,7 @@ pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> boo
== 0
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
stream: &mut ConnectionTmpl<T>,
token: &str,
@@ -1173,6 +1208,7 @@ where
}
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
stream: &mut ConnectionTmpl<T>,
mut validate_token: F,
@@ -1209,6 +1245,103 @@ async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType<Connection
Ok(ConnectionTmpl::new(client))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
fn select_server_uid_for_user_main_ipc(
server_uids: &[u32],
active_uid: Option<u32>,
prefer_root: bool,
) -> ResultType<u32> {
let mut server_uids = server_uids.to_vec();
server_uids.sort_unstable();
server_uids.dedup();
match server_uids.as_slice() {
[] => {
if let Some(uid) = active_uid {
// If no `--server` processes are found but the active user is identifiable,
// try the active user anyway because the main process may also listen on "" IPC.
return Ok(uid);
} else {
bail!("No --server process found for user main IPC")
}
}
[uid] => return Ok(*uid),
_ => {}
}
if prefer_root && server_uids.contains(&0) {
return Ok(0);
}
if let Some(active_uid) = active_uid.filter(|uid| server_uids.contains(uid)) {
return Ok(active_uid);
}
bail!("Multiple --server processes found for user main IPC");
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn running_server_uids_for_current_exe() -> ResultType<Vec<u32>> {
let current_exe = std::env::current_exe()?;
let current_exe_path = std::fs::canonicalize(&current_exe)?;
let current_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id());
let mut sys = hbb_common::sysinfo::System::new();
sys.refresh_processes();
let mut server_uids = Vec::new();
for process in sys.processes().values() {
if process.pid() == current_pid {
continue;
}
if process.cmd().get(1).map_or(true, |arg| arg != "--server") {
continue;
}
let Ok(process_path) = std::fs::canonicalize(process.exe()) else {
continue;
};
if process_path != current_exe_path {
continue;
}
let Some(uid) = process.user_id().map(|uid| **uid as u32) else {
// Root CLI management commands need a stable matching `--server` target.
// If this key process races during enumeration, failing the command is clearer
// than silently skipping it; `--server` is not expected to exit frequently.
bail!("Failed to read --server process uid");
};
server_uids.push(uid);
}
Ok(server_uids)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn user_main_ipc_server_uid() -> ResultType<u32> {
let server_uids = running_server_uids_for_current_exe()?;
#[cfg(target_os = "linux")]
let prefer_root = crate::platform::linux::is_login_screen_wayland();
#[cfg(target_os = "macos")]
let prefer_root = false;
select_server_uid_for_user_main_ipc(&server_uids, active_uid(), prefer_root)
}
pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType<ConnectionTmpl<ConnClient>> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let use_user_main_ipc = USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.get());
let is_root_main_ipc =
unsafe { hbb_common::libc::geteuid() == 0 } && postfix.is_empty() && use_user_main_ipc;
if is_root_main_ipc {
let uid = user_main_ipc_server_uid()?;
let path = Config::ipc_path_for_uid(uid, postfix);
return connect_with_path(ms_timeout, &path).await;
}
let path = Config::ipc_path(postfix);
return connect_with_path(ms_timeout, &path).await;
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let path = Config::ipc_path(postfix);
connect_with_path(ms_timeout, &path).await
}
}
#[cfg(target_os = "linux")]
pub async fn connect_for_uid(
ms_timeout: u64,
@@ -2002,7 +2135,16 @@ mod test {
assert!(std::mem::size_of::<Data>() <= 120);
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_service_ipc_path_is_shared_across_uids() {
assert_eq!(
Config::ipc_path_for_uid(0, crate::POSTFIX_SERVICE),
Config::ipc_path_for_uid(501, crate::POSTFIX_SERVICE)
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_ipc_path_differs_by_uid_for_cm() {
let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 };
@@ -2021,4 +2163,46 @@ mod test {
Config::ipc_path_for_uid(other_uid, postfix)
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_uses_active_uid_when_no_server_found() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[], Some(501), false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_uses_single_server_uid() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[501], None, false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_prefers_active_uid_with_multiple_servers() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), false).unwrap(),
501
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_prefers_root_on_wayland_login_screen() {
assert_eq!(
select_server_uid_for_user_main_ipc(&[0, 501], Some(501), true).unwrap(),
0
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_select_server_uid_fails_when_multiple_servers_are_ambiguous() {
assert!(select_server_uid_for_user_main_ipc(&[501, 502], None, false).is_err());
}
}

View File

@@ -607,27 +607,30 @@ pub(crate) fn log_rejected_windows_ipc_connection(
peer_session_id: Option<u32>,
expected_session_id: Option<u32>,
peer_is_system: Option<bool>,
peer_is_elevated: Option<bool>,
) {
static LOG_THROTTLE: OnceLock<Mutex<UnauthorizedIpcLogThrottle>> = OnceLock::new();
throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| {
if suppressed > 0 {
log::warn!(
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?} (suppressed {} similar events)",
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?} (suppressed {} similar events)",
postfix,
peer_pid,
peer_session_id,
expected_session_id,
peer_is_system,
peer_is_elevated,
suppressed
);
} else {
log::warn!(
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}",
"Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?}",
postfix,
peer_pid,
peer_session_id,
expected_session_id,
peer_is_system
peer_is_system,
peer_is_elevated
);
}
});
@@ -655,8 +658,14 @@ pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postf
#[cfg(windows)]
pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool {
let (authorized, peer_pid, peer_session_id, server_session_id, peer_is_system) =
stream.server_authorization_status();
let (
authorized,
peer_pid,
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
) = stream.server_authorization_status();
if !authorized {
log_rejected_windows_ipc_connection(
postfix,
@@ -664,6 +673,7 @@ pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
);
return false;
}
@@ -776,7 +786,14 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
fn server_authorization_status(
&self,
) -> (bool, Option<u32>, Option<u32>, Option<u32>, Option<bool>) {
) -> (
bool,
Option<u32>,
Option<u32>,
Option<u32>,
Option<bool>,
Option<bool>,
) {
let peer_pid = self.peer_pid();
let server_session_id = crate::platform::windows::get_current_process_session_id();
let peer_session_id =
@@ -786,20 +803,34 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
let peer_is_system = peer_is_system_result
.as_ref()
.and_then(|r| r.as_ref().ok().copied());
if server_session_id.is_none() && !peer_is_system.unwrap_or(false) {
// When the server session id cannot be determined, the session-id allow-path is
// disabled and only SYSTEM peers can be authorized.
log::debug!(
"IPC authorization: server session id unavailable; rejecting non-SYSTEM peer, peer_pid={:?}, peer_session_id={:?}",
peer_pid,
peer_session_id
);
}
let authorized = is_allowed_windows_session_scoped_peer(
let session_authorized = is_allowed_windows_session_scoped_peer(
peer_is_system.unwrap_or(false),
peer_session_id,
server_session_id,
);
let peer_is_elevated_result = if session_authorized {
None
} else {
peer_pid.map(|pid| crate::platform::windows::is_elevated(Some(pid)))
};
let peer_is_elevated = peer_is_elevated_result
.as_ref()
.and_then(|r| r.as_ref().ok().copied());
if server_session_id.is_none()
&& !peer_is_system.unwrap_or(false)
&& !peer_is_elevated.unwrap_or(false)
{
// When the server session id cannot be determined, the session-id allow-path is
// disabled and only privileged peers can be authorized.
log::debug!(
"IPC authorization: server session id unavailable; rejecting non-privileged peer, peer_pid={:?}, peer_session_id={:?}",
peer_pid,
peer_session_id
);
}
// Main IPC trusts same-session peers, LocalSystem, and elevated administrators.
// Service-scoped IPC channels keep their own stricter authorization paths.
let authorized = session_authorized || peer_is_elevated.unwrap_or(false);
if !authorized {
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) {
log::debug!(
@@ -808,6 +839,13 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
err
);
}
if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_elevated_result.as_ref()) {
log::debug!(
"Failed to determine whether peer process is elevated, pid={}, err={}",
pid,
err
);
}
}
(
authorized,
@@ -815,6 +853,7 @@ impl ConnectionTmpl<parity_tokio_ipc::Connection> {
peer_session_id,
server_session_id,
peer_is_system,
peer_is_elevated,
)
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "كلمة المرور مخفية"),
("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."),
("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "永久密码已设置(已隐藏)"),
("preset-password-in-use-tip", "当前使用预设密码"),
("Enable privacy mode", "允许隐私模式"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."),
("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."),
("Enable privacy mode", "Datenschutzmodus aktivieren"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -274,5 +274,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"),
("password-hidden-tip", "Permanent password is set (hidden)."),
("preset-password-in-use-tip", "Preset password is currently in use."),
("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Closed manually by the peer", "Cerrado manualmente por el par"),
("Enable remote configuration modification", "Habilitar modificación remota de configuración"),
("Run without install", "Ejecutar sin instalar"),
("Connect via relay", ""),
("Connect via relay", "Conectar a través de relay"),
("Always connect via relay", "Conéctese siempre a través de relay"),
("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"),
("Login", "Iniciar sesión"),
@@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Username missed", "Olvidó su nombre de usuario"),
("Password missed", "Olvidó su contraseña"),
("Wrong credentials", "Credenciales incorrectas"),
("The verification code is incorrect or has expired", ""),
("The verification code is incorrect or has expired", "El código de verificación es incorrecto o ha caducado"),
("Edit Tag", "Editar tag"),
("Forget Password", "Olvidar contraseña"),
("Favorites", "Favoritos"),
@@ -302,8 +302,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"),
("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"),
("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"),
("Start on boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Start on boot", "Iniciar al arrancar"),
("Start the screen sharing service on boot, requires special permissions", "Iniciar el servicio de pantalla compartida al arrancar, requiere permisos especiales"),
("Connection not allowed", "Conexión no disponible"),
("Legacy mode", "Modo heredado"),
("Map mode", "Modo mapa"),
@@ -326,8 +326,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Ratio", "Relación"),
("Image Quality", "Calidad de imagen"),
("Scroll Style", "Estilo de desplazamiento"),
("Show Toolbar", ""),
("Hide Toolbar", ""),
("Show Toolbar", "Mostrar herramientas"),
("Hide Toolbar", "Ocultar herramientas"),
("Direct Connection", "Conexión directa"),
("Relay Connection", "Conexión Relay"),
("Secure Connection", "Conexión segura"),
@@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Security", "Seguridad"),
("Theme", "Tema"),
("Dark Theme", "Tema Oscuro"),
("Light Theme", ""),
("Light Theme", "Tema claro"),
("Dark", "Oscuro"),
("Light", "Claro"),
("Follow System", "Tema del sistema"),
@@ -355,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Audio Input Device", "Dispositivo de entrada de audio"),
("Use IP Whitelisting", "Usar lista de IPs admitidas"),
("Network", "Red"),
("Pin Toolbar", ""),
("Unpin Toolbar", ""),
("Pin Toolbar", "Anclar herramientas"),
("Unpin Toolbar", "Desanclar herramientas"),
("Recording", "Grabando"),
("Directory", "Directorio"),
("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"),
("Automatically record outgoing sessions", ""),
("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"),
("Change", "Cambiar"),
("Start session recording", "Comenzar grabación de sesión"),
("Stop session recording", "Detener grabación de sesión"),
@@ -368,7 +368,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable LAN discovery", "Habilitar descubrimiento de LAN"),
("Deny LAN discovery", "Denegar descubrimiento de LAN"),
("Write a message", "Escribir un mensaje"),
("Prompt", ""),
("Prompt", "Solicitud"),
("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"),
("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."),
("Disconnected", "Desconectado"),
@@ -616,9 +616,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("During service is on", "Mientras el servicio está activo"),
("Capture screen using DirectX", "Capturar pantalla con DirectX"),
("Back", "Atrás"),
("Apps", ""),
("Volume up", "Bajar volumen"),
("Volume down", "Subir volumen"),
("Apps", "Aplicaciones"),
("Volume up", "Subir volumen"),
("Volume down", "Bajar volumen"),
("Power", "Encendido"),
("Telegram bot", "Bot de Telegram"),
("enable-bot-tip", "Si activas esta característica puedes recibir código 2FA de tu bot. También puede funcionar como notificación de conexión."),
@@ -651,7 +651,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Actualizar portapapeles del cliente"),
("Untagged", "Sin itiquetar"),
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
("Accessible devices", ""),
("Accessible devices", "Dispositivos accesibles"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"),
("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."),
("Use D3D rendering", "Usar renderizado D3D"),
@@ -689,9 +689,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Use WebSocket", "Usar WebSocket"),
("Trackpad speed", "Velocidad de trackpad"),
("Default trackpad speed", "Velocidad predeterminada de trackpad"),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("Numeric one-time password", "Contraseña numérica de un solo uso"),
("Enable IPv6 P2P connection", "Habilitar conexión IPv6 P2P"),
("Enable UDP hole punching", "Habilitar perforación de agujero UDP"),
("View camera", "Ver cámara"),
("Enable camera", "Habilitar cámara"),
("No cameras", "No hay cámaras"),
@@ -708,8 +708,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."),
("Supported only in the installed version.", "Soportado solo en la versión instalada."),
("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Preparing for installation ...", "Preparando instlación..."),
("Show my cursor", "Mostrar mi cursor"),
("Scale custom", "Escala personalizada"),
("Custom scale slider", "Control deslizante de escala personalizada"),
("Decrease", "Disminuir"),
@@ -721,28 +721,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Mostrar joystick virtual"),
("Edit note", "Editar nota"),
("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", ""),
("ScrollEdge", "Desplazamiento de pantalla"),
("Allow insecure TLS fallback", "Permitir conexión TLS insegura de respaldo"),
("allow-insecure-tls-fallback-tip", "De forma predeterminada, RustDesk verifica el certificado de servidor para protocolos que usen TLS.\nCon esta opción habilitada, Rustdesk volverá al paso de omisión de verificación y procederá en caso de fallo de verificación."),
("Disable UDP", "Inhabilitar UDP"),
("disable-udp-tip", "Controla si se usa TCP solamente.\nCuando esta opción está activa, RustDesk no usará más el puerto UDP 21116, en su lugar se usará el TCP 21116."),
("server-oss-not-support-tip", "NOTA: El servidor RustDesk OSS no incluye esta característica."),
("input note here", "Introducir nota aquí"),
("note-at-conn-end-tip", "Pedir nota al finalizar la conexión"),
("Show terminal extra keys", "Mostrar teclas extra del terminal"),
("Relative mouse mode", "Modo de ratón relativo"),
("rel-mouse-not-supported-peer-tip", "El modo relativo de ratón no está soportado por el par."),
("rel-mouse-not-ready-tip", "El modo relativo de ratón aún no está preparado. Por favor, inténtalo de nuevo."),
("rel-mouse-lock-failed-tip", "Ha fallado el bloqueo del cursor. El modo relativo del ratón ha sido inhabilitado."),
("rel-mouse-exit-{}-tip", "Pulsa {} para salir."),
("rel-mouse-permission-lost-tip", "Permiso de teclado revocado. El modo relativo del ratón ha sido inhabilitado."),
("Changelog", "Registro de cambios"),
("keep-awake-during-outgoing-sessions-label", "Mantener la pantalla activa durante sesiones salientes"),
("keep-awake-during-incoming-sessions-label", "Mantener la pantalla activa durante sesiones entrantes"),
("Continue with {}", "Continuar con {}"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("Display Name", "Nombre de pantalla"),
("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."),
("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."),
("Enable privacy mode", "Habilitar modo privado"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."),
("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."),
("Enable privacy mode", "Activer le mode de confidentialité"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "એક્સેસિબલ ઉપકરણો"),
("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"),
("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"),
("Use D3D rendering", ""),
("Printer", "પ્રિન્ટર"),
("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."),
("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."),
@@ -743,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."),
("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "सुलभ डिवाइस"),
("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"),
("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"),
("Use D3D rendering", ""),
("Printer", "प्रिंटर"),
("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"),
("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"),
@@ -742,5 +743,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "प्रदर्शित नाम"),
("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"),
("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."),
("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."),
("Enable privacy mode", "Adatvédelmi mód aktiválása"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "È impostata una password permanente (nascosta)."),
("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."),
("Enable privacy mode", "Abilita modalità privacy"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"),
("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."),
("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."),
("Enable privacy mode", "개인정보 보호 모드 사용함"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"),
("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"),
("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"),
("Use D3D rendering", ""),
("Printer", "പ്രിന്റർ"),
("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."),
("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."),
@@ -742,5 +743,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "ഡിസ്‌പ്ലേ പേര്"),
("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."),
("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -743,6 +743,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Naam Weergeven"),
("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."),
("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."),
("Enable privacy mode", "Schakel privacymodus in"),
("Enable privacy mode", "Privacymodus inschakelen"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Ustawiono (ukryto) stare hasło."),
("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -740,9 +740,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"),
("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
("Continue with {}", "Continuar com {}"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("Display Name", "Nome de Exibição"),
("password-hidden-tip", "A senha permanente está definida como (oculta)."),
("preset-password-in-use-tip", "A senha predefinida está sendo usada."),
("Enable privacy mode", "Habilitar modo de privacidade"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -540,7 +540,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."),
("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"),
("Check for software update on startup", "Verifică actualizări la pornire"),
("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."),
("upgrade_rustdesk_server_pro_to_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."),
("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."),
("Filter by intersection", "Filtrează prin intersecție"),
("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"),
@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."),
("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Установлен постоянный пароль (скрытый)."),
("preset-password-in-use-tip", "Установленный пароль сейчас используется."),
("Enable privacy mode", "Использовать режим конфиденциальности"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "Parola gizli"),
("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"),
("Enable privacy mode", "Gizlilik modunu etkinleştir"),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", "固定密碼已設定(已隱藏)"),
("preset-password-in-use-tip", "目前正在使用預設密碼"),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
("Enable privacy mode", ""),
("allow-remote-toolbar-docking-any-edge", ""),
].iter().cloned().collect();
}

View File

@@ -614,6 +614,7 @@ fn authorize_service_scoped_ipc_connection(
peer_session_id,
expected_active_session_id,
peer_is_system,
None,
);
return false;
}