feat(web): supersede a stuck upload and complete zero-byte files#1381
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the web RdpFileTransferProvider (in iron-remote-desktop-rdp) to make uploads more resilient: starting a new upload now supersedes any in-progress batch, and empty (0-byte) files can complete without ever receiving a RANGE request—preventing the provider’s uploadState from becoming stuck.
Changes:
- Change
uploadFiles()to supersede an existing upload batch instead of throwing, and addisUploadInProgress()as a source-of-truth getter for hosts. - Fix 0-byte uploads by marking files complete on the
FILECONTENTS_SIZEpath, and factor completion/teardown logic into shared helpers. - Update/extend unit tests to cover supersede behavior,
isUploadInProgress(), and 0-byte completion scenarios.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| web-client/iron-remote-desktop-rdp/src/RdpFileTransferProvider.ts | Implements supersede-on-new-upload, adds isUploadInProgress(), fixes 0-byte completion, and refactors teardown/completion logic. |
| web-client/iron-remote-desktop-rdp/src/RdpFileTransferProvider.test.ts | Adds coverage for supersede behavior, upload-in-progress lifecycle, and zero-byte completion (single and mixed batches). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
It may be worth to address Copilot’s comment, but otherwise a sensible patch, LGTM!
3245a70 to
f4e1ea5
Compare
Good catch! I pushed an amended commit with the fix a new test for it as well |
f12bbce
into
Devolutions:master
Problem
#1372 made
RdpFileTransferProviderrecoveruploadStatewhen a paste is interrupted or neverpulled (paste-ack + inactivity watchdogs), so a stuck paste no longer wedges every later upload. But
recovery is only automatic and only after the ~60s watchdog windows. Two gaps remain:
uploadFiles()throwsUpload already in progresswhile
uploadStateis set. So a user who cancels the copy on the remote (no CLIPRDR signal forthat) or hits a stalled paste can't just retry — they wait for a watchdog. There's also no way for
a host to ask whether an upload is in progress, so hosts track a parallel flag that can drift
from
uploadState.onload). A 0-byte file has no bytes, so the remote requests onlyFILECONTENTS_SIZEand never aFILECONTENTS_RANGE— the file is counted inexpectedFileCountbut never added tocompletedFiles, the batch never finishes, anduploadStatesticks (which fix(web): recover file-upload state when a paste is interrupted or never pulled #1372's watchdog thenfails after ~60s). Pasting a folder containing empty files hangs.
Fix
A natural extension of #1372's recovery model — same
uploadStatelifecycle, two more terminalpaths:
uploadStateis already set,uploadFiles()tears the stalebatch down (new
supersedeUpload(): abort in-flight reads, clear fix(web): recover file-upload state when a paste is interrupted or never pulled #1372's watchdogs, resolve theold
completion— no error, since it's a replacement, not a failure) and advertises the new files.A new Format List supersedes the previous offer anyway per [MS-RDPECLIP] 3.1.1.1. The superseding
upload re-arms fix(web): recover file-upload state when a paste is interrupted or never pulled #1372's watchdogs, so all of its recovery still applies.
suppressUploadMonitoringis made idempotent for the old→new handoff. The in-flight-read teardown shared by
supersedeUpload,failPendingUpload, anddisposeis factored into oneabortInFlightReads()helper (was copied inall three).
isUploadInProgress(): boolean— small getter (uploadState !== undefined) so a host can usethe provider as the single source of truth for its own one-upload-at-a-time decision (covering both
host uploads and remote re-pastes), and decide when to supersede.
FILECONTENTS_SIZErequest but never a RANGE, soin the SIZE branch it's marked complete right after replying — mirroring the download path's existing
size === 0handling. The "mark one file complete" step (record it, emitupload-completeonce,finishUploadBatchwhen the batch is fully accounted) is a singlemarkUploadFileComplete()helpershared by the RANGE-onload path and this SIZE path, so the two can't drift. Purely client-side; the
protocol already delivers the SIZE request.
Where #1372 recovers a stuck upload automatically, this lets a new paste replace it immediately —
and stops empty files from getting stuck in the first place.
Scope
iron-remote-desktop-rdp(web) only. Noironrdp-cliprdr/ironrdp-web/ binding changes. Buildson fix(web): recover file-upload state when a paste is interrupted or never pulled #1372. The internal helpers (
markUploadFileComplete,abortInFlightReads) are purerefactors that de-duplicate existing logic — no behavior change from them.
isUploadInProgress(): boolean.uploadFiles()— note for existing callers: it no longer throwsUpload already in progresswhen a batch is already in flight; it supersedes the prior batch andresolves that batch's
completion. A caller that wrappeduploadFiles()in a try/catch for thatspecific error can drop it — the call now always starts the new batch. Callers that want
one-upload-at-a-time semantics should gate on the new
isUploadInProgress()and decide whether torefuse or supersede (this is exactly what the getter enables). No other behavior changes.
Tests
RdpFileTransferProvider.test.ts:isUploadInProgressacross a lifecycle (idle → advertised → complete), released after a stalledfailure, and true again during a re-paste.
completionand advertises a new batch in itsplace.
FILECONTENTS_SIZErequest alone (no RANGE); a 0-byte filefinishes a mixed batch only after the data files are served.
Upload already in progressthrow now assertisUploadInProgress()(the supersede behavior).