Skip to content

Commit 356d06e

Browse files
authored
fix(web): recover file-upload state when a paste is interrupted or never pulled (#1372)
1 parent c36032f commit 356d06e

5 files changed

Lines changed: 620 additions & 32 deletions

File tree

crates/ironrdp-web/src/clipboard.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@ pub(crate) enum WasmClipboardBackendMessage {
173173
LocksExpired {
174174
clip_data_ids: Vec<u32>,
175175
},
176+
/// [MS-RDPECLIP] 2.2.3.2 Remote's response to one of our outbound Format Lists.
177+
///
178+
/// `ok` is `true` when the remote accepted the advertised formats
179+
/// (`CB_RESPONSE_OK`) and `false` when it rejected them (`CB_RESPONSE_FAIL`).
180+
/// A rejected advertise is silently discarded by the remote, so a file paste
181+
/// cannot proceed; backends use this to detect and recover from a refused
182+
/// paste instead of stalling until a lock timeout.
183+
FormatListResponse {
184+
ok: bool,
185+
},
176186

177187
// JS-initiated file transfer operations
178188
/// JS requests file contents from remote (download).
@@ -235,6 +245,7 @@ pub(crate) struct JsClipboardCallbacks {
235245
pub(crate) on_lock: Option<js_sys::Function>,
236246
pub(crate) on_unlock: Option<js_sys::Function>,
237247
pub(crate) on_locks_expired: Option<js_sys::Function>,
248+
pub(crate) on_format_list_response: Option<js_sys::Function>,
238249
}
239250

240251
impl WasmClipboard {
@@ -841,6 +852,16 @@ impl WasmClipboard {
841852
);
842853
}
843854
}
855+
WasmClipboardBackendMessage::FormatListResponse { ok } => {
856+
if let Some(callback) = self.js_callbacks.on_format_list_response.as_ref() {
857+
if let Err(e) = callback.call1(&JsValue::NULL, &JsValue::from_bool(ok)) {
858+
error!(error = ?e, ok, "Failed to call JS format list response callback");
859+
return Ok(());
860+
}
861+
} else {
862+
trace!(ok, "Format list response received but no JS callback registered");
863+
}
864+
}
844865
// The following variants are handled directly in the event loop and should never reach here
845866
WasmClipboardBackendMessage::FileContentsRequestSend { .. }
846867
| WasmClipboardBackendMessage::FileContentsResponseSend { .. }
@@ -948,6 +969,10 @@ impl CliprdrBackend for WasmClipboardBackend {
948969
self.send_event(WasmClipboardBackendMessage::Unlock { data_id });
949970
}
950971

972+
fn on_format_list_response(&mut self, ok: bool) {
973+
self.send_event(WasmClipboardBackendMessage::FormatListResponse { ok });
974+
}
975+
951976
fn on_remote_file_list(&mut self, files: &[ironrdp::cliprdr::pdu::FileDescriptor], clip_data_id: Option<u32>) {
952977
let file_metadata: Vec<FileMetadata> = files.iter().map(FileMetadata::from_file_descriptor).collect();
953978

@@ -1227,6 +1252,7 @@ mod tests {
12271252
on_lock: Some(js_sys::Function::new_no_args("")),
12281253
on_unlock: Some(js_sys::Function::new_no_args("")),
12291254
on_locks_expired: Some(js_sys::Function::new_no_args("")),
1255+
on_format_list_response: Some(js_sys::Function::new_no_args("")),
12301256
}
12311257
}
12321258

crates/ironrdp-web/src/session.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ struct SessionBuilderInner {
7979
lock_callback: Option<js_sys::Function>,
8080
unlock_callback: Option<js_sys::Function>,
8181
locks_expired_callback: Option<js_sys::Function>,
82+
format_list_response_callback: Option<js_sys::Function>,
8283

8384
// Setting printer stream callbacks activates the virtual printer.
8485
invalid_print_job_stream_callbacks: bool,
@@ -120,6 +121,7 @@ impl Default for SessionBuilderInner {
120121
lock_callback: None,
121122
unlock_callback: None,
122123
locks_expired_callback: None,
124+
format_list_response_callback: None,
123125

124126
invalid_print_job_stream_callbacks: false,
125127
print_job_stream_callbacks: None,
@@ -276,6 +278,9 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
276278
|locks_expired_callback: JsValue| {
277279
self.0.borrow_mut().locks_expired_callback = locks_expired_callback.dyn_into::<js_sys::Function>().ok();
278280
};
281+
|format_list_response_callback: JsValue| {
282+
self.0.borrow_mut().format_list_response_callback = format_list_response_callback.dyn_into::<js_sys::Function>().ok();
283+
};
279284
|print_job_stream_callbacks: JsValue| {
280285
let mut inner = self.0.borrow_mut();
281286
match parse_print_job_stream_callbacks(print_job_stream_callbacks) {
@@ -341,6 +346,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
341346
lock_callback,
342347
unlock_callback,
343348
locks_expired_callback,
349+
format_list_response_callback,
344350
invalid_print_job_stream_callbacks,
345351
print_job_stream_callbacks,
346352
printer_name,
@@ -381,6 +387,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
381387
lock_callback = inner.lock_callback.clone();
382388
unlock_callback = inner.unlock_callback.clone();
383389
locks_expired_callback = inner.locks_expired_callback.clone();
390+
format_list_response_callback = inner.format_list_response_callback.clone();
384391
invalid_print_job_stream_callbacks = inner.invalid_print_job_stream_callbacks;
385392
print_job_stream_callbacks = inner.print_job_stream_callbacks.clone();
386393
printer_name = inner.printer_name.clone();
@@ -410,6 +417,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
410417
on_lock: lock_callback,
411418
on_unlock: unlock_callback,
412419
on_locks_expired: locks_expired_callback,
420+
on_format_list_response: format_list_response_callback,
413421
},
414422
)
415423
});

0 commit comments

Comments
 (0)