mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-25 00:03:14 +00:00
26af07a5c9b03fd7891c7babe698d38bf03e6a47
11504
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
26af07a5c9 |
cursor: keep native cursors for every frame of an animated cursor
An animated cursor is a shape per frame, 18 for the Windows busy cursor and 23 for KDE's wait cursor. With 16 native cursors a cycle longer than that missed on every frame: fetched from the core, decoded, registered again and the replaced cursor deleted, about thirty times a second for as long as the peer was busy. 64 holds two such animations and the everyday set besides. The tests settle in 10 ms instead of 100 ms, so the ones that fill the limit stay quick. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
ce614b5fa9 |
cursor: let the kept-shape tests run without the flutter feature
CI's cargo test builds the default features, and the two functions under test are pure, so they compile for every test build and the module loses its feature gate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
22ec5e3f97 |
cursor: drop the tests nothing can fail
a_handle_is_named_once_its_shape_is_shown dates from when `name` wrote the handle map. `name` is pure now, so its last assertion cannot fail, and the others repeat two neighbours. every_handle_the_peer_named_stays_for_the_connection fails only for a bound added on purpose, which the struct's doc rules out. The Dart arrival test is covered by the first test of its file. The kept-shape test now asserts what `cursor_shape` is for, a copy rather than a view of the received buffer, in place of the unknown fields it drops. The content-id test moves out from between two `use` lines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
b93d71899f |
cursor: say why the peer answers request_cursor_data with what it holds
No controller sends it yet, and a review asked for it to go because a cache reset on the peer could leave a request unanswered. A shape the peer's cache dropped is rebuilt, and indexed again, the next time it is shown, before any connection names it by id, so the shape a controller has just been told to show is always there to give; that is the contract a future bounded controller relies on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
0f67165689 |
cursor: say the 1.5.0 gate means the release
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
9f521537dd |
cursor: leave the DRM cursor's own id as it was
The wire id is cursor_content_id whatever the platform, set in run_cursor over the shape's pixels, so the id the DRM reader derives for itself only tells one hardware cursor from the next and keys the service's cache. Which hash it uses does not reach a controller; the change to xxh3 and the dependency it brought to scrap go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
7f845af13b |
cursor: forget the shape the core lacks once the peer selects another
The mark was replaced only by another shape the core lacked, so a lost shape selected again after a known one was still not asked for. Selecting any other shape now clears it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
611c5b4aad |
cursor: remember the shape the core lacks only while it is in use
Every id the core could not give was kept in a set for the session, so a peer sending cursor_id after cursor_id of shapes it never sent grew it by a string each, at a few bytes of traffic per entry. The mark exists so that painting does not ask for the shape in use on every frame, and the shape in use is one, so one mark is kept: the peer selecting another shape forgets it, and selecting a lost shape again asks once more. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
6cf2ef387a |
cursor: a shape that does not decode leaves no handle and no copy
CursorDedupe named a handle as it hashed the shape, before the shape was checked, so a peer sending shapes that fail the checks under new handles grew the map with nothing the UI could ever select, and such a shape took over the handle of one that had decoded. The handle is now named once its shape decoded, or was shown before. The flutter build also copied a shape's compressed colors out of the message before the checks, so a message with a small size and huge colors was copied whole before being rejected. The copy is made once the shape decoded; until then the message's own bytes are held. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
7a91b0c91a |
cursor: delete a replaced native cursor at the next build that shows one
A replaced native cursor was deleted only by a build of a cursor already registered, so a run of new shapes, each shown once, left every replaced cursor registered: 16 on the books, all of them in the system. Now every build that shows a cursor deletes the ones replaced before it, right before it registers or takes one back; what replaced them was activated in an earlier frame, so they are off screen. A build that has to ask for pixels shows nothing new and deletes nothing, so a raster returned to right after being replaced still takes its cursor back; one returned to later registers again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
024ba94c78 |
cursor: a DRM cache reset keeps the shape it was making room for
The shape being sent was filed in CURSOR_SHAPES before the DRM ceiling cleared it with everything else, so a controller asking for the shape on screen got nothing until the next new shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
3d0b80b3de |
cursor: read a shape's pixels from where its view starts
The pixels used to be copied out of the JSON event into a fresh list, so the list's buffer began at its first pixel. Now they come straight from the core, native or web, as whatever Uint8List it hands over, and img2's fromBytes takes the buffer, not the list, so a view into a larger buffer would have been read from the buffer's start. Neither core gives a view today; this stops depending on that. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
6ad1bc7895 |
flutter: leave two untouched signatures as they were
dart format had rewrapped registerEventHandler and sessionSetCommon next to the cursor additions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
504de6debf |
cursor: a shape switched away from while it decodes is not marked lost
A shape asked of the core was taken for undecodable when it was no longer kept once its decode finished, but a shape the peer switched away from in the meantime is let go on purpose, and marking it made every later request for it be skipped: its native cursor stayed deferred and its painted cursor missing for the rest of the session. The decode now reports whether it decoded, and only a failure marks the shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
3b036d2015 |
cursor: hold a native cursor to 512 pixels a side
A native cursor could be registered up to 1024 pixels a side, four MiB of pixels each, which with sixteen kept made 64 MiB at worst. The largest cursor a system shows is 256 pixels (Windows' largest pointer size), so a raster twice that is shown no larger: the cursor is drawn smaller rather than rejected, and sixteen kept come to 16 MiB at most. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
77242d52a7 |
cursor: forget the native cursors a session clear deletes
The clear deleted every registered native cursor but kept their names, so a cursor model used again, as the mobile client's is, took each name for registered and showed a cursor that no longer existed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
a77de619c2 |
cursor: keep the native cursors of the 16 shapes used last
Keeping a native cursor for the shape in use alone rebuilt one on nearly every switch, since the pointer moves among a few shapes: the arrow, text, hand, four resize arrows, wait. The native cursors of the peer's shapes are now kept in order of use, 16 of them, and the one used longest ago gives its cursor up the way a replaced raster does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
18ebd9cf17 |
cursor: keep a native cursor for the shape in use alone
Every shape the peer showed kept a native cursor for the session, each a raster the size of the shape as displayed. A shape switched away from now gives its native cursor up the way a replaced raster does: deleted the next time a cursor already registered is built, taken back if the shape returns first. A shape shown again after that is rebuilt from the compressed copy the core keeps. The predefined cursors are not the peer's shapes and stay registered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
85214000b5 |
cursor: a decode that finishes after the session clears keeps nothing
A shape asked of the core while a tab closed was decoded after the cursor model had been cleared and stored in it, where nothing disposed of its ui.Image again; a shape whose cache failed to build left its ui.Image undisposed as well. A clear now starts a new generation, and a fetch or decode from an earlier one disposes what it made. A shape the core gave but that did not decode is not asked for again on every frame it is painted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
3a1785d3c3 |
cursor: keep the shapes compressed in the core, and only the one in use in the UI
The UI kept every shape as a ui.Image, one raw copy each, for as long as the session lasted, because the peer sends a shape once and a shape might have to be drawn again. The core receives the shapes compressed, at a few hundredths of that, and now keeps them so: Session::cursor_shapes holds each shape that decoded, as the peer sent it, under the id the UI knows it by. The colors are copied, since the message's may be a slice of a larger received buffer. The UI keeps a ui.Image for the shape in use alone. A shape shown again at a raster its native cursor has needs no pixels at all; for a raster it lacks, for painting it, or for the window a tab moved to, the UI asks the core through session_get_cursor_shape, which decompresses and checks the shape again with decode_cursor_data, the same size and decompression limits as on arrival. A shape the core cannot give is not asked for again until the peer sends it. A tab move now carries only the id in use, and nothing is read back from the GPU. The web core keeps the shapes the same way and gives them back through getCursorShape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
1007d29687 |
cursor: keep one native cursor per shape, and each tab its own
A shape got a native cursor for every raster it was shown at, and all of them stayed until the session ended, so every zoom or DPR change added a native cursor for each shape in use. A shape now keeps the one at its current raster; the one it replaces is deleted the next time a cursor already registered is built, by when its successor has been activated. A raster returned to before that takes its cursor back instead of registering another under the same name. The native cursors of a window's engine are shared by its tabs, and the predefined cursors had one name in all of them, so a tab closing deleted the default and forbidden cursors the other tabs still showed. Names are now scoped to the tab's cursor model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
d9223ae333 |
cursor: pin that a released shape keeps neither its pixels nor its bytes
The shape in use once registered, the shapes not in use, and the pixels read back for a moved tab are all dropped; the tests now say so, so that a later change keeping one of them fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
0c95a2af9c |
cursor: keep a shape as its ui.Image and nothing else
Each shape the UI had been sent stayed in four copies for the session: the pixels for a tab move, an image package copy to resize from (on a macOS or Linux controller of a Windows or Linux peer, a second one decoded back from a PNG), the bytes handed to the native cursor, and the ui.Image painted when the peer moves the pointer. A shape now keeps only its ui.Image. The resize source and the native bytes exist while a native cursor is being registered and are dropped once it is; a shape shown again at a raster it was registered at needs neither. For a raster it lacks, the pixels are read back from the ui.Image, which returns them exactly as they came. A tab move reads every shape back the same way, and the moved window decodes them as it would have on arrival. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
49710e45ee |
cursor: let a controller ask the peer for a shape again
A peer sends each shape once per connection and afterwards only its id, so a controller must keep every shape it was sent for as long as it is connected. The controlled side now answers Misc::request_cursor_data with the CursorData of that id, from the shapes it has sent, and only to a connection the cursor service would send it to. A controller from 1.5.0 on can then bound what it keeps and ask again for a shape it let go. None asks yet: the peer has to have the answer before a controller can rely on it, and peers are updated last. The shapes are kept by content id, and every handle for a shape shares its one message, where each handle used to keep its own copy. They are dropped with the service's own cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
f88407472b |
cursor: hand the UI a shape's pixels as they are, not as text
A shape's pixels went to the Flutter UI as a JSON array of integers inside the cursor_data event: the core serialised a 1 MiB shape into up to 4 MiB of text on the receive loop, and the UI parsed that into a million-element list and copied it into bytes, on the UI thread. The event, text and all, was then kept for the session so that a tab moved to another window could replay it, and a reconnect, on which the peer sends every shape again, appended another copy of each. The core now sends a shape as an EventToUI::Cursor variant carrying Vec<u8>, which flutter_rust_bridge hands to Dart as a Uint8List, on the same per-session stream as the events so that it keeps its order with the cursor_id events around it. The web core calls onCursorData with the Uint8Array, as it calls onRgba for a frame. The id stays text, since the peer's ids can exceed Dart's int. The replay keeps each shape's pixels once, by id, and encodes them as base64 only when a tab moves. A shape arriving is also recorded as the last one selected, since the replay goes in first-seen order; before, a tab moved right after a new shape came back on the previous one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
7825168f84 |
cursor: hold a content id to what the web client can read
The web client decodes a u64 into a JS number and throws on a value past 2^53, which drops the whole message. A full 64-bit xxh3 id is almost always past it, so a web client connected to a peer on this build would never have been given a cursor shape. DRM's ids were already full width, so web clients lost the cursor on DRM peers too; the controlled side now renames those as well. The id keeps the hash's low 53 bits. Two of ten thousand distinct shapes then collide with a chance of about one in two hundred million. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
d64ff4961b |
cursor: name a shape by what it looks like, not by its handle
A cursor id was the platform's handle for the shape: HCURSOR on Windows, the XFixes serial on X11. Apps mint a new handle for a shape they have shown before (Chrome, Electron, Qt custom cursors), and an X11 animated cursor gets one per frame, so one arrow arrived under thousands of ids over a session. The controlled side cached and sent it again under each, and the controller decoded, rendered and kept it again under each, and grew by gigabytes over weeks of connection. The controlled side now names a shape by an xxh3 hash of its size, hotspot and pixels, taken once when a handle is first seen, before the pixels are compressed. The per-connection send already sends a shape once and afterwards only its id, so a shape now crosses once however many handles it has. The id is opaque in the protocol, so controllers of any version accept it. The DRM backend already named shapes by content with FNV-1a; it uses the same hash now. A peer before 1.5.0 still names shapes by handle, and the fix has to work against every deployed peer. For those peers the controller hashes each CursorData's compressed colors with its geometry: one peer compresses the same pixels to the same bytes, so a shape seen before is recognised without being decompressed. The UI gets that shape once, under one id, and every later handle for it is passed on as a cursor_id of that id. The map from the peer's ids is kept for the connection, unbounded, since the peer sends a shape once and may select any id it named again; an entry is two integers. From 1.5.0 the controller passes the peer's ids through unchanged, on the old path. The web core in flutter/web does the same in its own tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
58ff78a823 |
Hdr tonemap (#16033)
* scrap: tone-map HDR desktops to SDR in the Windows capturer With HDR enabled, Windows composes the desktop as linear scRGB in R16G16B16A16_FLOAT and places SDR white at the user's "SDR content brightness" (DISPLAYCONFIG_SDR_WHITE_LEVEL / 1000, e.g. 2.5 for 200 nits) rather than at 1.0. The legacy IDXGIOutput1::DuplicateOutput we used always converts that surface to BGRA8, and it does so by clipping, so everything above ~40% linear brightness lands on white. That is the washed-out / overexposed / high-contrast picture reported for HDR hosts in #5368, #9951, #12707 and discussion #7652. Ask for the desktop with IDXGIOutput5::DuplicateOutput1 and the format list [R16G16B16A16_FLOAT, B8G8R8A8_UNORM]. An SDR desktop still yields BGRA8 and takes the unchanged path. A float frame is converted on the GPU by a small pixel shader: divide by the SDR white level, clamp, apply the sRGB transfer. SDR content round-trips exactly, HDR highlights clip at white, the same result the local user sees. The white level is read per output through DisplayConfigGetDeviceInfo and refreshed once a second, so dragging the Windows slider tracks remotely. The converted BGRA8 texture feeds both the staging-copy (CPU) path and the vram (hwcodec texture) path; rotation is unchanged. Toggling HDR mid-session invalidates the duplication as before, and the recreated capturer re-detects the format. The shaders are compiled at runtime from a dynamically loaded d3dcompiler_47.dll so no new import is added to the binary. If the DLL, the compile or any D3D object creation fails, a process-wide flag makes later capturers fall back to the legacy DuplicateOutput, i.e. today's behaviour. This deliberately stops at SDR on the controlled side, with no option and no protocol change, which is how OBS, Sunshine (client without HDR) and macOS screen capture handle it. Real HDR pass-through would need 10-bit capture, Main10/AV1-10 encoders in the hwcodec fork, colour metadata on the wire and, decisively, an HDR output path on the controller: Flutter's desktop external textures are BGRA8888/RGBA8888 only on every platform, so nothing would be visible. When that changes it should follow the Sunshine/Moonlight pattern: an hdr capability bit advertised by the controller behind an explicit user toggle, negotiated like i444. Type-checked against x86_64-pc-windows-msvc (with and without the vram feature); not yet run on an HDR machine, which needs Windows 10 1703+ with HDR on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F (cherry picked from commit ca3c2f8ac7d1549a40855411b0539a69c82d7223) * scrap: stay on DXGI when the HDR conversion fails, load the compiler once Review follow-up: - A tone-map failure surfaced as a capture error, and the capture loop answers any DXGI error by switching the capturer to GDI for the rest of its life. The capturer now drops the tone-map, re-creates the duplication with the legacy DuplicateOutput (DXGI's clipped BGRA8, the pre-HDR behaviour) and returns WouldBlock, so the session stays on DXGI and simply fetches the next frame. - Only failures that cannot succeed anywhere in the process (no d3dcompiler_47.dll, shaders that do not compile) set the global UNAVAILABLE flag; D3D object creation failures stay with the capturer that hit them, so another adapter or a recreated capturer tries again. - D3DCompile is resolved once through a OnceLock instead of a LoadLibrary per capturer that was never freed. - The module doc now states what this pass is: normalization of an HDR desktop to SDR, with everything above SDR white clipping, not a tone map, and why a roll-off is deliberately not applied. The earlier claim that clipping matches what the local user sees was wrong for HDR content on an HDR display. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F * scrap: only apply the SDR white level to outputs DXGI reports as HDR Review follow-up: - An FP16 desktop is not necessarily HDR. Since Windows 11 22H2 an Advanced Color (WCG) SDR display is composed in FP16 scRGB too, and there 1.0 is the display's reference white, not 80 nits, so the SDRWhiteLevel scaling does not apply (Microsoft: "Reference white applies only to HDR displays"). Query IDXGIOutput6::GetDesc1 and treat the output as HDR only for DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, the documented Win32 check; a non-HDR FP16 frame now gets just the scRGB -> sRGB transfer. Without IDXGIOutput6 (before Windows 10 1803) FP16 can only mean HDR. - The SDR white level is now Option: a failed query or a reported 0 is "unknown" on both the initial and the refresh path, an HDR output with an unknown level logs a warning and keeps the 80-nit assumption until the periodic re-query succeeds. DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL needs Windows 10 1709, not 1703 as an earlier message said. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F * scrap: re-read the Advanced Color state while capturing, gate FP16 on IDXGIOutput6 Review follow-up: - The HDR/WCG decision was taken once when the conversion was created. HDR can be switched on or off, and a WCG desktop can turn into an HDR one, without the duplication being invalidated, so the choice between "divide by the SDR white level" and "no scaling" could go stale. The once-a-second refresh now re-reads it: it keeps an IDXGIFactory1, and when IsCurrent reports FALSE it creates a fresh factory and re-finds the output by GDI name, since a stale factory's outputs keep stale descriptions (this is the procedure the GetDesc1 docs require). On a change it switches modes, re-reads the white level and updates the shader constant; an unreadable level keeps the last known one. - Float frames are only requested when IDXGIOutput6 exists, as Microsoft's duplication sample does. That interface is also what tells HDR from WCG, so there is no longer a state where FP16 is requested without being able to interpret it. IDXGIOutput6 dates from Windows 10 1703, not 1803 as the previous commit said; 1803 added IDXGIFactory6. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F * scrap: replace the DXGI factory and output together when re-reading HDR state Review follow-up: when the fresh factory did not find the output by name, the refresh kept the new, current factory next to the old output, and because IsCurrent then reported TRUE it never enumerated again, so the stale output could stay forever. enumerate_output6 now returns both or neither, and the refresh only replaces the pair together; a null factory forces another enumeration on the next refresh while the old output is still read in the meantime. The constructor gets the same guarantee for free, since a failed lookup leaves the factory null there too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F * scrap: keep enumerate_output6's both-or-neither promise when IDXGIOutput6 is missing Review nit: a matched output whose IDXGIOutput6 query fails returned the fresh factory next to a null output, which let the constructor pair a current factory with the capturer's fallback output. Return neither in that case so the next refresh enumerates again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLb1dNdhFqUQExJPANjo1F * scrap: log DuplicateOutput1 failures before fallback Signed-off-by: 21pages <sunboeasy@gmail.com> * scrap: avoid null duplication after HDR fallback failure Signed-off-by: 21pages <sunboeasy@gmail.com> * scrap: load the D3D compiler securely from System32 Signed-off-by: 21pages <sunboeasy@gmail.com> * scrap: release the frame through the state machine when abandoning the tone-map master's FrameState machine is left at Acquired by the raw ReleaseFrame(), so the next release_frame() hits DXGI_ERROR_INVALID_CALL on the fresh duplication and the capture loop falls back to GDI -- the outcome the fallback exists to avoid. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Signed-off-by: 21pages <sunboeasy@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: 21pages <sunboeasy@gmail.com> |
||
|
|
a8ae0d6d66 |
Review and complete pt_PT.rs translation (European Portuguese) (#16311)
* Update pt_PT.rs * Update pt_PT.rs * Update src/lang/pt_PT.rs fiz misspelling of "Apenas" Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Fix config_screen: replace trailing slash with period Per CodeRabbit's suggestion — the slash was rendered directly in the permission prompt text, not as a line continuation, so it read as a stray character instead of proper punctuation. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix terminal-clipboard-write-tip: translate full 3-sentence source text Per Greptile's review — the previous translation for this key only covered a short summary (matching an equally incomplete ptbr.rs entry), not the full English source defined in en.rs, which explains the permission's scope (persists across all connections until disabled in Settings) and that manual copy/paste is unaffected. Translated from the actual en.rs source text as required by AGENTS.md's localization guidelines. --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
2e333f64a4 |
Update sc.rs (#16314)
* Update sc.rs * Update sc.rs |
||
|
|
e424b57fca |
Kx v1 (#16326)
* key exchange version 1 on the rendezvous and peer handshakes Bump hbb_common for key exchange version 1, one stream key per direction, and negotiate it on both handshakes. Rendezvous: `secure_tcp` reads the version the server advertises in `KeyExchange.version`, answers with the highest both speak, splits the key when that is at least 1, and arms the check of the server's echo on its first encrypted message. Peer: the controlled side advertises `KX_VERSION_LATEST` inside the signed `IdPk`, the controller answers in `PublicKey.kx_version` and both split the key when the pick is at least 1. A pick above what was offered is refused as a bug or tampering. `decode_id_pk_dtls` returns the advertised version along with the fingerprint. An absent field on either side is version 0, so any old peer or server keeps today's stream byte for byte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * bump hbb_common: keep the advertisement check armed until an echo arrives Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * bump hbb_common: drop box_pk_of until something calls it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * key exchange: say 0 on WebRTC, where no stream key applies A WebRTC stream is encrypted by DTLS and `set_key_split` collapses to `set_key` on it, which sets nothing but `peer_verified`. Both sides still put 1 on the wire, the controlled peer in its signed identity and the controller in its pick, and agreed on a version neither applied. That held only because both fell into the same branch: the day one side splits keys on WebRTC and the other does not, they would agree on 1, derive different keys, and have no version mismatch to point at. The controlled peer now advertises 0 on WebRTC and refuses a pick above what it advertised rather than above the newest it speaks anywhere; the controller picks 0 there. What is on the wire is what runs. Also bumps hbb_common for the echo contract: the server tags every message after the exchange, not the first alone, so dropping one frame at a known position does not rid an attacker of the downgrade check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * key exchange: verify the server's signed version before trusting it bump hbb_common d9f519e: KeyExchange.signed_version A server that signs its version sets bit 255 of the ephemeral key inside the signed keys[0]. A client that sees that bit, or the field itself, verifies sign("rdkx-ver" || key || version LE) under the server's signing key before picking a version and refuses the exchange otherwise, so a version lowered in the clear is caught at the handshake rather than one frame pair later by the echo. A legacy server sends canonical keys and no field and takes the unchanged path; the echo still checks its version. Tests: version 1 keys match the server both ways; an advertisement lowered in transit is refused at the first echo; legacy, signed-1 and signed-3 servers each exchange application data; nine tamperings of the signed version are refused before the client replies. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: comments say what the fields and functions are bump hbb_common e3452ac: the same on the shared side. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: drop the advertisement echo bump hbb_common 5194159: RendezvousMessage.kx_advertised and the check on decrypted frames are gone. signed_version settles the version inside the exchange, so the client arms nothing after it; the stub in the tests stops tagging its frames and the test of a lowered advertisement goes, the signed-version cases covering that refusal before any reply. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: verify KxParams, the server's signed structure bump hbb_common e74a188: KeyExchange.signed_params and KxParams. The client parses the signed bytes as KxParams and compares its fields to the key it verified and the version it was shown, rather than matching a fixed byte string, so a field added to KxParams later parses past an older client. The tamper cases now include a payload signed as the old byte string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: require the KxParams signing prefix before parsing bump hbb_common 26582e1: KX_PARAMS_DOMAIN. Without it a signed IdPk, which the server signs with the same key, parsed as a KxParams whose pk was the id and whose version was 0, so an id registered with the bytes of a marked ephemeral key could stand in for the params at version 0. Two tamper cases pin this: params signed without the prefix, and a signed IdPk in their place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: one call sets the negotiated key bump hbb_common cbc8772: Stream::set_negotiated_key. The rendezvous client, the controller and the controlled side each branched on the picked version to choose between split and single keys. They now hand the transcript to Stream, which makes that choice once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common: refuse unknown key exchange versions, pin wire vectors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * client: prove the peer handshake picks its version and encrypts under it The rendezvous exchange had tests against a stub server; the peer handshake had none, so a controller that fell back to version 0 would still connect, still report secured, and pass every test. These run Client::secure_connection against a stub controlled peer and check the pick it sees and the application data both ways: new peers pick 1, a peer without versions gets 0, and a pick lowered in transit leaves the two sides unable to read each other. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common: pin the subkeys for an advertisement above the pick Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * Update hbb_common to main, with #614 merged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * key exchange: name the picked version, say why the marker bit is safe Review follow-ups with no change in behaviour: the rendezvous pick is called picked, as in the peer handshake and KxTranscript; the marker comment says X25519 ignores the bit and the bytes stay as signed; the controlled side's refusal names the version it offered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b30f781fd6 | fix: restrict VRAM stall detection to AMD SDK (#16319) | ||
|
|
68a35e65f2 |
fix(unix): ignore SIGPIPE in native Flutter entry (#16322)
* fix(linux): ignore SIGPIPE in Flutter runner Initialize SIGPIPE before entering the Rust core so writes to closed IPC peers return EPIPE instead of terminating the server. Add a native-entry regression test for the failure reported in #16297. * test(linux): remove SIGPIPE runner test * fix(flutter): initialize SIGPIPE in the Rust native entry Share SIGPIPE initialization between the Linux and macOS native runners before core startup. Remove the Linux C++ initialization. * fix: share SIGPIPE initialization with Sciter startup Initialize SIGPIPE before global_init in core_main, shared by Flutter and Sciter on Linux and macOS. * fix(flutter): keep SIGPIPE setup in the native FFI entry Sciter already receives SIGPIPE initialization from the Rust runtime. Scope the missing startup initialization to native Flutter runners on Linux and macOS. |
||
|
|
7c21b4569c |
test(drm): bound the excused shapes at their worst angle, not only on average (#16320)
Both of the review follow-ups on rustdesk#16242. One comment still said alias was 0.11 px worse than master. That was the number before the directional rule; it is 0.51. And the exemption for help and alias was bounded only on the average over the angles master handles. The worst single angle is the larger number and the one that would be felt - help is 8.00 px under master at 270 and 21.54 here - so leaving it unbounded let the exemption widen quietly. Both directions are bounded now, at the measured values: alias 0.51 mean and 8.94 at 90, help 4.73 mean and 13.54 at 270. Tests only. |
||
|
|
c6a358afbe |
Update lang.rs - Splits ambiguous pt into pt-pt and pt-br (#16155)
* Update lang.rs - Splits ambiguous pt into pt-pt and pt-br Splits the ambiguous "Português" entry into two explicit options: Português (Portugal) [pt-pt] and Português (Brasil) [pt-br]. The pt_PT.rs translation file already existed in the repo (fully translated, key-complete) but was never wired into lang.rs. Kept the old "pt" and "br" locale codes mapped to ptbr::T for backward compatibility with existing user configs. * Preserve pt-PT/pt-BR region in automatic locale resolution Preserve pt-PT/pt-BR region in automatic locale resolution * Fix pt-pt detection: check region suffix instead of redundant "pt" substring Fix pt-pt detection: check region suffix instead of redundant "pt" substring * Normalize legacy pt/br saved lang option to pt-br Normalize legacy pt/br saved lang option to pt-br * Normalize legacy pt/br lang option on Web client * Add regression tests for pt-pt/pt-br locale resolution * Follow CLDR inheritance for Portuguese regional locales (pt-AO, pt-MZ, etc.) |
||
|
|
458ffe44c6 |
Update Ukrainian README (#16309)
* Start Ukrainian README terminology update Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Synchronize Ukrainian README with current documentation Update content, terminology, build instructions, badges, and project structure to match the current English README. Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Polish Ukrainian README dependency wording Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Clarify Docker build argument placement Address review feedback by explaining where optional build arguments should be appended to the docker run command. Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> --------- Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> |
||
|
|
2e7255f083 |
fix: suppress heartbeat HTTP retry and TCP proxy logs (#16273)
* fix: suppress heartbeat HTTP retry and TCP proxy logs Signed-off-by: 21pages <sunboeasy@gmail.com> * refactor: move API log interval to common module --------- Signed-off-by: 21pages <sunboeasy@gmail.com> |
||
|
|
12f61b1fda |
Fix/drm rotated cursor (#16242)
* fix(drm): turn the cursor by the output's real angle, and guess its hotspot upright Two things were wrong with a rotated output's cursor, and the maintainer's physical test found both. The bare-metal hotspot is a guess - the top-left of the sprite's opaque box, since HOTSPOT_X/Y only exist on para-virtualised drivers - and it was guessed on the sprite as scanned out, then mapped as if it were a point on it. A turned arrow's tip is never in that corner: at 90 the guess is off by the arrow's width along x, small enough to pass as fine, and at 270 by its height along y, which is what was noticed. Measured on an i915 box with a 64x64 Adwaita arrow, hotspot against tip: +11,0 at 90 and 0,+18 at 270 before, 0,0 at every angle after. A guessed hotspot is now guessed on the upright sprite, in the consumer, and the wire says whether the driver measured it so a real one still maps as a point. And 180 was clamped to 0 for the whole session because a hardware-rotated primary plane scans out upright. That holds for the frame and not for the cursor: the compositor pre-rotates the sprite in software either way, so it arrived upside down. The frame keeps the clamp; the cursor gets the real angle. (cherry picked from commit 6a73100b4783fbf9c8f43e0dc7651b7568db9ea9) * test(drm): drive the cursor path, and drop the field nothing reads The two tests called the helpers, so nothing pinned the production decision: restore master's `t == 90 || t == 270` in deliver_drm_cursor and both still pass while the 180 sprite goes back to upside down. The new test drives deliver_drm_cursor and reads back what it published, and it fails under exactly that mutation. The other way to reintroduce the bug was to point the receive thread at `Shared::transform` again. That field had no readers left once both gates moved to `cursor_transform` - it was stored and never loaded - so it is gone, and the doc above TRANSFORM_PENDING now names the field that actually holds a racing cursor back. Also four comments that said more than the code does: the sprite is treated as pre-rotated at every angle because wl_output cannot say otherwise, not because the compositor provably always does it; the old flow was off at every angle, not just at one of 90/270; the 180 symptom had its own cause, the clamp, rather than all of it coming out of the hotspot guess; and the test arrow's opaque box is its left three columns, not the whole bitmap. * docs(drm): say what the 180 measurement actually showed The loop asserts the old mapping was wrong at 180 too, and that is true of the mapping, but master never ran it there: the clamp sent 180 down the untouched branch, so guess and sprite turned together and the hotspot landed on the tip. The bench measured exactly that - (0,0) at 180, with only the sprite upside down. The comment said the old flow was wrong at every angle without that distinction, which is the kind of gap between a claim and a measurement worth closing before someone else finds it. * fix(drm): centre the guessed hotspot of an I-beam lying either way The bounding-box guess only centred TALL shapes, so a HORIZONTAL I-beam was handed the top-left corner of its box. Adwaita's `vertical-text` is exactly that shape: at 24 px its opaque box is 20x9 and the theme's own hotspot is (12, 11), its centre. The corner guess lands 11.2 px away; the centre lands 1.4 px away. It matters more since this branch turns the sprite upright before guessing. Such a cursor used to reach a quarter-turned output as a TALL bitmap, where the tall-only rule centred it by accident; guessing on the upright bitmap - correct in itself, and what fixes arrows at every angle - is what exposed the asymmetry. Found by fufesou at pixel level on rustdesk#16242, and reproduced here against the installed theme before changing anything: same declared hotspot, same bounding box, same two errors. Then checked across ALL 35 shapes of that theme at 24 px: making the aspect test symmetric moves exactly one of them, `vertical-text`, from 11.2 px to 1.4 px, and leaves the other 34 byte-identical. The old flow is not the fix. It was right for this one shape and wrong for arrows at every angle, which is what `a_guessed_hotspot_is_guessed_on_the_upright_sprite` pins; the fix belongs in the guess, not in the order of operations. The regression test that missed this used only `arrow()`, as fufesou pointed out. Three things the first version of the replacement got wrong, all of them in what it CLAIMED rather than in the fix: - Its expected point was the centre of the BITMAP, which equals the centre of the opaque box only because the fixtures filled their bitmaps symmetrically. Both I-beam fixtures are now off-centre in their buffers, the way a sprite sits in a 64x64 hardware cursor, and the test asserts up front that the two centres differ so the fixture cannot quietly stop discriminating. - It called the arrow's box "elongated by less than the factor of two", when 3x6 IS the factor of two and the arrow keeps its tip only because the comparison is strict. That makes the arrow the control for one side of the threshold, and it is now described as such. - It said the case was driven "through the whole delivery path" while chaining the helpers itself. It now calls `deliver_drm_cursor` and reads the published hotspot back out of DRM_CURSOR, at 0, 90, 180 and 270, feeding it what the producer really sends for a guessed hotspot: the producer's own guess, made on the sprite as scanned out. And the threshold was pinned from one side only. A third fixture carries the real 2.22:1 aspect of the shape that was wrong, so a stricter rule cannot pass on 6:1 bars while silently un-centring the actual cursor. Mutations, each restored byte-for-byte afterwards: back to tall-only, the bitmap centre instead of the box centre, the threshold loosened to one and tightened to three. All four fail the test. * build(drm): point the pin at the synced mirror and drop the helper fallback The mirror is synced, so the pin moves from 5da68a3a (libdrmtap 0.5.4) to 49b204f (0.5.6), which is what rustdesk-org/libdrmtap now carries. -Dhelper=disabled, as the maintainer asked on #16242. rustdesk never uses the privileged helper: every drmtap_open lives in src/ipc/drm.rs, i.e. the root service, which already holds CAP_SYS_ADMIN, and the unprivileged side opens a render node instead. What the library carries without the option is a fallback that walks six hardcoded paths, two of them under /usr/local, and execs the first one that passes access(X_OK) -- no check of its owner, no check of its mode -- from inside the ROOT process. The option compiles that path out. Asserted on the artifact as well as passed as a flag, for the same reason the EGL check is: a flag cannot notice a stale build-pkg or an object substituted by hand, and meson accepts an unknown -D silently on versions predating the option. Measured on the produced .so: socketpair 4 -> 0, the helper search paths 3 -> 0, and nm -D shows no fork, execl or socketpair import. The check fails as it should when pointed at a .so built with the helper. * fix(drm): use the library's hotspot provenance, and keep legacy IPC meaning Answers the two blockers in the maintainer's review of |
||
|
|
6a0eb9a1fd |
fix(terminal): preserve trackpad scrolling in alternate screen
Prevent the inner terminal scrollable from accepting user scrolling in the alternate buffer, so xterm's existing outer handler receives trackpad gestures after main-screen history. Keep wheel encoding and drag reporting unchanged. Cover trackpad forwarding, subsequent drag coordinates, arrow fallback, and return to local scrollback. |
||
|
|
763d4eeb05 |
bump hbb_common: tungstenite 0.29 forks, read buffer grown as bytes arrive
hbb_common 7dc56d6..d9895ff, one commit: tokio-tungstenite and tungstenite up from 0.26 to rustdesk-org's rustdesk-patches-0.29 branches. The tungstenite fork grows the read buffer a read_buffer_size chunk at a time as bytes arrive, where upstream reserved the whole length a frame header declared. It is pinned here at the workspace root, the one place cargo reads [patch], so hbb_common's copy and tokio-tungstenite's resolve to the same one. The lockfile moves only those two crates and drops utf-8, which 0.29 no longer pulls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab |
||
|
|
a5d4ef97e8 |
fix(ws): reject text frames after encryption is enabled (#16295)
Signed-off-by: fufesou <linlong1266@gmail.com> |
||
|
|
dd9b21cf86 |
server: hold an unauthenticated connection to a small message (#16254)
* server: hold an unauthenticated connection to a small message Until a peer authorizes it sends only a public key, a login request, a test delay and a close reason, none of them large. Nothing said so: a frame header could declare up to whatever the transport allowed, 1 GiB on TCP and WebRTC, and a connection holds its place for up to LOGIN_GRACE before it has to authorize. With MAX_UNAUTHORIZED_CONNS places to fill, that is 64 GiB of header-declared payload one peer could make us hold - or, on WebSocket, 1 GiB bought outright with a few hundred bytes of frame headers, because tungstenite reserves a frame's declared payload as soon as it passes max_frame_size. The cap goes on in create_tcp_connection, before the identity handshake, so that read is bounded too, and comes off once the login is settled. It comes off before connect_port_forward_if_needed rather than beside the rest of authorization: a multiplexed tunnel narrows the same knob again for its own framing and has to have the last word. 128 KiB is several times the largest login request anyone sends - a long hostname, an os_login, an avatar URL, a file-transfer path - and is also the read buffer tungstenite allocates per WebSocket connection whatever we do, so on that transport the bound costs nothing beyond a floor already paid. A server hands that avatar out as a URL; only a custom client that inlines an image into the avatar option instead can reach the bound at all. Together with MAX_UNAUTHORIZED_CONNS it holds every unauthorized connection to 8 MiB. Redis answered this same shape in CVE-2021-32675 with 16 KiB, tighter because a per-message bound is the only one it has; here the connection count is the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * server: test that the unauthenticated cap is on before the handshake reads hbb_common covers each transport's cap; nothing covered where the connection layer puts it. A header one byte over MAX_UNAUTHORIZED_MESSAGE, written to a connection stalled in the identity handshake, has to end the handshake on the header alone and release the connection's place: the test fails with the cap moved past the handshake or dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * client: hold the clipboard until this round's login is accepted The clipboard listener is one per process, started by the first session to log in, and its broadcast went to every session - one whose login was still waiting on a password, 2FA or the peer's consent included. What the user copied meanwhile went to a machine that had not admitted them; the peer drops it unread before authorization, but it is in that peer's hands. The check goes on the round, not the session: a session-level one reads a state and later a sender that a reconnect can have swapped in between, so a broadcast that passed for the round before could still queue on the next. Remote is the round - its queue, and is_connected set once its own PeerInfo is in - so a Clipboard or MultiClipboards that reaches handle_msg_from_ui before then is dropped there, on the line before it would go out. What the login itself sends through the same queue, Auth2FA among it, goes as before. The unauthenticated cap on the other side is how this came up: a clipboard over 128 KiB ended such a login with "Reset by the peer". The small case had always gone through quietly. A test drives a round's Remote over a loopback pair before any login: a clipboard does not reach the far end, a 2FA code does, and once the round is connected the clipboard does too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
97811acbdd | Update Dutch translation (#16284) | ||
|
|
7a367f7cc6 |
bump webrtc fork: one IPv6 host candidate per interface and prefix (#16263)
* bump webrtc fork: one IPv6 host candidate per interface and prefix ICE gathered every IPv6 address on every interface. Beside the temporary address that privacy extensions rotate, a prefix usually carries a stable one, often derived from the MAC, that the OS never picks as a source: a host candidate for it hands the peer an identifier that outlives every rotation and that nothing else this machine sends out ever shows. RFC 8445 §5.1.1.1 has the trackable addresses of an interface and prefix left out once a privacy one is gathered. There is no portable way to tell the two apart, so the fork's `local_interfaces` stands in for that rule with a best-effort approximation: of an interface's addresses in one prefix, it asks the OS which one it sends from - a UDP `connect` inside the group's own prefix, nothing sent, nothing outside this machine's own prefixes involved - and if the answer is one of them, keeps that one alone. If the answer is none of them, the whole group is kept, as it was: the probe is bound to no interface, so where Ethernet and Wi-Fi share a LAN the route picks one of them and the answer for the other is an address it does not hold, and the enumeration order would be no better a guess - on macOS its first address is the stable one. Every other interface and prefix keeps its address, a VPN's unique-local one among them. Two static addresses in one prefix keep one, the recorded price of the stand-in; the interface a shared prefix's route bypasses keeps both of its addresses, a gap the stand-in leaves open rather than a regression. The Windows enumeration, which named every adapter "" with no mask, now carries the adapter's name and on-link prefix, without which Ethernet and Wi-Fi on one LAN would have been a single group. That reaches IPv4 too: its addresses carry the adapter's name and mask where they were "" at /32, so the candidates gathered are the same but `interface_filter` sees the real names. hbb_common is untouched: its fe80::/10 filter still applies to what the fork keeps. Two more fork commits ride along, found by the same review. SCTP never reported a DATA chunk received again: `handle_data` asks `can_push` before `push`, and `push` was where a duplicate was noted, so the no-cwnd sender's reordering window, which widens on reported duplicates, never heard of one from another of these endpoints; and the SACK, its gap blocks and now its duplicates unbounded, could outgrow the MTU the DATA chunks keep to under a thousand chunks in flight with holes among them. Duplicates are now listed and SACKed at once (RFC 9260 §6.2), and the SACK reports the lowest gap blocks that fit (§6.7). And the Windows adapter struct, split into nested parts, read `Ipv6IfIndex` eight bytes late on x64 - only into a scope id that `Interface::convert` drops, so nothing gathered wrongly, but the field an interface-bound probe would need; it is flat now, with a compile-time check written for Rust 1.75, the version this crate builds with - `offset_of!` would have wanted 1.77. rustdesk-org/webrtc 80d5a20..49c89bd8, six commits: the heuristic, the fail-open it grew in review, its comments brought in line with that, the SCTP duplicates and SACK bound, the flat Windows adapter struct, and its check made to build on 1.75. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * pin hbb_common to main 7a5ad52 0eb1759..7a5ad52, six commits: the message cap's tests on WebSocket and WebRTC with the fragmented WebSocket message bounded too (e999dce, f0f1548); hyper_util's debug logs out of the default filters (#608); and `new_direct_udp_for_unverified`, the controller's UDP socket on the resolver's preferred address for the rendezvous server, with a second for the lookup and without the TCP connection `test_target` opens and drops to prove it - while `new_direct_udp_for`, `new_udp_for` and `rebind_udp_for`, which the controlled side's punch reply and its registration live on, keep that proof (f688d41, 0bc8336, 7a5ad52). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * ipv6 punch: ask the kernel for the route without resolving a name first `test_ipv6` is awaited on the connection path by both sides of a punch, and before it looked for a public IPv6 address in the background it resolved the STUN hosts' names inline - racing the four, so that one resolver that hangs would not decide. It could still: `select_ok` returns the first success or the last failure, so with no resolver answering the probe waits for the slowest lookup to give up, as long as the system resolver takes, once a minute, on the first connection of that minute. The name was never needed. `connect` on a UDP socket sends nothing; it has the kernel pick a route and a source address for the destination, and any global address serves, so the probe now names one - the one libwebrtc's QueryDefaultLocalAddress asks for - and touches no network at all: a bind, a connect, a local_addr. A machine without an IPv6 route learns so from the connect's error, at once, as before. Two smaller things beside it. The minute's gate read the timestamp under one lock and set it under another, so two connections arriving together both found it over and both probed; it is one critical section now. And the background STUN probe, bounded so far by the STUN client's own ten seconds and the resolver's, has a deadline of its own, five seconds: a probe that outlived the minute could write an earlier network's address over a later probe's. Test: the route probe completes within a second, an address found or not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns * controller: the NAT test takes the resolver's address for the server `new_direct_udp_for` learned which address to send the UDP NAT test to by opening a TCP connection to the rendezvous server and dropping it - a handshake, one round trip, on every connection the controller starts, right before `_start_inner` opens the connection it keeps to the same host. The NAT test now takes the resolver's preferred address, `new_direct_udp_for_unverified`, which spends no round trip proving it and gives its lookup a second: an address the server does not answer on costs this connection its UDP punch, which the TCP punch and the relay cover, as they do whenever the test finds no port. The controlled side's punch reply keeps `new_direct_udp_for` and its proof - a reply sent to such an address is a device online and unreachable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019aokqJuhjvB3kijXtAg5Ns --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ea04f04f9d |
add hide-elevate-button-in-accept-window (#16271)
A portable client handed to standard users offers them "Accept and Elevate", which they have no credentials to complete. The builtin option takes that button out of the accept window and leaves elevation to the controlling side's "Request Elevation" during the session. https://github.com/rustdesk/rustdesk-server-pro/discussions/1016 Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef00ed78a7 |
Update missing Ukrainian UI translations (#16272)
* Update Ukrainian translations Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> * Complete Ukrainian voice call translation Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> --------- Signed-off-by: Dmytro <dmitriy.gaponuk@gmail.com> |
||
|
|
5278fcab68 |
fix(cursor): correct native cursor sizing and validate received images (#16213)
* fix(flutter): shrink the unzoomed remote cursor by DPR on macOS and Linux With "Zoom cursor" off in Adaptive or Custom view, the remote cursor bitmap was registered at scale 1.0. NSCursor and GdkCursor treat the bitmap size as logical pixels, so on a HiDPI controller the cursor was drawn DPR times larger than in Original view (which already passes 1/DPR) and than on Windows (whose cursor path is in physical pixels). A HiDPI remote such as KDE Wayland sends a 48-64 px bitmap, which then showed up 3-4x too big on a Retina Mac. Scale the bitmap by 1/DPR in that case, and scale the Flutter-painted cursor used while the peer moves the mouse the same way so its size does not jump. The new branch is an identity at DPR 1 and the Windows paths are untouched. Fixes https://github.com/rustdesk/rustdesk/discussions/15363 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * fix(flutter): check the cursor height against the min cursor size `_checkUpdateScale` computed the scaled height from `width`, so the min-size clamp never looked at the height. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * fix(flutter): keep the painted cursor hotspot in place when zoom cursor is off `CursorPaint` subtracted the hotspot in remote pixels and then scaled it by the canvas scale, but drew the image at scale 1.0, so the hotspot landed hotx * (1 - scale) logical pixels away from the remote cursor position. Cursors with a centered hotspot (I-beam, crosshair) were off by up to half their size in Adaptive view. Subtract the hotspot after scaling the position instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * fix(flutter): read the live DPR and clamp the painted cursor like the native one CanvasModel caches devicePixelRatio and only refreshes it when the view style changes, so after the window moves to a monitor with a different DPR the unzoomed cursor kept the previous monitor's scale. Read it from MediaQuery instead, which also rebuilds the cursor when it changes. The native path clamps the scaled bitmap to kMinCursorSize; apply the same clamp to the painted cursor so a small cursor does not change size when the peer moves the mouse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8HSHTEHo27mDcJXVyVfSP * build(cursor): declare existing Zstd dependency for bounded decoding * fix(cursor): validate and bound received cursor images * fix(cursor): preserve thin cursor sizes when scaling * fix(cursor): limit view changes to native cursor sizing * fix(cursor): apply long-edge minimum to Web cursor sizing * fix: preserve Linux cursor alpha and match Windows Custom scale * fix(cursor): keep scaled buffers and raster dimensions in sync * fix(cursor): preserve Windows peer alpha when resizing * fix(cursor): preserve mixed alpha during downsampling * comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): match desktop cursor size to peer display density * refactor(cursor): clarify desktop and web scale branches * fix(cursor): correct adaptive display scaling and fixed cursor size * fix(cursor): apply all-display adaptive density on every desktop * fix(cursor): normalize unzoomed all-display cursor density * fix(cursor): synchronize original view on DPR changes * fix(cursor): match original zoom to the active renderer * fix: align Wayland software scrolling with input coordinates * fix: preserve scroll offsets when switching scrolling modes * fix: correct cursor size and pointer mapping with custom scale Apply the hovered Linux display density to Custom cursor zoom in All Displays. Refresh scroll fractions after layout so changing the Custom percentage uses current scrollbar extents and detached controllers. Validated with macOS and Windows component tests, formatting, and static analysis. * fix(linux): pad tall native cursors to prevent clipping * fix(cursor): preserve macOS point size in unzoomed views * fix(linux): pad rectangular cursors to square canvases * fix(cursor): divide dpr on Linux -> macOS Signed-off-by: fufesou <linlong1266@gmail.com> * fix: cursor size test Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): cursor size of controlled side macOS Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): limit received cursor allocations * test(cursor): retain reverse raster transition coverage * fix(cursor): bound compressed cursor input * fix(cursor): restrict the scaled size of the cursor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): align hotspots with resized raster dimensions Calculate each hotspot axis from the actual raster-to-source ratio. Update existing boundary tests and run cursor tests in Flutter CI. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): align Sciter hotspots with raster dimensions Calculate native and overlay hotspots from the final raster size. Preserve input coordinate scaling and cursor refresh order. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(cursor): use `1.0` instead of `1.0/dpr` on Linux -> macOS, Zoom off, Scale adaptive The mouse cursor currently appears somewhat large. However, this is difficult to adjust because the cursor size is fixed while the window size varies; its relative size depends on the specific desktop environment. We can modify it if users actually provide feedback. Ideally, we should check the "Zoom cursor" option. Further adjustments may also be needed later based on cursor density. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: rustdesk <info@rustdesk.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
53ec874c39 | Translate terminal clipboard tips to Chinese (#16266) | ||
|
|
04974591b8 |
Refine Portuguese translations for user options (#16264)
Updated Portuguese translations for clarity and conciseness. |
||
|
|
4da167517e |
Turkish: translate the three empty entries (#16261)
terminal-clipboard-write-tip, "Allow terminal apps to copy to clipboard" and the voice-call hint were empty, so Turkish users saw the English text. |