fix(web): recover file-upload state when a paste is interrupted or never pulled#1372
fix(web): recover file-upload state when a paste is interrupted or never pulled#1372Yuval Marcus (ymarcus93) wants to merge 1 commit into
Conversation
3c14ae1 to
b3dc24f
Compare
|
Hey Benoît Cortier (@CBenoit), do you mind taking a look at this PR? It fixes an issue where file upload no longer works (with no way to recover except a hard refresh) if we get to this broken state |
b3dc24f to
53ddb3c
Compare
53ddb3c to
ea54087
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens the web RDP file-upload (“clipboard file paste”) flow so that an advertised upload can’t wedge future uploads when the remote never pulls, rejects, supersedes, or stalls the paste. It also surfaces the remote FormatListResponse (accept/reject) signal from cliprdr through ironrdp-web into iron-remote-desktop-rdp, and adds unit tests for the new recovery paths.
Changes:
- Add paste-ack + inactivity watchdogs and centralized “fail pending upload” cleanup to reliably clear
uploadStateand resume clipboard monitoring. - Surface
on_format_list_responseas a JS callback /format-list-responseevent and use it to fail early on explicit reject. - Expand
RdpFileTransferProvidertests to cover deferred monitoring resume, watchdog timeouts, remote clipboard replacement, and format-list response behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web-client/iron-remote-desktop-rdp/src/RdpFileTransferProvider.ts | Adds watchdogs + new recovery paths, monitoring suppression deferral, and format-list-response event wiring. |
| web-client/iron-remote-desktop-rdp/src/RdpFileTransferProvider.test.ts | Adds regression tests for the new watchdog behavior and format-list response handling. |
| web-client/iron-remote-desktop-rdp/src/extensions.ts | Adds a builder extension factory for format_list_response_callback. |
| crates/ironrdp-web/src/session.rs | Threads the new callback through SessionBuilder into the WASM clipboard backend callbacks. |
| crates/ironrdp-web/src/clipboard.rs | Adds a backend message + JS callback dispatch for FormatListResponse and implements the on_format_list_response hook. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Upload paste-window watchdog. Armed when we advertise an upload, disarmed on the | ||
| // first FileContentsRequest (the remote pulled the files). Being armed is the single | ||
| // "advertised but not yet pulled" signal: on timeout it resumes monitoring and fails | ||
| // the never-pulled upload, and handleUnlock / handleFormatListResponse consult it to | ||
| // fail a refused paste early. Upload completion/failure run only after that first | ||
| // request, so they need not touch it. |
| /** Suppress clipboard monitoring for the duration of an upload's paste window. */ | ||
| private suppressUploadMonitoring(): void { | ||
| this.onUploadStarted?.(); | ||
| this.uploadMonitoringSuppressed = true; | ||
| } |
| private resumeUploadMonitoring(): void { | ||
| if (!this.uploadMonitoringSuppressed) { | ||
| return; | ||
| } | ||
| this.uploadMonitoringSuppressed = false; | ||
| this.onUploadFinished?.(); | ||
| } |
| } else { | ||
| warn!(ok, "Format list response received but no JS callback registered"); | ||
| } |
Problem
RdpFileTransferProvider.uploadFiles()setsuploadStateand refuses to start another upload whileit is set (
throw "Upload already in progress").uploadStatewas cleared only when the remotepulled the advertised files to completion, or on
dispose(). So any paste that is advertised butdoesn't run to completion wedges every later upload until the page is reloaded. That happens whenever:
overwrites our file FormatList before the paste is pulled, or the remote rejects the FormatList with
CB_RESPONSE_FAILso it never requests the files.makes it the new clipboard owner and supersedes our advertise, so our files are never pulled.
thing that cleared
uploadState) never arrives.Backends also had no way to know whether an advertise was accepted or rejected.
Fix
Release
uploadState(rejectcompletion, emit an uploaderror, resume clipboard monitoring) onevery path where an advertised paste won't run to completion — not just the happy-path completion:
FileContentsRequest. On timeout (60s, matchingCliprdr'slock_inactivity_timeout) it fails thepending upload regardless of how the peer behaves. Covers "advertised but never pulled."
on_format_list_response(explicit reject). Newly surfaced from the cliprdr hook (feat(cliprdr): add CliprdrBackend::on_format_list_response(ok) hook #1300)through
ironrdp-webas aformatListResponseCallback/format-list-responseevent; onCB_RESPONSE_FAILthe provider fails the pending upload at once. The event also lets a frontenddrive paste (inject on accept, retry on reject).
handleFilesAvailable(remote replaced the clipboard with files). When the remote copies itsown files mid-paste the web client auto-requests the list (no delayed rendering) and surfaces it
here — i.e. the remote took clipboard ownership, so our advertise is superseded and we fail the
pending upload. The symmetric counterpart of (2): that recovers when the remote rejects our
advertise, this when it replaces it.
FileContentsRequestonce pulling hasbegun — picking up where the paste-ack watchdog (which only covers the never-pulled case) leaves
off. If requests then stop for 60s the paste stalled — e.g. a text/image copy took the clipboard
(which arrives as a FormatList without
FileGroupDescriptorW, so it never reacheshandleFilesAvailable), or the transfer simply died — and the upload is failed. Aslow-but-progressing transfer keeps resetting it, so it is never killed.
Together (1) and (4) give continuous coverage across the paste's lifetime (before the first pull, then
after it), while (2) and (3) short-circuit the two cases the remote signals explicitly.
Monitoring stays suppressed until the first
FileContentsRequest(not just the wire send), closing thewindow where the clipboard monitor could clobber our file FormatList.
handleUnlockis a deliberate no-op:Unlockis not a reliable failure signal — the peer, and our own cliprdr on the lock-inactivity timeout, emit it routinely (often before anyFileContentsRequest) — so recovery isleft to the watchdogs.
Scope
ironrdp-web:FormatListResponsebackend message +on_format_list_responsewiring; the cliprdrtrait method has a no-op default (feat(cliprdr): add CliprdrBackend::on_format_list_response(ok) hook #1300), so it is non-breaking for other backends.
iron-remote-desktop-rdp(web): provider recovery paths (paste-ack + inactivity watchdogs,handleFilesAvailablerelease, reject handling) and aformatListResponseCallbackfactory. Noiron-remote-desktopchange.Tests
RdpFileTransferProvider.test.ts:FileContentsRequest.succeeds).
pulled upload that keeps making progress.
handleFilesAvailablereleases an in-flight upload when the remote replaces the clipboard.Unlockdoes not fail an in-flight upload (it is not a paste-failure signal).on_format_list_response: extension exposure, event emission, reject-on-fail, and a reject with noupload in progress is a no-op.