mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-11 22:42:00 +00:00
* 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * cursor: release only the shapes that can hold pixels Selecting a cursor swept every shape of the session to release pixels, though only two can hold any: the shape selected before, and a shape that finished decoding after the peer moved on. Release those, so a cursor_id costs the same however many shapes the peer has shown. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: show the native cursor limit holds two animations and the everyday set 64 is a working set, not the longest animation: KDE's wait and progress cursors are 23 frames each, and a session that meets both plus a dozen static shapes must keep them all, or every turn rebuilds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: let the shape in use keep its pixels for a new raster Dropping them once the native cursor was registered meant every scale change, zooming with the cursor zoomed or crossing to a display of another scale, showed the system arrow while the core gave the shape back and it decoded again. The shape in use now keeps them and makes the new raster at once; a shape switched away from still keeps none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: never fall back to the default cursor on a switch A switch to a shape without its image, or without a native cursor at its raster, showed the default arrow until the core gave the shape back and it decoded: the painted cursor on every switch back, the native one after an eviction or while a new shape decoded. - The shape shown before stays, image, hotspot and native cursor, until the one in use is ready. - Where the cursor is painted, on mobile or with the remote cursor shown, the images of the 64 shapes used last are kept, like the native cursors, so an animation does not decode a frame per turn. Elsewhere only the shape in use keeps one. - A shape that finishes decoding after the peer moved on is kept with them instead of dropped, so a fast animation fills them at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: name the dedupe's content-id set for what it holds `shown` read as the shape on screen. It is the shapes decoded and given to the UI, keyed by content id, so a shape arriving under a new handle is looked up once instead of scanning the handle map: `decoded`, `has_decoded`, and `record` for the call that fills both maps. The web mirror is renamed the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: keep native cursors for the session, as before Evicting native cursors and deleting a shape's replaced raster made Windows leak more than master. The engine's deleteCustomCursor/windows frees the HCURSOR with DeleteObject, which does not take a cursor, so a delete frees nothing and each cursor made again for a raster, or for a shape shown once more, leaks another handle. Keeping every raster of every shape until the session is cleared creates each one once, as master does. Fixing the engine instead is not cheap: x64 runs our fork on Flutter 3.24, where the fix is one more patch to rebuild and carry across every upgrade; arm64 runs the stock engine of a newer Flutter with the same code, where it means porting the fork and publishing an arm64 engine too, or waiting for an upstream fix to reach stable. Content ids keep the count to the shapes the peer really shows. The painted images, the compressed shapes in the core, and a switch showing the cursor shown before are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: record the Windows measurement behind keeping native cursors A measurement on Windows confirmed what the engine source implied: of 500 cursors made by CreateIconIndirect, DeleteObject freed none and left 500 USER objects alive, while DestroyCursor and DestroyIcon freed all 500. The comment now says so, that native cursors have no LRU for that reason only, and that one can come back once both engines destroy cursors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: look a native cursor up by the raster asked for The key ended in the raster made last. A shape switched away from keeps no pixels, so its raster stays where it was, and going back to a raster it was shown at before looked up the wrong key: A at 1.0, A at 0.5, then B, then A at 1.0 again missed the 32x32 cursor that was still there, fetched A from the core, and showed B meanwhile. With native cursors kept for every raster, the key now ends in the raster asked for; with pixels, that is the raster made, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: let a shape keep its pixels until a native cursor holds them A shape switched away from, or decoded after the peer moved on, let its pixels go whether or not it had a native cursor, and a native cursor is made only for the shape in use. A shape whose restore kept finishing after the next switch, as an animation's frames can, was fetched and decoded again every time it came back and never got a native cursor. A shape without one now keeps its pixels until one is made, at most kRecentShapes of them, the one waiting longest let go first. Mobile only paints the cursor, so it lets them go as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * proto: keep cursor ids as strings in JS A Windows peer before 1.5.0 names a cursor by its handle, sign-extended above 2^53 when its top bit is set. The web client decodes u64 fields into JS numbers, and ts-proto throws on a value a number cannot hold, so each such cursor_data or cursor_id was dropped whole and the web cursor stayed on the shape before. [jstype = JS_STRING] makes ts-proto generate these two fields as strings; the wire format, the Rust code generated and every other client are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: wait for a native cursor at the raster asked for, not any raster A shape let its pixels go once it had a native cursor at some raster. After a zoom, or on a display of another scale, an animation's frames had native cursors at the old raster only; each was fetched again at the new one, and a restore that finished after the next switch let its pixels go at once, so the frames never got a native cursor at the new raster and the cursor stayed on the one shown before. A lookup that finds no native cursor at the raster asked for now counts the shape as waiting for one, so a late restore keeps its pixels until it has one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: count a shape held again when its asked raster has a native cursor A lookup that missed took the shape out of _nativeIds, but one that hit did not put it back. A shape asked at a new raster, then at one it has a native cursor for, still counted as waiting: a restore finishing after the switch away kept its pixels in _awaitingNative instead of letting them go. The set now says whether the raster asked last has a native cursor, either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * Revert "cursor: count a shape held again when its asked raster has a native cursor" This reverts commit7cefbfc7a. _nativeIds is meant as "no raster of this shape is still missing its native cursor", not "the raster asked last has one". A shape asked at a new raster whose restore is on its way, then shown at a raster it has a cursor for, still waits for the new one; counting it held let the late restore's pixels go, so going back to the new raster fetched it again, and with an animation's frames and a scale that keeps changing, the loop1e4388771fixed could come back. Keeping the pixels costs at most a waiting slot, within the 64. A test now locks this: a raster still missing keeps a late restore whatever was shown meanwhile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: reuse a shape sent before without compressing it again A handle not seen before sent the service through the whole path: capture the pixels, name them by content, compress them, and only then find the shape already in CURSOR_SHAPES and drop what it compressed. An app that mints a new handle for the same cursor, as Chrome and Electron do, paid a compress per handle, a few ms for an enlarged cursor. The content id is now looked up first, and only a new shape is compressed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: bound the handles the service files, and the shapes with them A platform may mint a new handle each time it shows a shape, as Chrome and Electron do, so cached_cursor_data grew by an entry per handle for the life of the service, even when every one named the same shape; and the compressed shapes in CURSOR_SHAPES were bounded by nothing but the handles. Past 4096 handles, or 32 MiB of compressed shapes however few the handles, both start over together, keeping the shape being sent: a handle shown again is captured and named again. On every platform; the DRM build's own 64-entry ceiling is unchanged and comes first there. CURSOR_SHAPES keeps the bytes it holds beside the shapes, cleared with them, so every path that clears it resets the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: let the macOS seed report a change, and the content name the cursor macOS named a cursor by a fingerprint of its size, hotspot and two pixels near the hotspot. It collides between standard cursors: the open and the closed hand give one value, and the arrows with a badge (copy, not allowed, contextual menu, disappearing item) another, so a switch between them was never sent, or a cached shape of the other was. CGSCurrentCursorSeed already says when the cursor changed; get_cursor now reports the seed, and the shape is named by its content, as on every platform. The fingerprint is gone. get_cursor_data checks the seed before and after the capture. The seed was taken before capturing, so a capture that found the cursor changed again lost the change; it now leaves it for the next poll. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: file no macOS seeds, and send nothing for the shape already shown A macOS seed marks a change and never comes back, so filing it as a handle only filled the map; the byte ceiling still bounds the shapes there. A new handle or seed for the shape already shown, which a seed change without a new shape or an app minting handles gives, sent every connection a cursor_id for what it already showed; a shape is one message however many handles name it, so the same message is not sent again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: capture the macOS cursor shown without checking the seed around it Every macOS seed change is now captured, pixel by pixel, and a seed that moved during that capture failed it; the service then logged an error and slept a second, so the controller's cursor lagged while it changed. The check guarded nothing: the shape is named by its content, not the seed, and a change after get_cursor read the seed moves it on, so the next poll captures it anyway. Resetting the seed on a failed check was not needed for the same reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: fall back to the default cursor for a shape the core cannot give The shape shown before stays while the one in use decodes, so a switch does not flash the default cursor. It also stayed once the shape in use was known to be lost (rejected by the core, or not decoding): the controller kept drawing, say, an I-beam for a pointer it never got, until the peer changed shape. Both the native and the painted cursor now leave the default one, as master did for the native cursor. A shape that does not decode is marked in updateCursorData, whether it was sent or fetched again, so both paths agree. The client also selects a shape it rejects as it arrives. It showed the shape before until the peer selected the rejected one again, and the default cursor from then on; it now looks the same both times. Sciter ignores an id it has no shape for, as it did. The default and forbidden cursors are made with the model. They were made the first time a build asked for one, and their decode lands later with nothing to draw again, so that first build showed the shape before in their place. A fetch failing after the session was cleared marks nothing in the new one, and a fetch that throws at once tells the listeners after the build that asked for it, not during it. The forbidden cursor, shown while input is off, became the cursor shown last, so a raster made after input came back showed it until the shape's pixels were fetched, or for good if they never came. Only a shape stands in for a shape now; CursorModel.shown keeps the rule for both builders. _checkView asks for a single registration again: every native cursor is made once per session, and one made twice leaks an HCURSOR on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: name a handle by the shape rejected under it, not the one before A shape the client rejects is selected as it arrives, so that it shows the default cursor then as it does later. Under a handle reused for it, the dedupe still named the shape the handle had brought before, and selecting that switched the pointer to an old shape: A under handle 7, then B under 8, then a rejected C under 7 showed A in place of B, and again whenever the peer selected 7. The handle now names the rejected content, which is never marked decoded, since only a shape not decoded before reaches the decode: the UI has no shape under it and shows the default cursor, then and when the handle is selected again. The shape the handle named before stays kept for the other handles naming it, and a handle that brings it again names it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: file no handle for a shape that is rejected under it A shape rejected under a handle named the handle by the rejected content, so that a known handle does not show the shape it brought before. A handle never seen was filed the same way, so a peer sending shapes that do not decode under new handles grew the map with each, where a rejected shape used to leave nothing. Only a known handle is renamed now; one never seen is not filed and is passed on as unknown, which the UI shows as the default cursor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab * cursor: say what Future.sync defers, and give the default cursor time on a slow runner The comment on the fetch in restorePixels read as if the fetch were put off; it runs at once, and Future.sync only turns its throw into an error the await hands over later, which is what keeps the notify out of the build. cursor_default_test gave the default cursor 100 ms to decode, which a slow runner may miss. It cannot wait for the cursor itself, since asking for it would make it; it now waits 500 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1035 lines
22 KiB
Protocol Buffer
1035 lines
22 KiB
Protocol Buffer
syntax = "proto3";
|
|
package hbb;
|
|
|
|
message EncodedVideoFrame {
|
|
bytes data = 1;
|
|
bool key = 2;
|
|
int64 pts = 3;
|
|
}
|
|
|
|
message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; }
|
|
|
|
message RGB { bool compress = 1; }
|
|
|
|
// planes data send directly in binary for better use arraybuffer on web
|
|
message YUV {
|
|
bool compress = 1;
|
|
int32 stride = 2;
|
|
}
|
|
|
|
enum Chroma {
|
|
I420 = 0;
|
|
I444 = 1;
|
|
}
|
|
|
|
message VideoFrame {
|
|
oneof union {
|
|
EncodedVideoFrames vp9s = 6;
|
|
RGB rgb = 7;
|
|
YUV yuv = 8;
|
|
EncodedVideoFrames h264s = 10;
|
|
EncodedVideoFrames h265s = 11;
|
|
EncodedVideoFrames vp8s = 12;
|
|
EncodedVideoFrames av1s = 13;
|
|
}
|
|
int32 display = 14;
|
|
}
|
|
|
|
message DisplayInfo {
|
|
sint32 x = 1;
|
|
sint32 y = 2;
|
|
int32 width = 3;
|
|
int32 height = 4;
|
|
string name = 5;
|
|
bool online = 6;
|
|
bool cursor_embedded = 7;
|
|
Resolution original_resolution = 8;
|
|
double scale = 9;
|
|
}
|
|
|
|
message PortForward {
|
|
string host = 1;
|
|
int32 port = 2;
|
|
bool multiplex = 3;
|
|
}
|
|
|
|
message FileTransfer {
|
|
string dir = 1;
|
|
bool show_hidden = 2;
|
|
}
|
|
|
|
message ViewCamera {}
|
|
|
|
message OSLogin {
|
|
string username = 1;
|
|
string password = 2;
|
|
}
|
|
|
|
message LoginRequest {
|
|
string username = 1;
|
|
bytes password = 2;
|
|
string my_id = 4;
|
|
string my_name = 5;
|
|
OptionMessage option = 6;
|
|
oneof union {
|
|
FileTransfer file_transfer = 7;
|
|
PortForward port_forward = 8;
|
|
ViewCamera view_camera = 15;
|
|
Terminal terminal = 16;
|
|
}
|
|
bool video_ack_required = 9;
|
|
uint64 session_id = 10;
|
|
string version = 11;
|
|
OSLogin os_login = 12;
|
|
string my_platform = 13;
|
|
bytes hwid = 14;
|
|
string avatar = 17;
|
|
}
|
|
|
|
message Terminal {
|
|
string service_id = 1; // Service ID for reconnecting to existing session
|
|
}
|
|
|
|
message Auth2FA {
|
|
string code = 1;
|
|
bytes hwid = 2;
|
|
}
|
|
|
|
message ChatMessage { string text = 1; }
|
|
|
|
message Features {
|
|
bool privacy_mode = 1;
|
|
bool terminal = 2;
|
|
bool port_forward_mux = 3;
|
|
}
|
|
|
|
message CodecAbility {
|
|
bool vp8 = 1;
|
|
bool vp9 = 2;
|
|
bool av1 = 3;
|
|
bool h264 = 4;
|
|
bool h265 = 5;
|
|
}
|
|
|
|
message SupportedEncoding {
|
|
bool h264 = 1;
|
|
bool h265 = 2;
|
|
bool vp8 = 3;
|
|
bool av1 = 4;
|
|
CodecAbility i444 = 5;
|
|
}
|
|
|
|
message PeerInfo {
|
|
string username = 1;
|
|
string hostname = 2;
|
|
string platform = 3;
|
|
repeated DisplayInfo displays = 4;
|
|
int32 current_display = 5;
|
|
bool sas_enabled = 6;
|
|
string version = 7;
|
|
Features features = 9;
|
|
SupportedEncoding encoding = 10;
|
|
SupportedResolutions resolutions = 11;
|
|
// Use JSON's key-value format which is friendly for peer to handle.
|
|
// NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string.
|
|
string platform_additions = 12;
|
|
WindowsSessions windows_sessions = 13;
|
|
}
|
|
|
|
message WindowsSession {
|
|
uint32 sid = 1;
|
|
string name = 2;
|
|
}
|
|
|
|
message LoginResponse {
|
|
oneof union {
|
|
string error = 1;
|
|
PeerInfo peer_info = 2;
|
|
}
|
|
bool enable_trusted_devices = 3;
|
|
}
|
|
|
|
message TouchScaleUpdate {
|
|
// The delta scale factor relative to the previous scale.
|
|
// delta * 1000
|
|
// 0 means scale end
|
|
int32 scale = 1;
|
|
}
|
|
|
|
message TouchPanStart {
|
|
int32 x = 1;
|
|
int32 y = 2;
|
|
}
|
|
|
|
message TouchPanUpdate {
|
|
// The delta x position relative to the previous position.
|
|
int32 x = 1;
|
|
// The delta y position relative to the previous position.
|
|
int32 y = 2;
|
|
}
|
|
|
|
message TouchPanEnd {
|
|
int32 x = 1;
|
|
int32 y = 2;
|
|
}
|
|
|
|
message TouchEvent {
|
|
oneof union {
|
|
TouchScaleUpdate scale_update = 1;
|
|
TouchPanStart pan_start = 2;
|
|
TouchPanUpdate pan_update = 3;
|
|
TouchPanEnd pan_end = 4;
|
|
}
|
|
}
|
|
|
|
message PointerDeviceEvent {
|
|
oneof union {
|
|
TouchEvent touch_event = 1;
|
|
}
|
|
repeated ControlKey modifiers = 2;
|
|
}
|
|
|
|
message MouseEvent {
|
|
int32 mask = 1;
|
|
sint32 x = 2;
|
|
sint32 y = 3;
|
|
repeated ControlKey modifiers = 4;
|
|
}
|
|
|
|
enum KeyboardMode{
|
|
Legacy = 0;
|
|
Map = 1;
|
|
Translate = 2;
|
|
Auto = 3;
|
|
}
|
|
|
|
enum ControlKey {
|
|
Unknown = 0;
|
|
Alt = 1;
|
|
Backspace = 2;
|
|
CapsLock = 3;
|
|
Control = 4;
|
|
Delete = 5;
|
|
DownArrow = 6;
|
|
End = 7;
|
|
Escape = 8;
|
|
F1 = 9;
|
|
F10 = 10;
|
|
F11 = 11;
|
|
F12 = 12;
|
|
F2 = 13;
|
|
F3 = 14;
|
|
F4 = 15;
|
|
F5 = 16;
|
|
F6 = 17;
|
|
F7 = 18;
|
|
F8 = 19;
|
|
F9 = 20;
|
|
Home = 21;
|
|
LeftArrow = 22;
|
|
/// meta key (also known as "windows"; "super"; and "command")
|
|
Meta = 23;
|
|
/// option key on macOS (alt key on Linux and Windows)
|
|
Option = 24; // deprecated, use Alt instead
|
|
PageDown = 25;
|
|
PageUp = 26;
|
|
Return = 27;
|
|
RightArrow = 28;
|
|
Shift = 29;
|
|
Space = 30;
|
|
Tab = 31;
|
|
UpArrow = 32;
|
|
Numpad0 = 33;
|
|
Numpad1 = 34;
|
|
Numpad2 = 35;
|
|
Numpad3 = 36;
|
|
Numpad4 = 37;
|
|
Numpad5 = 38;
|
|
Numpad6 = 39;
|
|
Numpad7 = 40;
|
|
Numpad8 = 41;
|
|
Numpad9 = 42;
|
|
Cancel = 43;
|
|
Clear = 44;
|
|
Menu = 45; // deprecated, use Alt instead
|
|
Pause = 46;
|
|
Kana = 47;
|
|
Hangul = 48;
|
|
Junja = 49;
|
|
Final = 50;
|
|
Hanja = 51;
|
|
Kanji = 52;
|
|
Convert = 53;
|
|
Select = 54;
|
|
Print = 55;
|
|
Execute = 56;
|
|
Snapshot = 57;
|
|
Insert = 58;
|
|
Help = 59;
|
|
Sleep = 60;
|
|
Separator = 61;
|
|
Scroll = 62;
|
|
NumLock = 63;
|
|
RWin = 64;
|
|
Apps = 65;
|
|
Multiply = 66;
|
|
Add = 67;
|
|
Subtract = 68;
|
|
Decimal = 69;
|
|
Divide = 70;
|
|
Equals = 71;
|
|
NumpadEnter = 72;
|
|
RShift = 73;
|
|
RControl = 74;
|
|
RAlt = 75;
|
|
VolumeMute = 76; // mainly used on mobile devices as controlled side
|
|
VolumeUp = 77;
|
|
VolumeDown = 78;
|
|
Power = 79; // mainly used on mobile devices as controlled side
|
|
CtrlAltDel = 100;
|
|
LockScreen = 101;
|
|
}
|
|
|
|
message KeyEvent {
|
|
// `down` indicates the key's state(down or up).
|
|
bool down = 1;
|
|
// `press` indicates a click event(down and up).
|
|
bool press = 2;
|
|
oneof union {
|
|
ControlKey control_key = 3;
|
|
// position key code. win: scancode, linux: key code, macos: key code
|
|
uint32 chr = 4;
|
|
uint32 unicode = 5;
|
|
string seq = 6;
|
|
// high word. virtual keycode
|
|
// low word. unicode
|
|
uint32 win2win_hotkey = 7;
|
|
}
|
|
repeated ControlKey modifiers = 8;
|
|
KeyboardMode mode = 9;
|
|
}
|
|
|
|
message CursorData {
|
|
// A string in JS: a Windows peer before 1.5.0 sends its cursor handle, sign-extended above
|
|
// 2^53 when its top bit is set, which a JS number cannot hold.
|
|
uint64 id = 1 [jstype = JS_STRING];
|
|
sint32 hotx = 2;
|
|
sint32 hoty = 3;
|
|
int32 width = 4;
|
|
int32 height = 5;
|
|
bytes colors = 6;
|
|
}
|
|
|
|
message CursorPosition {
|
|
sint32 x = 1;
|
|
sint32 y = 2;
|
|
}
|
|
|
|
message Hash {
|
|
string salt = 1;
|
|
string challenge = 2;
|
|
}
|
|
|
|
enum ClipboardFormat {
|
|
Text = 0;
|
|
Rtf = 1;
|
|
Html = 2;
|
|
ImageRgba = 21;
|
|
ImagePng = 22;
|
|
ImageSvg = 23;
|
|
Special = 31;
|
|
}
|
|
|
|
message Clipboard {
|
|
bool compress = 1;
|
|
bytes content = 2;
|
|
int32 width = 3;
|
|
int32 height = 4;
|
|
ClipboardFormat format = 5;
|
|
// Special format name, only used when format is Special.
|
|
string special_name = 6;
|
|
}
|
|
|
|
message MultiClipboards { repeated Clipboard clipboards = 1; }
|
|
|
|
enum FileType {
|
|
Dir = 0;
|
|
DirLink = 2;
|
|
DirDrive = 3;
|
|
File = 4;
|
|
FileLink = 5;
|
|
}
|
|
|
|
message FileEntry {
|
|
FileType entry_type = 1;
|
|
string name = 2;
|
|
bool is_hidden = 3;
|
|
uint64 size = 4;
|
|
uint64 modified_time = 5;
|
|
}
|
|
|
|
message FileDirectory {
|
|
int32 id = 1;
|
|
string path = 2;
|
|
repeated FileEntry entries = 3;
|
|
}
|
|
|
|
message ReadDir {
|
|
string path = 1;
|
|
bool include_hidden = 2;
|
|
}
|
|
|
|
message ReadEmptyDirs {
|
|
string path = 1;
|
|
bool include_hidden = 2;
|
|
}
|
|
|
|
message ReadEmptyDirsResponse {
|
|
string path = 1;
|
|
repeated FileDirectory empty_dirs = 2;
|
|
}
|
|
|
|
message ReadAllFiles {
|
|
int32 id = 1;
|
|
string path = 2;
|
|
bool include_hidden = 3;
|
|
}
|
|
|
|
message FileRename {
|
|
int32 id = 1;
|
|
string path = 2;
|
|
string new_name = 3;
|
|
}
|
|
|
|
message FileAction {
|
|
oneof union {
|
|
ReadDir read_dir = 1;
|
|
FileTransferSendRequest send = 2;
|
|
FileTransferReceiveRequest receive = 3;
|
|
FileDirCreate create = 4;
|
|
FileRemoveDir remove_dir = 5;
|
|
FileRemoveFile remove_file = 6;
|
|
ReadAllFiles all_files = 7;
|
|
FileTransferCancel cancel = 8;
|
|
FileTransferSendConfirmRequest send_confirm = 9;
|
|
FileRename rename = 10;
|
|
ReadEmptyDirs read_empty_dirs = 11;
|
|
}
|
|
}
|
|
|
|
message FileTransferCancel { int32 id = 1; }
|
|
|
|
message FileResponse {
|
|
oneof union {
|
|
FileDirectory dir = 1;
|
|
FileTransferBlock block = 2;
|
|
FileTransferError error = 3;
|
|
FileTransferDone done = 4;
|
|
FileTransferDigest digest = 5;
|
|
ReadEmptyDirsResponse empty_dirs = 6;
|
|
}
|
|
}
|
|
|
|
message FileTransferDigest {
|
|
int32 id = 1;
|
|
sint32 file_num = 2;
|
|
uint64 last_modified = 3;
|
|
uint64 file_size = 4;
|
|
bool is_upload = 5;
|
|
bool is_identical = 6;
|
|
uint64 transferred_size = 7; // For resume. Indicates the size of the file already transferred
|
|
bool is_resume = 8; // For resume. Indicates if the transfer is a resume.
|
|
// `is_resume` can let the controlled side know whether to check the `.digest` file.
|
|
// When `is_resume` is false, `.digest` exists, the same file does not exist,
|
|
// the controlled side should not check `.digest`, it should confirm with a new transfer request.
|
|
}
|
|
|
|
message FileTransferBlock {
|
|
int32 id = 1;
|
|
sint32 file_num = 2;
|
|
bytes data = 3;
|
|
bool compressed = 4;
|
|
uint32 blk_id = 5;
|
|
}
|
|
|
|
message FileTransferError {
|
|
int32 id = 1;
|
|
string error = 2;
|
|
sint32 file_num = 3;
|
|
}
|
|
|
|
message FileTransferSendRequest {
|
|
int32 id = 1;
|
|
string path = 2;
|
|
bool include_hidden = 3;
|
|
int32 file_num = 4;
|
|
|
|
enum FileType {
|
|
Generic = 0;
|
|
Printer = 1;
|
|
}
|
|
FileType file_type = 5;
|
|
}
|
|
|
|
message FileTransferSendConfirmRequest {
|
|
int32 id = 1;
|
|
sint32 file_num = 2;
|
|
oneof union {
|
|
bool skip = 3;
|
|
uint32 offset_blk = 4;
|
|
}
|
|
}
|
|
|
|
message FileTransferDone {
|
|
int32 id = 1;
|
|
sint32 file_num = 2;
|
|
}
|
|
|
|
message FileTransferReceiveRequest {
|
|
int32 id = 1;
|
|
string path = 2; // path written to
|
|
repeated FileEntry files = 3;
|
|
int32 file_num = 4;
|
|
uint64 total_size = 5;
|
|
}
|
|
|
|
message FileRemoveDir {
|
|
int32 id = 1;
|
|
string path = 2;
|
|
bool recursive = 3;
|
|
}
|
|
|
|
message FileRemoveFile {
|
|
int32 id = 1;
|
|
string path = 2;
|
|
sint32 file_num = 3;
|
|
}
|
|
|
|
message FileDirCreate {
|
|
int32 id = 1;
|
|
string path = 2;
|
|
}
|
|
|
|
// main logic from freeRDP
|
|
message CliprdrMonitorReady {
|
|
}
|
|
|
|
message CliprdrFormat {
|
|
int32 id = 2;
|
|
string format = 3;
|
|
}
|
|
|
|
message CliprdrServerFormatList {
|
|
repeated CliprdrFormat formats = 2;
|
|
}
|
|
|
|
message CliprdrServerFormatListResponse {
|
|
int32 msg_flags = 2;
|
|
}
|
|
|
|
message CliprdrServerFormatDataRequest {
|
|
int32 requested_format_id = 2;
|
|
}
|
|
|
|
message CliprdrServerFormatDataResponse {
|
|
int32 msg_flags = 2;
|
|
bytes format_data = 3;
|
|
}
|
|
|
|
message CliprdrFileContentsRequest {
|
|
int32 stream_id = 2;
|
|
int32 list_index = 3;
|
|
int32 dw_flags = 4;
|
|
int32 n_position_low = 5;
|
|
int32 n_position_high = 6;
|
|
int32 cb_requested = 7;
|
|
bool have_clip_data_id = 8;
|
|
int32 clip_data_id = 9;
|
|
}
|
|
|
|
message CliprdrFileContentsResponse {
|
|
int32 msg_flags = 3;
|
|
int32 stream_id = 4;
|
|
bytes requested_data = 5;
|
|
}
|
|
|
|
// Try empty clipboard in the following case(Windows only):
|
|
// 1. `A`(Windows) -> `B`, `C`
|
|
// 2. Copy in `A, file clipboards on `B` and `C` are updated.
|
|
// 3. Copy in `B`.
|
|
// `A` should tell `C` to empty the file clipboard.
|
|
message CliprdrTryEmpty {
|
|
}
|
|
|
|
// Clipobard file message for audit.
|
|
message CliprdrFile {
|
|
string name = 1;
|
|
uint64 size = 2;
|
|
}
|
|
|
|
message CliprdrFiles {
|
|
repeated CliprdrFile files = 1;
|
|
}
|
|
|
|
message Cliprdr {
|
|
oneof union {
|
|
CliprdrMonitorReady ready = 1;
|
|
CliprdrServerFormatList format_list = 2;
|
|
CliprdrServerFormatListResponse format_list_response = 3;
|
|
CliprdrServerFormatDataRequest format_data_request = 4;
|
|
CliprdrServerFormatDataResponse format_data_response = 5;
|
|
CliprdrFileContentsRequest file_contents_request = 6;
|
|
CliprdrFileContentsResponse file_contents_response = 7;
|
|
CliprdrTryEmpty try_empty = 8;
|
|
CliprdrFiles files = 9;
|
|
}
|
|
}
|
|
|
|
message Resolution {
|
|
int32 width = 1;
|
|
int32 height = 2;
|
|
}
|
|
|
|
message DisplayResolution {
|
|
int32 display = 1;
|
|
Resolution resolution = 2;
|
|
}
|
|
|
|
message SupportedResolutions { repeated Resolution resolutions = 1; }
|
|
|
|
message SwitchDisplay {
|
|
int32 display = 1;
|
|
sint32 x = 2;
|
|
sint32 y = 3;
|
|
int32 width = 4;
|
|
int32 height = 5;
|
|
bool cursor_embedded = 6;
|
|
SupportedResolutions resolutions = 7;
|
|
// Do not care about the origin point for now.
|
|
Resolution original_resolution = 8;
|
|
}
|
|
|
|
message CaptureDisplays {
|
|
repeated int32 add = 1;
|
|
repeated int32 sub = 2;
|
|
repeated int32 set = 3;
|
|
}
|
|
|
|
message ToggleVirtualDisplay {
|
|
int32 display = 1;
|
|
bool on = 2;
|
|
}
|
|
|
|
message TogglePrivacyMode {
|
|
string impl_key = 1;
|
|
bool on = 2;
|
|
}
|
|
|
|
message PermissionInfo {
|
|
enum Permission {
|
|
Keyboard = 0;
|
|
Clipboard = 2;
|
|
Audio = 3;
|
|
File = 4;
|
|
Restart = 5;
|
|
Recording = 6;
|
|
BlockInput = 7;
|
|
PrivacyMode = 8;
|
|
}
|
|
|
|
Permission permission = 1;
|
|
bool enabled = 2;
|
|
}
|
|
|
|
enum ImageQuality {
|
|
NotSet = 0;
|
|
Low = 2;
|
|
Balanced = 3;
|
|
Best = 4;
|
|
}
|
|
|
|
message SupportedDecoding {
|
|
enum PreferCodec {
|
|
Auto = 0;
|
|
VP9 = 1;
|
|
H264 = 2;
|
|
H265 = 3;
|
|
VP8 = 4;
|
|
AV1 = 5;
|
|
}
|
|
|
|
int32 ability_vp9 = 1;
|
|
int32 ability_h264 = 2;
|
|
int32 ability_h265 = 3;
|
|
PreferCodec prefer = 4;
|
|
int32 ability_vp8 = 5;
|
|
int32 ability_av1 = 6;
|
|
CodecAbility i444 = 7;
|
|
Chroma prefer_chroma = 8;
|
|
}
|
|
|
|
message OptionMessage {
|
|
enum BoolOption {
|
|
NotSet = 0;
|
|
No = 1;
|
|
Yes = 2;
|
|
}
|
|
ImageQuality image_quality = 1;
|
|
BoolOption lock_after_session_end = 2;
|
|
BoolOption show_remote_cursor = 3;
|
|
BoolOption privacy_mode = 4;
|
|
BoolOption block_input = 5;
|
|
int32 custom_image_quality = 6;
|
|
BoolOption disable_audio = 7;
|
|
BoolOption disable_clipboard = 8;
|
|
BoolOption enable_file_transfer = 9;
|
|
SupportedDecoding supported_decoding = 10;
|
|
int32 custom_fps = 11;
|
|
BoolOption disable_keyboard = 12;
|
|
// Position 13 is used for Resolution. Remove later.
|
|
// Resolution custom_resolution = 13;
|
|
// BoolOption support_windows_specific_session = 14;
|
|
// starting from 15 please, do not use removed fields
|
|
BoolOption follow_remote_cursor = 15;
|
|
BoolOption follow_remote_window = 16;
|
|
BoolOption disable_camera = 17;
|
|
BoolOption terminal_persistent = 18;
|
|
BoolOption show_my_cursor = 19;
|
|
}
|
|
|
|
message TestDelay {
|
|
int64 time = 1;
|
|
bool from_client = 2;
|
|
uint32 last_delay = 3;
|
|
uint32 target_bitrate = 4;
|
|
}
|
|
|
|
message PublicKey {
|
|
bytes asymmetric_value = 1;
|
|
bytes symmetric_value = 2;
|
|
// The key exchange version the controller picked, the highest both sides speak; absent,
|
|
// i.e. 0, is the original scheme.
|
|
uint32 kx_version = 3;
|
|
}
|
|
|
|
message SignedId { bytes id = 1; }
|
|
|
|
message AudioFormat {
|
|
uint32 sample_rate = 1;
|
|
uint32 channels = 2;
|
|
}
|
|
|
|
message AudioFrame {
|
|
bytes data = 1;
|
|
}
|
|
|
|
// Notify peer to show message box.
|
|
message MessageBox {
|
|
// Message type. Refer to flutter/lib/common.dart/msgBox().
|
|
string msgtype = 1;
|
|
string title = 2;
|
|
// English
|
|
string text = 3;
|
|
// If not empty, msgbox provides a button to following the link.
|
|
// The link here can't be directly http url.
|
|
// It must be the key of http url configed in peer side or "rustdesk://*" (jump in app).
|
|
string link = 4;
|
|
}
|
|
|
|
message BackNotification {
|
|
// no need to consider block input by someone else
|
|
enum BlockInputState {
|
|
BlkStateUnknown = 0;
|
|
BlkOnSucceeded = 2;
|
|
BlkOnFailed = 3;
|
|
BlkOffSucceeded = 4;
|
|
BlkOffFailed = 5;
|
|
}
|
|
enum PrivacyModeState {
|
|
PrvStateUnknown = 0;
|
|
// Privacy mode on by someone else
|
|
PrvOnByOther = 2;
|
|
// Privacy mode is not supported on the remote side
|
|
PrvNotSupported = 3;
|
|
// Privacy mode on by self
|
|
PrvOnSucceeded = 4;
|
|
// Privacy mode on by self, but denied
|
|
PrvOnFailedDenied = 5;
|
|
// Some plugins are not found
|
|
PrvOnFailedPlugin = 6;
|
|
// Privacy mode on by self, but failed
|
|
PrvOnFailed = 7;
|
|
// Privacy mode off by self
|
|
PrvOffSucceeded = 8;
|
|
// Ctrl + P
|
|
PrvOffByPeer = 9;
|
|
// Privacy mode off by self, but failed
|
|
PrvOffFailed = 10;
|
|
PrvOffUnknown = 11;
|
|
}
|
|
|
|
oneof union {
|
|
PrivacyModeState privacy_mode_state = 1;
|
|
BlockInputState block_input_state = 2;
|
|
}
|
|
// Supplementary message, for "PrvOnFailed" and "PrvOffFailed"
|
|
string details = 3;
|
|
// The key of the implementation
|
|
string impl_key = 4;
|
|
}
|
|
|
|
message ElevationRequestWithLogon {
|
|
string username = 1;
|
|
string password = 2;
|
|
}
|
|
|
|
message ElevationRequest {
|
|
oneof union {
|
|
bool direct = 1;
|
|
ElevationRequestWithLogon logon = 2;
|
|
}
|
|
}
|
|
|
|
message SwitchSidesRequest {
|
|
bytes uuid = 1;
|
|
}
|
|
|
|
message SwitchSidesResponse {
|
|
bytes uuid = 1;
|
|
LoginRequest lr = 2;
|
|
}
|
|
|
|
message SwitchBack {}
|
|
|
|
message PluginRequest {
|
|
string id = 1;
|
|
bytes content = 2;
|
|
}
|
|
|
|
message PluginFailure {
|
|
string id = 1;
|
|
string name = 2;
|
|
string msg = 3;
|
|
}
|
|
|
|
message WindowsSessions {
|
|
repeated WindowsSession sessions = 1;
|
|
uint32 current_sid = 2;
|
|
}
|
|
|
|
// Query messages from peer.
|
|
message MessageQuery {
|
|
// The SwitchDisplay message of the target display.
|
|
// If the target display is not found, the message will be ignored.
|
|
int32 switch_display = 1;
|
|
}
|
|
|
|
message Misc {
|
|
oneof union {
|
|
ChatMessage chat_message = 4;
|
|
SwitchDisplay switch_display = 5;
|
|
PermissionInfo permission_info = 6;
|
|
OptionMessage option = 7;
|
|
AudioFormat audio_format = 8;
|
|
string close_reason = 9;
|
|
bool refresh_video = 10;
|
|
bool video_received = 12;
|
|
BackNotification back_notification = 13;
|
|
bool restart_remote_device = 14;
|
|
bool uac = 15;
|
|
bool foreground_window_elevated = 16;
|
|
bool stop_service = 17;
|
|
ElevationRequest elevation_request = 18;
|
|
string elevation_response = 19;
|
|
bool portable_service_running = 20;
|
|
SwitchSidesRequest switch_sides_request = 21;
|
|
SwitchBack switch_back = 22;
|
|
// Deprecated since 1.2.4, use `change_display_resolution` (36) instead.
|
|
// But we must keep it for compatibility when peer version < 1.2.4.
|
|
Resolution change_resolution = 24;
|
|
PluginRequest plugin_request = 25;
|
|
PluginFailure plugin_failure = 26;
|
|
uint32 full_speed_fps = 27; // deprecated
|
|
uint32 auto_adjust_fps = 28;
|
|
bool client_record_status = 29;
|
|
CaptureDisplays capture_displays = 30;
|
|
int32 refresh_video_display = 31;
|
|
ToggleVirtualDisplay toggle_virtual_display = 32;
|
|
TogglePrivacyMode toggle_privacy_mode = 33;
|
|
SupportedEncoding supported_encoding = 34;
|
|
uint32 selected_sid = 35;
|
|
DisplayResolution change_display_resolution = 36;
|
|
MessageQuery message_query = 37;
|
|
int32 follow_current_display = 38;
|
|
// Since 1.5.0: send the CursorData of this id again. No controller sends this yet; it is
|
|
// here so a controller can one day bound its own shape store and ask for the shape the
|
|
// peer just selected. The peer answers with what it holds then: a shape its cache dropped
|
|
// is rebuilt, and held again, the next time it is shown, before any connection names it
|
|
// by id again, so the shape just selected is always there to give.
|
|
uint64 request_cursor_data = 39;
|
|
}
|
|
}
|
|
|
|
message VoiceCallRequest {
|
|
int64 req_timestamp = 1;
|
|
// Indicates whether the request is a connect action or a disconnect action.
|
|
bool is_connect = 2;
|
|
}
|
|
|
|
message VoiceCallResponse {
|
|
bool accepted = 1;
|
|
int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp].
|
|
int64 ack_timestamp = 3;
|
|
}
|
|
|
|
message ScreenshotRequest {
|
|
int32 display = 1;
|
|
// sid is the session id on the controlling side
|
|
// It is used to forward the message to the correct remote (session) window.
|
|
string sid = 2;
|
|
}
|
|
|
|
message ScreenshotResponse {
|
|
string sid = 1;
|
|
// empty if success
|
|
string msg = 2;
|
|
bytes data = 3;
|
|
}
|
|
|
|
// Terminal messages - standalone feature like FileAction
|
|
message OpenTerminal {
|
|
int32 terminal_id = 1; // 0 for default terminal
|
|
uint32 rows = 2;
|
|
uint32 cols = 3;
|
|
}
|
|
|
|
message ResizeTerminal {
|
|
int32 terminal_id = 1;
|
|
uint32 rows = 2;
|
|
uint32 cols = 3;
|
|
}
|
|
|
|
message TerminalData {
|
|
int32 terminal_id = 1;
|
|
bytes data = 2;
|
|
bool compressed = 3;
|
|
}
|
|
|
|
message CloseTerminal {
|
|
int32 terminal_id = 1;
|
|
}
|
|
|
|
message TerminalAction {
|
|
oneof union {
|
|
OpenTerminal open = 1;
|
|
TerminalData data = 2;
|
|
ResizeTerminal resize = 3;
|
|
CloseTerminal close = 4;
|
|
}
|
|
}
|
|
|
|
message TerminalOpened {
|
|
int32 terminal_id = 1;
|
|
bool success = 2;
|
|
string message = 3;
|
|
uint32 pid = 4;
|
|
string service_id = 5; // Service ID for persistent sessions
|
|
repeated int32 persistent_sessions = 6; // Used to restore the persistent sessions.
|
|
bool replay_terminal_output = 7; // Whether the next data response replays buffered terminal output.
|
|
}
|
|
|
|
message TerminalClosed {
|
|
int32 terminal_id = 1;
|
|
int32 exit_code = 2;
|
|
}
|
|
|
|
message TerminalError {
|
|
int32 terminal_id = 1;
|
|
string message = 2;
|
|
}
|
|
|
|
message TerminalResponse {
|
|
oneof union {
|
|
TerminalOpened opened = 1;
|
|
TerminalData data = 2;
|
|
TerminalClosed closed = 3;
|
|
TerminalError error = 4;
|
|
}
|
|
}
|
|
|
|
message PortForwardOpen {
|
|
int32 channel_id = 1;
|
|
string host = 2;
|
|
int32 port = 3;
|
|
uint32 window = 4;
|
|
}
|
|
|
|
message PortForwardOpened {
|
|
int32 channel_id = 1;
|
|
bool success = 2;
|
|
string message = 3;
|
|
uint32 window = 4;
|
|
}
|
|
|
|
message PortForwardData {
|
|
int32 channel_id = 1;
|
|
bytes data = 2;
|
|
}
|
|
|
|
message PortForwardClose {
|
|
int32 channel_id = 1;
|
|
}
|
|
|
|
message PortForwardWindowUpdate {
|
|
int32 channel_id = 1;
|
|
uint32 add = 2;
|
|
}
|
|
|
|
// One symmetric message for both directions. `open` only travels controller -> controlled,
|
|
// `opened` only controlled -> controller; a frame in the wrong direction is ignored.
|
|
message PortForwardChannel {
|
|
oneof union {
|
|
PortForwardOpen open = 1;
|
|
PortForwardOpened opened = 2;
|
|
PortForwardData data = 3;
|
|
PortForwardClose close = 4;
|
|
PortForwardWindowUpdate window_update = 5;
|
|
}
|
|
}
|
|
|
|
message Message {
|
|
oneof union {
|
|
SignedId signed_id = 3;
|
|
PublicKey public_key = 4;
|
|
TestDelay test_delay = 5;
|
|
VideoFrame video_frame = 6;
|
|
LoginRequest login_request = 7;
|
|
LoginResponse login_response = 8;
|
|
Hash hash = 9;
|
|
MouseEvent mouse_event = 10;
|
|
AudioFrame audio_frame = 11;
|
|
CursorData cursor_data = 12;
|
|
CursorPosition cursor_position = 13;
|
|
uint64 cursor_id = 14 [jstype = JS_STRING]; // as CursorData.id
|
|
KeyEvent key_event = 15;
|
|
Clipboard clipboard = 16;
|
|
FileAction file_action = 17;
|
|
FileResponse file_response = 18;
|
|
Misc misc = 19;
|
|
Cliprdr cliprdr = 20;
|
|
MessageBox message_box = 21;
|
|
SwitchSidesResponse switch_sides_response = 22;
|
|
VoiceCallRequest voice_call_request = 23;
|
|
VoiceCallResponse voice_call_response = 24;
|
|
PeerInfo peer_info = 25;
|
|
PointerDeviceEvent pointer_device_event = 26;
|
|
Auth2FA auth_2fa = 27;
|
|
MultiClipboards multi_clipboards = 28;
|
|
ScreenshotRequest screenshot_request = 29;
|
|
ScreenshotResponse screenshot_response= 30;
|
|
TerminalAction terminal_action = 31;
|
|
TerminalResponse terminal_response = 32;
|
|
PortForwardChannel port_forward_channel = 33;
|
|
}
|
|
}
|