cool#9045 - close clipboard race by waiting for completion.

To avoid the HTTP[S] request racing the websocket and sometimes
loosing we need to:

* get a notification from the Kit when the copy / cut is complete
* wait on a Promise for this, to allow the HTTP fetch to start
* re-work to do a single, rather than two fetches by sharing
  the download promise.

Change-Id: Ic23f7f817cc855ff08f25a2afefcd73d6fc3472b
Signed-off-by: Michael Meeks <michael.meeks@collabora.com>
pull/9033/head
Michael Meeks 2024-05-13 16:41:24 +01:00
parent 0ef5e740a4
commit ff8dbe7fde
4 changed files with 67 additions and 22 deletions

View File

@ -13,7 +13,7 @@
* local & remote clipboard data.
*/
/* global app _ brandProductName $ ClipboardItem */
/* global app _ brandProductName $ ClipboardItem Promise */
// Get all interesting clipboard related events here, and handle
// download logic in one place ...
@ -38,6 +38,10 @@ L.Clipboard = L.Class.extend({
this._dummyPlainDiv = null;
this._dummyClipboard = {};
// Tracks waiting for UNO commands to complete
this._commandCompletion = [];
this._map.on('commandresult', this._onCommandResult, this);
div.setAttribute('id', this._dummyDivName);
div.setAttribute('style', 'user-select: text !important');
div.style.opacity = '0';
@ -818,6 +822,25 @@ L.Clipboard = L.Class.extend({
this.paste(ev);
},
// Gets status of a copy/paste command from the remote Kit
_onCommandResult: function(e) {
if (e.commandName === '.uno:Copy' || e.commandName === '.uno:Cut')
{
window.app.console.log('Resolve clipboard command promise ' + e.commandName);
const that = this;
while (that._commandCompletion.length > 0)
{
let a = that._commandCompletion.shift();
a.resolve(a.fetch.then(function(text) {
const content = that.parseClipboard(text)[a.shorttype];
const blob = new Blob([content], { 'type': a.mimetype });
console.log('Generate blob of type ' + a.mimetype + ' from ' +a.shorttype + ' text: ' +content);
return blob;
}));
}
}
},
// Executes the navigator.clipboard.write() call, if it's available.
_navigatorClipboardWrite: function() {
if (!L.Browser.hasNavigatorClipboardWrite) {
@ -828,26 +851,44 @@ L.Clipboard = L.Class.extend({
return false;
}
app.socket.sendMessage('uno ' + this._unoCommandForCopyCutPaste);
const url = this.getMetaURL() + '&MimeType=text/html,text/plain;charset=utf-8';
const command = this._unoCommandForCopyCutPaste;
app.socket.sendMessage('uno ' + command);
// This is sent down the websocket URL which can race with the
// web fetch - so first step is to wait for the result of
// that command so we are sure the clipboard is set before
// fetching it.
const that = this;
if (that._commandCompletion.length > 0)
window.app.console.error('Already have ' + that._commandCompletion.length +
' pending clipboard command(s)');
const url = that.getMetaURL() + '&MimeType=text/html,text/plain;charset=utf-8';
// Share a single fetch
var fetchPromise = new Promise((resolve, reject) => {
try {
var result = fetch(url).then(response => response.text());
resolve(result);
} catch (err) {
reject(err);
}
});
var awaitPromise = function(url, mimetype, shorttype) {
return new Promise((resolve, reject) => {
window.app.console.log('New ' + command + ' promise');
// FIXME: add a timeout cleanup too ...
that._commandCompletion.push({ fetch: fetchPromise, command: command,
resolve: resolve, reject: reject,
mimetype: mimetype, shorttype: shorttype});
}); };
const text = new ClipboardItem({
'text/html': fetch(url)
.then(response => response.text())
.then(function(text) {
const type = "text/html";
const content = that.parseClipboard(text)['html'];
const blob = new Blob([content], { 'type': type });
return blob;
}),
'text/plain': fetch(url)
.then(response => response.text())
.then(function(text) {
const type = 'text/plain';
const content = that.parseClipboard(text)['plain'];
const blob = new Blob([content], { 'type': type });
return blob;
}),
'text/html': awaitPromise(url, 'text/html', 'html'),
'text/plain': awaitPromise(url, 'text/plain', 'plain')
});
let clipboard = navigator.clipboard;
if (L.Browser.cypressTest) {

View File

@ -223,8 +223,9 @@ function assertSheetContents(expectedData, copy) {
function assertDataClipboardTable(expectedData) {
cy.log('>> assertDataClipboardTable - start');
cy.cGet('#copy-paste-container table td').should(function($td) {
expect($td).to.have.length(expectedData.length);
cy.cGet('#copy-paste-container table td')
.should('have.length', expectedData.length)
.should(function($td) {
var actualData = $td.map(function(i,el) {
return Cypress.$(el).text();
}).get();

View File

@ -1958,6 +1958,8 @@ bool ChildSession::unoCommand(const StringVector& tokens)
const bool bNotify = (tokens.equals(1, ".uno:Save") ||
tokens.equals(1, ".uno:Undo") ||
tokens.equals(1, ".uno:Redo") ||
tokens.equals(1, ".uno:Cut") ||
tokens.equals(1, ".uno:Copy") ||
tokens.equals(1, ".uno:OpenHyperlink") ||
tokens.startsWith(1, "vnd.sun.star.script:"));

View File

@ -242,9 +242,10 @@ dialog <command>
<command> is unique identifier for the dialog that needs to be painted.
uno <command>
uno <command> [arguments]
<command> is a line of text.
[argments] - JSON encoded arguments for the UNO command
save dontTerminateEdit=<value> dontSaveIfUnmodified=<value>