Commit Graph

5062 Commits

Author SHA1 Message Date
rustdesk
8b7f3b8b75 port forward: a legacy window stays legacy until it is reopened
Review: the merged `Claimed | Legacy` arm gave a legacy window a hot
transition to a tunnel — every accept re-negotiated, and a peer upgraded
while the window stayed open was promoted underneath live connections.
The product does not need a mode switch inside a window's lifetime, and
the transition was extra state-machine surface for nothing: reopening
the window picks up an upgraded peer.

The two arms are separate again. `Claimed` negotiates once and the
peer's answer fixes the window's mode. `Legacy` logs in for every accept
as before, asks for no tunnel — `LoginConfigHandler::port_forward_mux`
carries the request per login, so the raw pipe never has to talk to a
peer that thinks it agreed to multiplex — and ignores what the peer
reports. Both arms keep skipping a local socket that hung up during
login.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:58:00 +08:00
rustdesk
f69e706cfa port forward: apply the whole-branch review
Correctness:
- listen(): the Legacy arm is merged with the Claimed arm. On its own it
  ignored outcome.local_eof, so a client that hung up during login still
  got a target connect, an audit record and a CM row on the controlled
  side, and ignored outcome.mux, so a peer upgraded while a legacy window
  stayed open answered as a tunnel while the controller went raw.
- Refusal dialogs are deduplicated per quiet spell (10 s) rather than per
  tunnel lifetime; the lifetime set went silent for the rest of a
  long-lived window after the first burst.
- Android's CM listener handles UpdatePortForward; it fell into `_ => {}`.
- relay_socket_to_tunnel reads into one scratch buffer per channel and
  sends an exact-size copy. A frame owning its 64 KiB read allocation
  pinned it until sent, once per byte on interactive traffic.

Consistency and cleanups:
- The controlled side's refusal text is the raw pipe's wording, RDP
  substitution included.
- connection.rs: the PortForwardChannel arm is a one-line hook, the CM
  label is pushed from the 1 s tick alone, and the unreachable inner.tx
  fall-through is gone.
- The Ready enum is removed; wait_ready() returns Option<Claim>.
- SendCredit::add wakes with notify_one alone.
- on_ui_command() replaces the two ui_receiver handlers in listen().
- TunnelHandle is no longer re-exported (unused-import warning).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
e21d875ae7 Use on_error for refused-channel dialog in tunnel_loop
Redirect the refused-channel error through the standard on_error path
instead of calling msgbox directly, for consistency with other errors
in the port-forward flow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
3aca5a4c33 port_forward_mux: report a refused channel's reason as an error dialog
The controlled side already answers a refused port-forward channel with
opened { success: false, message }; on the multiplexed path TunnelHandle::
on_frame only logged that message at debug and closed the channel, so the
user saw a closed connection with no explanation, worst on the RDP path
where only the RDP client's own error remained. on_frame now returns the
message the window should show, deduplicated per distinct reason (capped
at MAX_REPORTED_OPEN_ERRORS) so one page load's dozen refused connections
surface one dialog per reason instead of a dozen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
a58ecef9f6 port_forward_mux: cap send credit and other final review fixes
Fix 1 (critical): clamp SendCredit to MAX_SEND_CREDIT (= CHANNEL_WINDOW)
in both new() and add(), so a peer with tunnel permission can no longer
advertise an unbounded window and force the controlled side's unbounded
FrameSink::Direct sink to buffer unlimited target data per channel.

Fix 2: rename the "starve" test to many_channels_echo_concurrently and
drop its (untrue) starvation claim, since it opens every channel before
the bulk transfer starts. Add a_channel_opened_during_a_bulk_transfer_
is_served_promptly, which opens the small channel while the bulk one is
demonstrably mid-flight.

Fix 3: only look up the tunnel permission for `open` frames in the
PortForwardChannel arm of on_message, instead of once per data frame.

Fix 4: two rustfmt deviations in connection.rs (matches! wrapping and a
tuple literal), fixed by hand without a blanket cargo fmt run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
59db691af5 port_forward_mux: fix bulk test's premature half-close, pin the half-close limitation
many_channels_echo_concurrently_and_a_bulk_one_does_not_starve_them dropped
its bulk write half as soon as writing finished, which shuts down the write
side of the socket and, by design (see the design doc's TCP half-close
non-goal; today's run_forward does the same), ends the whole channel. Keep
the write half alive until the reader is done so the test measures
starvation, not half-close. Add a_local_half_close_ends_the_whole_channel to
pin that limitation in code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
85087c81bb port_forward_mux: end-to-end tests over a loopback tunnel
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
115cbabb10 port forward: fix round 1 review findings
Drop the mux default-false assignment now that definite-assignment proves
every path that reads it has set it; the enable-port-forward-mux config
commit picks up the missing attribution trailers; the default-on test
pins the enable- prefix itself rather than option2bool's weaker fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
48fe740ad8 port forward: share one multiplexed tunnel across a window's listeners
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
a05247a07a port_forward_mux: publish Muxed before spawning the tunnel loop
Publishing after spawn let a loop that dies immediately reset the state
first, so the later publish pinned it at Muxed with a dead handle
forever. Also adds a test pinning open-before-data ordering across many
concurrently opened channels, and drops an unused Clone derive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
50a6f42bc7 port_forward_mux: controller tunnel with a single-writer stream loop
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
5daa020516 cm: update a port-forward row's targets as tunnel channels come and go
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
3fdbaf5006 server: multiplexed port-forward connections stay in the protobuf loop
Wire PortForwardMux into Connection: take the multiplexed path at login
when the controller sets PortForward.multiplex, route
PortForwardChannel frames to it from on_message, sweep the channel
table's targets after open/close, and clean it up on connection close.

Introduce is_port_forward() (socket-based or multiplexed) and use it
at the four sites that classify the connection, so a multiplexed
connection stays in the message loop, gets TestDelay keepalives, and
reports features.port_forward_mux in PeerInfo. The three sites that
break into the raw pipe loop or gate the keepalive still check
port_forward_socket specifically, since a multiplexed connection must
not take that path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
6f97250234 server: PortForwardMux channel table and per-channel tasks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
867a012db4 port_forward_mux: credit-windowed relay halves and channel coordinator
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
65f7965d15 port_forward_mux: fix RecvWindow counter overflow on long transfers
Replace cumulative accounting (granted/received) with remaining credit
tracking to prevent u32 overflow after 4 GiB of data on a single channel.
Wire behavior is identical, but the fix allows large file transfers
without mid-stream channel closure.

Add regression test for 8 GiB transfer to verify fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
899bfb3f69 port_forward_mux: window accounting and channel frame builders
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
rustdesk
d24ce8b62c hbb_common: bump to the port-forward-mux proto
Also latches PortForward.multiplex into login_scope_digest, which
destructures PortForward's fields exhaustively by design (a new field
must be latched or deliberately ignored to compile).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-04 20:57:59 +08:00
RustDesk
978c901f49 port forward: a login's target and challenge travel with its own accept (#16069)
* port forward: mappings take turns at the window's login slots

A window's mappings log in concurrently, and each login is built from
the shared `LoginConfigHandler`: `create_login_msg` reads
`port_forward`, which `listen()` set before connecting, and
`handle_login_from_ui` reads `hash`, which the last `Hash` to arrive
set. Two mappings logging in at once could swap targets, bridging a
local socket to the other's target, and answer each other's challenge,
failing one login. The window's password prompt is broadcast to every
mapping, so one whose `Hash` had not arrived answered with whatever the
handler held.

Each mapping now fills `port_forward` and `hash` and sends its login
under a per-window turn lock, and keeps its own `Hash` beside the
connection: a password typed before it arrived is left to the mapping
that prompted, which stores the salted password in the shared handler
for the others to log in with.

The fix stays in `port_forward.rs`. `LoginConfigHandler` gains the lock
and a setter for its private `hash`; `Interface`, `Session` and the
login functions keep their signatures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* port forward: the lock and the hash setter are crate-private; test the hash that arrives late on its real path

Both exist only so `port_forward.rs` can reach the handler's private
`hash`; neither is API.

The test for a password typed before a connection's hash ended by
answering the prompt again once the hash was there. What happens in the
code is that the hash's arrival runs `handle_hash`, which logs in with
the password the prompting mapping stored; the test now ends there, with
no preset password. Answering the prompt with one's own challenge while
the handler holds another's is a test of its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* port forward: a password typed before the connection's hash answers it when it comes

The previous commit dropped such a password, counting on the mapping
that prompted having stored it in the shared handler by the time this
connection's `Hash` arrived. The broadcast wakes both mappings at once
and `select!` picks between a ready `Hash` and a ready password at
random, so this one could reach `handle_hash` first, find the handler
empty, and prompt again.

The connection keeps the password until its `Hash` arrives and answers
with it then. `login_from_ui` takes the challenge it answers; the wait
is `connect_and_login`'s, in `hash_arrived`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 19:39:09 +08:00
RustDesk
50c4e435de connection: apply the non-video send timeout once the type is known (#16063)
* connection: apply the non-video send timeout once the type is known

`Connection::start` set the send timeout before the login request had
arrived, when `file_transfer`, `port_forward_socket` and `terminal` were
all still unset, so every connection got `SEND_TIMEOUT_VIDEO` (12 s) and
the `SEND_TIMEOUT_OTHER` branch never ran. A file transfer, terminal or
port forward whose peer stopped draining for 12 s — a Wi-Fi roam, a VPN
reconnect — was dropped.

The type-specific timeout is now set in `on_message` right after the
login request's union has been matched; `start` keeps the video figure
for the login phase.

`SEND_TIMEOUT_OTHER` also drops from 120 s to 30 s, the same horizon as
the 30 s read timeout: the timeout wraps a single `send`, so it only
fires when the peer makes no progress at all for that long, and beyond
30 s the read check would declare the same peer dead anyway. The raw
port-forward pipe's write to its local target shares the constant and
moves with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* connection: keep the raw port-forward local write at 120 s

`SEND_TIMEOUT_OTHER` also bounded `forward.send` in
`try_port_forward_loop`, the write to the local target, whose own idle
timeout is an hour. Lowering it to 30 s made a target that stops
draining for half a minute drop the whole tunnel. That write gets its
own constant at the value it always had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* connection: keep the non-video send timeout at its long-standing 120 s

Lowering `SEND_TIMEOUT_OTHER` to 30 s was a policy change on top of the
bug fix, argued from the 30 s read timeout, which measures something
else and cannot even run while a send is blocked. The constant goes
back to `SEND_TIMEOUT_VIDEO * 10`, where it has been since 2021, and
the raw port-forward loop's local write shares it again. What remains
is the fix alone: the type-specific timeout is chosen once the login
request has said what the connection is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 17:12:24 +08:00
fufesou
d5c6d0f6b7 fix(custom client): msi update, preserve exe name (#16057)
Keep the configured app name casing when renaming
the updated executable so legacy MSI custom actions
can terminate custom client processes.

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-09-04 15:49:50 +08:00
Daniel Nylander
b6ff62c74b l10n: fill remaining Swedish entries (#16050)
Signed-off-by: Daniel Nylander <github@danielnylander.se>
2026-09-04 15:26:21 +08:00
Mariano Abad
3f93005be2 fix(drm): deliver a rotated output upright (#15886) (#15889)
* fix(drm): deliver a rotated output upright instead of sideways (#15886)

the compositor draws a rotated desktop sideways into the landscape
scanout and the physically turned monitor straightens it locally, so the
raw scanout the drm path ships reads sideways in the viewer, and nothing
rebroadcasts on rotation because the framebuffer size never changes.

the capturer now resolves the output transform once per session from the
wayland enumeration, turns accepted frames upright into its own buffer,
and sizes the session in rotated dimensions. the advertised list swaps
width and height for 90/270 outputs, which also makes a mid-session
rotation a topology change that restarts the service, and computes scale
from the post-swap width so a rotated 1:1 monitor no longer advertises
scale 16/9. a non 4-byte format on a rotated session is a hard error and
degrades through the existing health path.

the greeter path where no compositor answers keeps today's behavior:
there is no transform source there. hbb_common carries the new transform
field (submodule bump).

* fix(drm): drop the wayland snapshot when the live layout drifts (#15886)

the advertised list is augmented from the cached wayland snapshot and
nothing invalidated it mid-session, so a rotation the 1.5 s live poll
plainly saw never reached check_changed: the poll reads live, the
advertise kept serving the pre-rotation snapshot. measured before this
commit: transform applied and held, 'desktop layout changed' logged,
zero new encoders. cleared only when the poll saw an actual change, so
the probe cost stays tied to real layout events; after it, the same
stimulus rebuilds into a 1080x1920 encoder within a poll turn.

* refactor: trim comment density to the file norm

* chore: bump hbb_common to the transform field from rustdesk/hbb_common#586

pinned to the #586 commits atop the current pin rather than main tip:
main also carries an unrelated config-keys refactor the app has not
adopted yet, and both #586 commits are reachable upstream through the
merge.

* fix: advertise a lone rotated output at delivered size, one snapshot per session

review findings, both real: the 90/270 swap sat below the origin-only
cut, so a single rotated output advertised unrotated dimensions while
the capturer delivered rotated frames; and transform and origin came
from two get_displays() reads that could straddle a cache invalidation.
the swap now precedes the cut (logical-scale adoption stays multi
output), and new() resolves one snapshot for transform, origin and the
session size, with tests for both. comments trimmed to the three-line
guideline.

* fix(drm): rotate every space the rotation touches, not just the pixels

review findings on #15889, all verified against the code first.

the uinput rect's single-display branches now serve the delivered
orientation, so the pointer reaches the whole of a rotated screen (1).
DisplayRect carries the transform, making 0/180 and 90/270 flips
visible to the drift comparison (5), and the drift poll is an edge on
live-vs-previous rather than a level against the baseline, so the cache
clear fires once per real layout event instead of every 300 ms
forever (9). on the drm path the baseline promotes together with the
clear, so the remap and the client rebase never correct the same origin
delta twice (7), and the poll now runs above the login-screen return,
which was the one place with no other invalidation trigger (6).

a snapshot generation gives a rotation a rebuild path at last (3, 4):
clears bump it, the capturer records it at build, and a stale
generation asks for a rebuild without counting against display health.
the cursor bitmap and hotspot turn with the same session transform the
frames use (11). original_resolution follows the 90/270 swap (12). the
transform comes only from an identity match, never the layout-order
fallback (13), and a missing wayland snapshot at build logs the degrade
instead of silently pinning an unrotated session (10).

unrotate_bgra's body is now libyuv's ARGBRotate, which the existing
direction tests pin to the measured anchor (14). 180 stays master
behavior: i915 advertises hardware rotate-180 and wl_output cannot tell
hardware from software rotation, so undoing it blind would invert an
already-upright frame; it needs the plane rotation property on the
wire (2). the pipewire fallback guard's comment now states the rotated
reality it compares (8).

* fix(drm): one owner for the layout generation, one identity rule for rotation

adversarial pass over the previous commit, three structural findings.

the generation bump rode on the cache clear, which every video-service
start also executes, so any session init or restart tore down every
other live capturer, with no damping against a ping-pong between two
displays. the bump now has a single owner: the edge-detected layout
change in the display-service poll. cache clears are side-effect free
again, and a two-display session survives a third session's init with
zero spurious rebuilds.

the advertise side swapped dimensions for a layout-order-fallback match
while the capturer's transform refused such matches, splitting
advertised size from delivered frames into a black screen. both sides
now key off the same identity-match pass (identity_matches), so a
guessed assignment rotates nothing anywhere.

an edge observed while the drm verdict was transiently non-available
was consumed unpromoted, leaving a rotation sideways for the session;
it now stays owed until the verdict returns. an enumeration that failed
at build pinned transform 0 forever with a warn promising a retry that
did not exist; a missing snapshot now makes the first successful poll
an edge, so the degrade is bounded by the outage. the multi-display
missing-logical-size fallback serves delivered orientation, stale docs
zhou named are updated, and the resolutions list stays mode-space on
purpose: resolution changes ride xrandr, which is inert on this path.

* fix: transpose-tolerant fallback size check, log a rejected rotate geometry

whether a portal stream's caps arrive rotated on a 90/270 output is
unmeasured either way (pipewiresrc does not apply
SPA_META_VideoTransform), and this guard has already broken two readers
who reasoned from its comment - so the size half now accepts either
orientation instead of gambling a permanent offline on one. a source
stride shorter than a row logs the rejected geometry instead of
publishing a silent black frame. comments trimmed to the guideline and
the stale sole-test claim updated.

* fix(wayland): never serve a transposed PipeWire stream

The fallback accepted a stream whose dimensions were the advertised
display's transposed, but CapturerInfo keeps the stream dimensions,
nothing on the wayland side ever reconciles the client afterwards,
and the flutter renderer drops every frame whose size differs from
the advertised display - a permanently blank fallback. Accept only
the exact orientation; a transposed pair now falls into the existing
bail, the display is advertised offline, and the client recovers by
re-enumerating.

* fix(drm): keep the cursor consistent with the session transform

Two holes from the same review pass. The wire cursor id hashes only
the plane pixels and geometry, so a stream rebuilt under a new
transform resent the SAME id and the client's by-id cursor cache kept
the old orientation until the shape itself changed; fold the session
transform into the served id. And a cursor racing new()'s transform
store was processed with transform 0 and never corrected, since the
producer resends only on a shape change; hold that cursor and replay
it once the transform is in - the receive loop wakes at least every
200 ms, so the replay is prompt even on an idle wire.

* fix(wayland): the single-display carve-out must not forgive a transposed stream

The carve-out forgives a size difference (a Full Workspace stream may
report the workspace rather than the mode), but a transposed pair is
the same served-vs-advertised orientation split the previous commit
rejects, and it blanks the client the same way.

* fix(drm): a lone display with a rejected fallback is honestly offline

The transposed rejection promised 'advertised offline', but the
lone-display carve-out in mark_demoted_displays kept the display
online on the grounds that the whole-desktop fallback remains usable
- which is exactly what the rejection just refuted. The video service
then restart-looped against a stream nothing can serve, rebuilding
the portal session about once a second, while the client saw a
display list that lied.

Record the geometry rejection in the display health and let it end
the carve-out; a delivered frame or the demote-cooldown re-arm clears
it, so a recovered output comes back on its own.

* ci: retrigger, the previous run died in the actions outage (all root jobs at exactly 8m)

* fix(drm): a blind capturer owes a rebuild, and name matches reserve globally

Two of the review's findings. A capturer built during a failed wayland
enumeration recorded nothing durable: a later successful enumeration
refills the cache, wayland_snapshot_missing goes false, and the first
live poll sees no edge - the session stays sideways until an unrelated
change. The build now latches that it ran blind and the layout poll
consumes the latch into the existing owed-promotion machinery.

And the identity matcher ran per-connector, so a resolution guess for
an earlier connector could steal a later connector's exact name match
and pin its rotation on the wrong output. Names now reserve in a
global first pass; resolution pairing runs on the remainder only when
forced - one free output and one unmatched connector at that size.

* fix(drm): consume the blind-build latch even on a live-changed poll

Adversarial pass on the previous commit: the short-circuit left the
latch set on exactly the poll where live_changed fired (the common
blind-recovery ordering, since a failed enumeration is not cached and
failed_init makes the first successful poll an edge), and the stale
latch then bought a second, spurious promotion one poll later,
tearing down the freshly rebuilt capturer. The latch is now taken
unconditionally so both edge sources merge into one promotion.

* fix(wayland): hand over a layout change the poll has not seen yet

set_wayland_layout_baseline clears live, which is the edge detector's only
memory of the previous layout. ensure_inited calls it at the top of every video
service start, so a second monitor service starting between a rotation and the
next 1.5s poll recorded the rotated layout as the baseline: the poll then found
baseline == live_rects, owed no promotion, and the first capturer kept its old
transform. Under mutter's software rotation the framebuffer size does not
change and the wayland display-change check is disabled, so the stream stayed
sideways until the next layout event.

The setter now arms the promotion itself when the outgoing live differs from
the incoming baseline, which is the one choke point every caller goes through.
An empty incoming baseline is the DRM-union fallback and proves nothing.

* fix(wayland): the edge detector needs a memory a session init cannot erase

The baseline reset was also the edge detector's memory, so two session inits
straddling a rotation left nothing to compare the next poll against. Keep the
observed layout separate from the per-session input baseline; before the first
poll the outgoing baseline seeds it.

* fix(wayland): a capturer records the layout it was built on

ensure_inited() runs the wayland query before the capturer exists, and a failure
there saves an empty baseline. The capturer's own retry can succeed a moment
later and build on that layout, and because the build was not blind nothing
latched it, so a rotation before the first poll had no memory to be an edge
against and the stream stayed at the old transform.

The build now seeds the edge detector when nothing else has, and only then, so a
capturer built later cannot overwrite what the poll is keeping.

* fix(wayland): keep a capturer record that lost the race with the first poll

The constructor reads its wayland snapshot and records it in the edge
detector in two steps, and the layout poll can land between them. After a
failed session init (empty baseline) the constructor takes layout A and
publishes it, the output rotates, and the poll reads B live: nothing is
recorded yet and the snapshot is present, so it is no edge, and observe()
sets seen=B. The late note_capturer(A) then met a non-empty memory and was
dropped, so the capturer showed A while the detector held B, and B against
B never bumped the generation.

note_capturer now flags a build layout that disagrees with the poll's
memory instead of dropping it (overwriting is still wrong: on a
multi-display session that memory is what the other capturers were built
against). edge() reports the flag as an edge whatever the live layout is,
observe() consumes it right after, and a session init's baseline reset
leaves it alone. Regression test for the interleaving, with the promotion
consuming it, a baseline reset in between, and an agreeing late record as
the control.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL

* fix(wayland): a late capturer record from a promoted generation is not a second edge

The record can also land after the poll consumed an edge but before the
bump it promotes, or after the bump with a snapshot taken before it. That
capturer is stale by generation and rebuilds on its own, but the flag it
raised survived the promotion, and the next poll spent a second promotion
on the freshly rebuilt capturers.

Tag the record with the generation the capturer read before taking its
snapshot and count it as an edge only while that generation is current;
the newest generation wins when two records land. Regression test for the
consumed-edge interleaving, with a disagreeing record at the promoted
generation and a stale record after a fresh one as controls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL

* chore: bump hbb_common to main tip

dc95b4f -> 05ed68f, a fast-forward: the flipped-transform warning and the
wlroots xdg-output positions (rustdesk/hbb_common#591, #592), 90-day logs,
the webrtc session cleanup deadlock fix and the hide-general-settings
option. No public API changes and no dependency changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZwSrP3DFA6ZiPKVHkU5dL

---------

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:10:01 +08:00
memory_clear
e4539fc304 Update cn.rs (#16041) 2026-09-03 10:08:30 +08:00
Maison da Silva
6dbd810454 Translate export-related strings to Portuguese (#16038)
Translate export-related strings to Portuguese
2026-09-03 08:51:05 +08:00
Stephan Paternotte
dfb5804dd0 Update nl.rs (#16036)
Re. export and import.
Without detailed information, reference or examples from en.rs, de.rs or fr.rs, I have simply translated the three strings verbatim
2026-09-02 21:49:37 +08:00
XLion
c312385ffd Update tw.rs (#16031)
* Update tw.rs

* Update tw.rs

* Update tw.rs
2026-09-02 14:36:49 +08:00
palmoni5
f28ac38ccf feat: optionally sync clipboard between connected sessions (#15934)
* feat(clipboard): optionally sync clipboard between connected sessions

Clipboard content received from a remote session is written to the local
clipboard with an owner marker, so the client clipboard loop deliberately
skips re-broadcasting it to avoid echo loops. As a result, text copied in
one remote window could not be pasted in another connected remote window.

Add an opt-in local option (allow-sync-clipboard-between-sessions) that
relays Clipboard/MultiClipboards messages received from one session to
all other connected sessions, excluding the source session. Per-session
clipboard permissions and view-only mode are still respected via the
existing send path, and the owner marker on the receiving peers prevents
any echo back.

Desktop (flutter) only; file clipboard is not affected.

* fix(lang): propagate sync-clipboard-between-sessions-tip to all locale files

Add the new key to template.rs and every locale file per the localization
convention, move the en.rs entry to the end of the list, and drop comments
that only restated the names next to them.

* fix(lang): add the 'Sync clipboard between sessions' label to the localization catalog

The checkbox label goes through translate(), so add it to template.rs
and every locale file so non-English locales can translate it. en.rs is
skipped since the English display text is identical to the key.

* fix(clipboard): check the source session's full clipboard permission before relaying

The relay was gated only by the incoming clipboard_allowed check
(!disable_clipboard && !view_only). Gate it with
is_text_clipboard_required() instead, which additionally respects the
source session's server_clipboard_enabled and server_keyboard_enabled
state, matching the predicate already applied to destination sessions.
A message arriving after the source permission was revoked (or from a
non-conforming peer) is no longer propagated to other sessions. The
existing local update_clipboard behavior is unchanged.

* fix(lang): translate the new clipboard sync entries in all locale files

Fill the 'Sync clipboard between sessions' label and its tooltip in
every locale file instead of leaving them blank, following each file's
existing terminology. template.rs keeps the empty master entries.
2026-09-01 10:47:26 +08:00
RustDesk
66ab0b87f6 Linux drop shell from service loop (#15979)
* perf(linux): stop the service loop from forking a shell per environment variable

The service loop re-derives the desktop every 500 ms, and every lookup on that
path forks. A healthy GNOME session spends ~104 process spawns a second, 8 full
`ps -u <uid>` scans and 2 full `ps aux` scans, to re-answer a question whose
answer has not changed. `get_env` alone is a `sh -c` pipeline of ~12 processes
per variable.

`get_envs` already reads `/proc` directly and was documented as the intended
replacement, so move the remaining `get_env` callers to it and delete it. The
xwayland probe drops from 4 pipelines (~48 processes) to one `/proc` walk, and
the pathological walk that #15952 was about drops from ~2900 processes to at
most 60 `/proc` walks. `get_cm` and `is_xwayland_running` read `/proc` instead
of forking `ps aux` and `pgrep -a`; `get_cm` also called `current_exe()` once
per line of `ps` output.

Selection semantics are preserved where they were load-bearing:

* `get_envs_of_newest` reproduces the `ps ... | tail -1` the removed pipelines
  used, so a variable the newest matching process does not have means moving on
  to the next pattern, never on to an older process that may belong to a session
  which has since logged out.
* `get_envs` keeps its own order (readdir) and its all-process ranking, so the
  existing `get_display_xauth_wayland` caller is unaffected. Only its handling
  of an exported-but-empty value changes: `DISPLAY=` no longer counts as found,
  where it used to satisfy a single-name query and return the empty value before
  a process holding a real one was examined.
* `get_envs_where` lets the caller state what a complete answer is. Ranking by
  how many of the requested names a process carries cannot know that `DISPLAY`
  is mandatory and the rest interchangeable, so it could rank a process holding
  three optional values above the one holding the pair that matters.

`is_xwayland_running` is scoped to the session's uid. The compositor starts
Xwayland as the session user, so another user's Xwayland -- a switched-away
session, a second seat -- used to route a pure-Wayland session into the Xwayland
probe, which has no display for it to find there.

Not addressed: this discovery path has never had any notion of the active
session, and filters by uid alone. Constraining candidates to the active session
is not possible for the most important one, since `xdg-desktop-portal` and its
backends run under `user@<uid>.service`, which spans sessions and carries no
`XDG_SESSION_ID`, no session cgroup and no audit sessionid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q5egQpH4q4GoXJiuMoTJ5t

* fix(linux): the newest-process walk must not answer with a grep or an older PID

Three findings from review of the commit before this one.

`/proc/<pid>/environ` failing to read left the walk on to the next PID, which in
`newest_first` mode is an older process -- possibly of a session that has since
logged out -- where the `ps ... | tail -1` pipeline this replaces stopped at the
one PID it had already picked. A read that fails is a process carrying none of
the requested names, not a process to skip. The `seen` latch that was meant to
hold the newest process is deleted: `accept` is reached once per matching
process, so returning on the first is what it already did.

The regex is matched against the whole `/proc/<pid>/cmdline`, where the pipeline
had a `grep -v 'grep'`. A user running `grep Xwayland` is otherwise the newest
match for that pattern and answers with whatever environment their shell had --
an X forwarding endpoint over ssh, say. This is the one place the walk still
differs from the `get_envs` it grew out of, which never had that filter and
could take an ssh `grep` over the portal it was looking for.

`get_envs` is left exactly as it was. Its completeness test was every requested
name *present*; stating it through `accept` turned it into every name *non-empty*
and, with the empty-value change that went with it, moved which process the
existing `get_display_xauth_wayland` caller settles on. `accept` is now told the
count and asks the question the loop it replaced asked. This supersedes the
`get_envs` bullet of the previous commit message: an exported-but-empty value
counts as found again, as it always did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QgsYAUYKDei1AM5yHJsMX

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:51:27 +08:00
Michael Clark
d4b06a6c5c fix: android: replace all-files access with scoped storage (#15602)
* fix: android: replace all-files access with scoped storage + system picker

Remove MANAGE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE, and
WRITE_EXTERNAL_STORAGE from the Android manifest. Remove
requestLegacyExternalStorage. Replace broad external storage with
app-scoped external storage for the file-transfer workspace.

File import uses the system file_picker. File export uses Android's
SAF ACTION_CREATE_DOCUMENT with path validation that restricts
export sources to app-owned directories.

Remove the external_path dependency.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: refine file import feedback

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: use SAF for file imports

Replace file_picker imports with Android's Storage Access Framework to avoid legacy storage permissions, stale cached files, and duplicate staging of large imports. Stream selected documents into app-scoped storage with failure-safe replacement, keep exports restricted to validated app storage roots, use filesDir for the internal fallback workspace, and remove legacy permissions contributed during manifest merging.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: keep file imports in the selected directory

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: reset projection and constrain file workspace

Release capture resources when media projection is revoked externally. Keep Android local file navigation within the app-scoped workspace.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: handle scoped storage start-up regressions. Allow zero digits in POSIX filenames by rejecting NUL explicitly, and initialise the app-specific home directory before the Android service starts the native server.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: update content resolver mode to use 'wt' instead of 'w' to prevent trailing bytes from old document whilst reporting sucess

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android, enforce file workspace boundary on the server, and unblock the ui thread.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: android: validate rename destinations against the app workspace bound file-operation paths. report rename failures, general import failures, and unregister / reregister projection when its onStop callback fires.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: reconnect was refreshing the directory with net entry instances, while selected items retained the old instances, it was reporting a selected item, but checkbox statue used object identity, and appeared unchecked. Fixed by reconciling by path and entry type before replacing the directory snapshot, rebinding valid selections, and dropping missing ones.

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix: (android) add SAF folder import and multi item export - import directories using ACTION_OPEN_DOCUMENT_TREE. Export multiple files, logs, and screen recordings via export buttons, add localisation keys for new actions

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>

* fix(android): harden scoped storage file handling

- create new SAF documents instead of overwriting export sources
- reject empty peer paths except for home directory reads
- report directory backup restore and cleanup failures
- resolve log export paths from the configured app name

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

* fix(android): harden scoped-storage file operations

- snapshot directory exports before writing to the destination
- query document provider metadata off the main thread
- reject invalid remote directories without read timeouts

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

* fix(android): handle SAF directory name collisions

- reject dot-segment folder names during import
- fail imports with duplicate document display names
- only reuse matching directories during export

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

* fix(android): handle SAF folder import collisions

Reject filesystem-equivalent destination names and
avoid showing a failure when folder overwrite is skipped.

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

---------

Signed-off-by: michaeljclarkk <104532890+michaeljclarkk@users.noreply.github.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-31 16:29:51 +08:00
fufesou
0b08a83d4b fix(file-transfer): improve large directory loading (#15830)
* fix(file-transfer): improve large directory loading

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

* fix(file-transfer): avoid failing newer directory reads

Track each remote directory request by its registered completer and only remove
the task when it still matches, preventing stale failures from affecting newer
requests for the same path.

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

* fix(file-transfer): handle slow directory listings safely

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

* fix(file transfer): correlate directory responses with requests

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

* fix(file transfer): prevent automatic directory responses from matching requests

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

* fix(file-transfer): handle large remote directory listings reliably

- build file rows lazily
- register remote reads before sending requests
- handle Home paths, stale responses, errors, and timeouts
- serialize same-path reads with different hidden-file options

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

* fix(file transfer): reduce diffs

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

* fix: build

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

* fix: invalidate pending dir reads on reconnect

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

* test(file-transfer): cover remote directory read lifecycle

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-27 13:01:11 +08:00
rustdesk
e9b81e3475 typo 2026-08-27 11:47:36 +08:00
RustDesk
7220f00410 fix(linux): a Wayland session without XAUTHORITY is not incomplete (#15978)
Fixes #15952.

Hyprland runs Xwayland without exporting `XAUTHORITY`, and
`get_display_xauth_xwayland` only returns once it has both `DISPLAY` and
`XAUTHORITY`. On such a session that condition is never met, so every refresh
runs the retry loop to the end: 10 rounds x 6 process patterns x 4 variables =
240 `get_env` calls, each a `sh -c` pipeline of ~12 processes starting with a
full `ps -u <uid> -f`. That is ~2900 fork/exec per refresh, and the service loop
repeats every 500 ms. The reporter measured a full core on a low-end laptop and
~60% of a core on a 13600KF.

The Wayland side answers for such a session, so accept `DISPLAY` together with
either `XAUTHORITY` or `WAYLAND_DISPLAY` + `DBUS_SESSION_BUS_ADDRESS`. The
portal answers on the first pattern, which ends the walk there, as it already
did on desktops that do export an xauth.

The loop also assigned all four variables unconditionally per pattern, so the
patterns that do not run on a given desktop blanked out what an earlier one had
answered with -- the portal's valid `DISPLAY=:1` included. That is why the
`--server` was then started with no `WAYLAND_DISPLAY` and no
`DBUS_SESSION_BUS_ADDRESS`. Candidates are now taken from one pattern as a whole
and ranked, so a later pattern replaces an earlier answer only by being better,
and a session that can only offer a compositor and a bus still keeps them.

A compositor that starts Xwayland on demand shows the same shape from the other
side: the portal came up before Xwayland did, so its environment carries a valid
`WAYLAND_DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` but no `DISPLAY`, and no pattern
here may ever produce one. That pair alone is a session the child server can be
started against -- it is exactly what `get_display_xauth_wayland` returns on --
so it outranks a bare `DISPLAY` and ends the retrying, while the rest of the
round still looks for something that completes the session.

Not specific to the drm build: the function is not feature-gated, and the commit
the report points at does not touch it.


Claude-Session: https://claude.ai/code/session_01Q5egQpH4q4GoXJiuMoTJ5t

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:39:59 +08:00
Jade
7c6e661fcc fix(linux): Set AppIndicator ID for tray-icon (#15981)
* set static AppIndicator ID in tray-icon init

allows DEs, eg. KDE to 'remember' the user's configuration of tray hidden/unhidden. see: https://github.com/rustdesk/rustdesk/discussions/15208

Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com>

* Update tray.rs

---------

Signed-off-by: Jade <5164609+gnosticJade@users.noreply.github.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-08-27 09:41:24 +08:00
Mariano Abad
3f207e91f6 fix(linux): a session logout should hand the peer to the login screen (#15905)
* fix(linux): a session logout should hand the peer to the login screen

Logging out closes every window in the session, the connection manager's
included, and its close handler kicks every peer with the reason a person
gets when they disconnect one by hand. That reason is the one thing the
client never retries on, so the remote session dies on a frozen frame
instead of reconnecting to the greeter that is already there.

The close carries nothing to tell the two apart: measured on KDE, the CM
receives no signal and logind still reports the session active at that
instant, and the server is killed within a few hundred ms either way, so
neither a state check nor a grace period can decide it. What is
distinguishable is the ACTION: disconnecting a peer is not the same event
as this window going away. So the window-close path now says so, and the
server ends the session without poisoning the retry; the Disconnect
button and the app's own close control keep kicking exactly as before.
Linux only, since that is where a logout closes the window.

Verified on plasma/sddm with a client attached: a logout now reconnects
to the greeter with no dialog, while closing the manager window still
shows Closed manually by the peer.

* fix(linux): close the tunnel too, and keep the web build compiling

Three seams the first pass missed. The web bridge is hand written, not
generated, so the new call needs its stub there or flutter build web
stops compiling - and that job is disabled in CI, so it would have gone
green. try_port_forward_loop is a second consumer of the same channel
and only knew Close, so a forwarded tunnel outlived the window it was
supposed to die with. And the variant had landed inside the DRM section,
whose comment says everything below it is drm-gated.
2026-08-26 18:26:26 +08:00
fufesou
0d917c6fa1 fix: remove dup translations (#15967)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-25 18:25:49 +08:00
Abdullah Kaleem
7cc82c1575 Add Urdu language support for UI strings (#15961)
* Add Urdu language support for UI strings till 329 line

Co-authored-by: Copilot <copilot@github.com>

* Add Urdu translations for additional UI strings

* Add Urdu language support in lang.rs

* Fix Urdu translations and remove unused keys in ur.rs

---------

Co-authored-by: Copilot <copilot@github.com>
2026-08-25 09:19:37 +08:00
jhertel
f07b6e2338 Correct Danish spelling, language and translation (#15943)
* Update da.rs

Corrected spelling, language and translation mistakes.

* Update da.rs

Missed one correction.
2026-08-24 17:22:03 +08:00
fufesou
a7deef02a2 fix(msi): keep only native ProductCode uninstall entry (#15891)
* fix(msi): keep only native ProductCode uninstall entry

Move installer state outside the Uninstall registry path,
clean up legacy duplicate entries, and use the MSI ProductCode
for updates and uninstalling.

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

* fix(msi): harden update and uninstall handling

- handle legacy EXE updates without an MSI ProductCode
- propagate MsiExec uninstall failures
- validate and XML-quote custom ARP values

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

* fix(msi): validate registry state before update and uninstall

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

* fix(msi): pass WindowsInstaller state to elevated sequence

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

* fix(msi): block unsupported MSI-to-EXE upgrades

- resolve native MSI state and ProductCode safely
- suppress reboot while preserving MSI uninstall results
- publish the resolved ARP install location
- skip invalid unrelated MSI uninstall entries

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

* fix(msi): fail uninstall when ProductCode is missing

Prevent known MSI installations from falling back to
EXE cleanup when the ProductCode cannot be resolved.

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

* fix(msi): do not abort update on ARP version write failure

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-22 17:49:00 +08:00
Saverio Miroddi
cbf9440281 Prefer active X11 session display (#15933)
* Prefer active X11 session display

* Update linux.rs

* fix(linux): keep the logind display only when it is a local one

`get_display_from_session` returns the value pam_systemd was handed at session
creation, and logind never updates it afterwards. That value is not always a
usable local display: it can be qualified with this host (`myhost:0`), name an
X forwarding endpoint (`localhost:10.0`), or be a bare `:`.

Taking it unconditionally is worse than taking nothing, because a non-empty
`self.display` suppresses every fallback below it, `get_display_by_user` and the
`:0` default alike. The stripping at the end of `get_display_x11` does not save
the last two cases either: it leaves `:` as is and turns `localhost:10.0` into a
local looking `:10.0`, either of which is then exported as DISPLAY and leaves the
session unreachable, where before this PR the host got a working `:0`.

Strip this host so `myhost:0` is still accepted as `:0`, leave `localhost` in
place, and require a display number after the colon. Anything else falls through
to the existing chain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

* docs(agents): prefer a little duplication over a restructure

The "Be minimally invasive" rules already ask for purely additive diffs, but not
in the case where the addition would otherwise reshape an existing function so
the two can share code. Repeating a few lines is the better diff there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKJxvTT6NQDEcnkWBx5bLA

---------

Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 00:28:12 +08:00
rustdesk
c45f7d2dd2 refactor is_public 2026-08-21 01:25:51 +08:00
RustDesk
798b73beb1 Update common.rs (#15924) 2026-08-21 00:52:31 +08:00
21pages
5679670506 fix(flutter): make Adjust Window reliable across desktop platforms (#15853)
* fix(flutter): make Adjust Window reliable across desktop platforms

  - Fix incorrect sizing on scaled displays by calculating the target from the
    rendered canvas scale and platform-specific window coordinate units.
  - Fix adjustments using the wrong monitor by querying the current remote
    window's screen, with the main window as fallback.
  - Fix stale geometry after fullscreen or maximized transitions by refreshing
    metrics before calculating and applying the target frame.
  - Fix fullscreen availability checks on Windows and macOS by predicting the
    restored window borders and caching each macOS window's pre-fullscreen work area.
  - Fix incorrect Linux work areas by handling GNOME Wayland fractional scaling
    and caching compositor/X11 work-area measurements when visibleFrame is wrong.
  - Prevent unsafe adjustments by rejecting invalid, oversized, or implausibly
    small target frames.
  - Avoid failures during window teardown by skipping adjustment when the view,
    screen, or native window frame is unavailable.

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

* fix(flutter): harden Adjust Window handling

  - Use the dynamic Linux resize edge when predicting restored window bounds.
  - Treat GNOME fractional-scaling lookup failures as unknown without repeating
    the lookup for the remote window.
  - Stop adjustment safely when native window calls fail during window teardown.

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

* fix(flutter): correct Linux monitor selection

Update window_size to use monitor height for vertical bounds, preventing incorrect screen selection with vertically stacked displays.

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

* docs(flutter): simplify Linux screen handling comments

Keep the source rationale concise and move platform measurements and investigation details out of the implementation.

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

* fix(flutter): align Adjust Window resize padding

Use the shared drag-to-resize padding for Linux restored-window predictions so menu validation matches the applied frame dimensions.

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

* fix(flutter): remove Adjust Window screen fallback

Return null when the current window screen is unavailable instead of using the main window's scale factor and work area.

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

* fix(linux): query Mutter monitor layout mode

  Use DisplayConfig.GetCurrentState instead of inferring scaling from
  experimental features, and handle Ubuntu's UI-scaled logical mode.

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

* fix(flutter): use native maximized state for Wayland cache

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

* fix(flutter): allow Adjust Window to fill work area

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

* fix(flutter): avoid racing screen info updates

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

* refactor(flutter): remove dead Adjust Window web plumbing

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

* fix(flutter): tolerate near-unity Wayland scale factors

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

* fix(flutter): harden window screen detection

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

* fix(linux): drop deprecated GNOME session detection

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

* fix(flutter): remove GNOME monitor layout mode flutter cache

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-08-19 15:08:58 +08:00
fufesou
b0008edcb5 refact: remove linux headless (#15866)
* refact: remove linux headless

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

* fix(linux): probe DRM availability asynchronously on login

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

* revert changes in drm_capturer.rs

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

* Update submodule hbb_common

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

* docs(linux): clarify DRM availability comments

Remove stale headless and unauthenticated-request
wording, and document the Available-only login-screen gate.

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

* fix(linux): remove unreachable session cleanup branch

Remove the obsolete empty-session path and
clarify the intended use of cached DRM availability.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-18 15:02:50 +08:00
RustDesk
6a27910f34 fix(wayland): back off the polling display lookups after a failure (drm) (#15865)
* fix(wayland): back off the polling display lookups after a failure (drm)

In drm builds an enumeration that fails with no endpoint named in the
environment falls back to the socket probe, which forks a child bounded by
seconds, and the display service asks again every 300 ms -- at a greeter
with no reachable compositor that is a probe child per turn, forever. Such
a failure now stamps a shared 5 s backoff, and only the polling callers
honor it: the 300 ms displays-changed check skips its turn and the 1.5 s
live layout poll returns no answer for that turn.

Only the failure that would fork stamps. A session server is spawned with
WAYLAND_DISPLAY set, so its failed connect bails in-process before any
fork; stamping there would buy nothing and cost recovery latency, so live
sessions keep master's behavior exactly. The stamp also survives
clear_wayland_displays_cache: it describes the seat, not the cache, and
the ~1/s capturer rebuild loop clears on every teardown -- dropping the
stamp with the cache would let that loop defeat the backoff and would
turn every post-hotplug failure into a "first" one forever.

The displays-changed check weighs the backoff against what is already
published. With nothing synced yet it always populates -- an unaugmented
DRM list beats the empty broadcast the send path would otherwise emit.
With a synced layout, a suppressed turn keeps it, and a fresh first
failure keeps it too; only a failure that persists across a backoff
replaces it with the DRM stack, so a hotplug at a failing seat converges
within one backoff while a transient failure never tears down a good
layout.

One-shot callers -- session init, pipewire stream setup, capturer info --
keep probing fresh through get_displays, whose failure semantics are
unchanged: replaying a transient failure there would latch an empty answer
into session-long state. Non-drm builds compile none of this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(wayland): log DRM lookup failure once

* fix(wayland): reset lookup warning after recovery

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-18 09:57:06 +08:00
rustdesk
14a4b197ad translations 2026-08-17 17:22:57 +08:00
Krik JIN
8ffe3117a5 feat(flutter): add mobile canvas lock (#15877)
* feat: add mobile canvas lock

* Update flutter/lib/models/model.dart

Remove redundant canvas-lock comment

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* remove redundant logic

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Krik Jin <isjinhk@outlook.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-17 17:04:58 +08:00
RustDesk
5a78be03e3 feat(rdp): title the mstsc window after the peer instead of "localhost" (#15781)
* feat(rdp): title the mstsc window after the peer instead of "localhost"

The RDP tunnel launched `mstsc /v:localhost:<port>`, so with several
sessions open every window is titled "localhost" and servers cannot be
told apart.

mstsc titles the session window after the launched .rdp file's base
name, so write a temp .rdp file (containing only the tunnel address)
named after the peer alias, cached hostname, or id, and launch that
instead. Falls back to the old /v: form when no usable name remains
after filename sanitization or the file cannot be written. Credential
handling is unchanged: cmdkey targets "localhost", which is still the
host mstsc resolves credentials against.

Fixes rustdesk/rustdesk#15775 (discussion)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(rdp): set mstsc title without temporary files

  Keep launching mstsc with /v so Default.rdp settings are preserved
  and unsigned RDP file warnings and policy restrictions are avoided.

  Track the launched mstsc process and reapply the peer name when the
  window title is reset during connection or reconnection.

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

* docs(rdp): clarify mstsc title limitation

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

* feat(rdp): show peer identity with hostname in mstsc title

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

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
2026-08-17 13:30:18 +08:00
RustDesk
7aa98d43cf Refact/plugin removal leftovers (#15864)
* fix(flutter): dispose the settings PageController and order dispose() correctly

`dispose()` began with `super.dispose()`, so the mixin chain marked the State
defunct before the WidgetsBindingObserver registration and the periodic timer
were released. The `PageController` was never disposed at all: `Get.delete`
only runs `onDelete()` for a `GetLifeCycleBase`, and a plain `ChangeNotifier`
is not one, so every open/close of the Settings tab leaked one controller with
its listener still attached.

Also guard `switch2page` on the `Rx<SettingsTabKey>` registration it actually
reads rather than only the `PageController` — now that both are really
deleted, a partial teardown would throw into the catch and silently open the
wrong tab — and re-check `mounted` after the await in the `_videoConnTimer`
tick, which `Timer::cancel` cannot stop once the body has started.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refact: finish the plugin-framework removal sweep

#15854 removed the feature but stopped short of its leftovers:

- `Uninstall`, `Enable`, `Disable`, `Options` and `Please install plugins`
  were consumed only by the deleted `flutter/lib/plugin/**`; drop them from
  template.rs and the 50 locale files (250 dead entries). `Update` and
  `Install` stay, still used by desktop_home_page.dart.
- The server no longer sends `PrvOnFailedPlugin`, and the client no longer
  offers to install plugins when privacy mode fails to turn on.
- Drop the MSI `F_Client_Plugins` / `F_Server_Plugins` localization strings;
  no `.wxs` references them.
- `_DisplayMenu`'s constructor became a pure pass-through once `pluginItem`
  was removed, and the cfg inside `handle_input` repeats the one on the
  function itself.
- Normalize `src/lang/sl.rs` to 0644, the only executable file under src/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): handle legacy privacy mode plugin failures

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-08-14 18:52:03 +08:00
fufesou
d1da05c4db refact: remove feature plugin-framework (#15854)
* refact: remove feature plugin-framework

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

* refact: remove unused translations

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

* fix: delete settings tab observable with correct type

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-08-14 14:31:13 +08:00