diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardConstants.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardConstants.cs new file mode 100644 index 00000000000..81c5c4aea44 --- /dev/null +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardConstants.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Private.Windows.Ole; + +internal static class ClipboardConstants +{ + /// + /// The number of times to retry OLE clipboard operations. + /// + internal const int OleRetryCount = 10; + + /// + /// The amount of time in milliseconds to sleep between retrying OLE clipboard operations. + /// + internal const int OleRetryDelay = 100; +} diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardCore.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardCore.cs index 994bbc07597..96018bcb342 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardCore.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardCore.cs @@ -12,23 +12,13 @@ namespace System.Private.Windows.Ole; internal static unsafe class ClipboardCore where TOleServices : IOleServices { - /// - /// The number of times to retry OLE clipboard operations. - /// - private const int OleRetryCount = 10; - - /// - /// The amount of time in milliseconds to sleep between retrying OLE clipboard operations. - /// - private const int OleRetryDelay = 100; - /// /// Removes all data from the Clipboard. /// /// An indicating the success or failure of the operation. internal static HRESULT Clear( - int retryTimes = OleRetryCount, - int retryDelay = OleRetryDelay) + int retryTimes = ClipboardConstants.OleRetryCount, + int retryDelay = ClipboardConstants.OleRetryDelay) { TOleServices.EnsureThreadState(); @@ -53,8 +43,8 @@ internal static HRESULT Clear( /// /// An indicating the success or failure of the operation. internal static HRESULT Flush( - int retryTimes = OleRetryCount, - int retryDelay = OleRetryDelay) + int retryTimes = ClipboardConstants.OleRetryCount, + int retryDelay = ClipboardConstants.OleRetryDelay) { TOleServices.EnsureThreadState(); @@ -85,8 +75,8 @@ internal static HRESULT Flush( internal static HRESULT SetData( IComVisibleDataObject dataObject, bool copy, - int retryTimes = OleRetryCount, - int retryDelay = OleRetryDelay) + int retryTimes = ClipboardConstants.OleRetryCount, + int retryDelay = ClipboardConstants.OleRetryDelay) { TOleServices.EnsureThreadState(); @@ -134,8 +124,8 @@ internal static HRESULT SetData( internal static HRESULT TryGetData( out ComScope proxyDataObject, out object? originalObject, - int retryTimes = OleRetryCount, - int retryDelay = OleRetryDelay) + int retryTimes = ClipboardConstants.OleRetryCount, + int retryDelay = ClipboardConstants.OleRetryDelay) { TOleServices.EnsureThreadState(); @@ -184,8 +174,8 @@ internal static HRESULT TryGetData( /// internal static bool IsObjectOnClipboard( object @object, - int retryTimes = OleRetryCount, - int retryDelay = OleRetryDelay) + int retryTimes = ClipboardConstants.OleRetryCount, + int retryDelay = ClipboardConstants.OleRetryDelay) { if (@object is null) { @@ -210,8 +200,8 @@ internal static bool IsObjectOnClipboard( /// internal static HRESULT GetDataObject( out TIDataObject? dataObject, - int retryTimes = OleRetryCount, - int retryDelay = OleRetryDelay) + int retryTimes = ClipboardConstants.OleRetryCount, + int retryDelay = ClipboardConstants.OleRetryDelay) where TDataObject : class, IDataObjectInternal, TIDataObject where TIDataObject : class { diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs index 115809900a6..4b1e6d3baba 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Composition.NativeToManagedAdapter.cs @@ -359,12 +359,27 @@ private static bool TryGetHGLOBALData( tymed = (uint)Com.TYMED.TYMED_HGLOBAL }; - if (dataObject->QueryGetData(formatetc).Failed) + HRESULT hr = HRESULT.S_OK; + + Utilities.ExecuteWithRetry(() => + { + hr = dataObject->QueryGetData(formatetc); + return hr == HRESULT.CLIPBRD_E_CANT_OPEN; + }); + + if (hr.Failed) { return false; } - HRESULT hr = dataObject->GetData(formatetc, out Com.STGMEDIUM medium); + Com.STGMEDIUM medium = default; + hr = HRESULT.S_OK; + + Utilities.ExecuteWithRetry(() => + { + hr = dataObject->GetData(formatetc, out medium); + return hr == HRESULT.CLIPBRD_E_CANT_OPEN; + }); // One of the ways this can happen is when we attempt to put binary formatted data onto the // clipboard, which will succeed as Windows ignores all errors when putting data on the clipboard. @@ -376,6 +391,7 @@ private static bool TryGetHGLOBALData( Debug.WriteLineIf(hr == HRESULT.E_UNEXPECTED, "E_UNEXPECTED returned when trying to get clipboard data."); Debug.WriteLineIf(hr == HRESULT.COR_E_SERIALIZATION, "COR_E_SERIALIZATION returned when trying to get clipboard data, for example, BinaryFormatter threw SerializationException."); + Debug.WriteLineIf(hr == HRESULT.CLIPBRD_E_CANT_OPEN, "CLIPBRD_E_CANT_OPEN returned when clipboard was in locked state."); // If GetData failed, don't try to read from the medium - it may contain uninitialized data. // (This can easily happen when the clipboard content changes between QueryGetData and GetData calls.) @@ -424,9 +440,30 @@ private static bool TryGetIStreamData( tymed = (uint)Com.TYMED.TYMED_ISTREAM }; + HRESULT result = HRESULT.S_OK; + + Utilities.ExecuteWithRetry(() => + { + result = dataObject->QueryGetData(formatEtc); + return result == HRESULT.CLIPBRD_E_CANT_OPEN; + }); + + if (result.Failed) + { + return false; + } + + Com.STGMEDIUM medium = default; + result = HRESULT.S_OK; + + Utilities.ExecuteWithRetry(() => + { + result = dataObject->GetData(formatEtc, out medium); + return result == HRESULT.CLIPBRD_E_CANT_OPEN; + }); + // Limit the # of exceptions we may throw below. - if (dataObject->QueryGetData(formatEtc).Failed - || dataObject->GetData(formatEtc, out Com.STGMEDIUM medium).Failed) + if (result.Failed) { return false; } diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Utilities.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Utilities.cs new file mode 100644 index 00000000000..b115e0df8df --- /dev/null +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/Utilities.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Private.Windows.Ole; + +internal static class Utilities +{ + /// + /// Executes the given action with retry logic for OLE operations. + /// + /// Execute the action which returns bool value indicating whether to continue retries or stop. + /// Number of retry attempts. + /// Delay in milliseconds between retries. + internal static void ExecuteWithRetry( + Func action, + int retryCount = ClipboardConstants.OleRetryCount, + int retryDelay = ClipboardConstants.OleRetryDelay) + { + int attempts = 0; + + while (attempts < retryCount) + { + if (action()) + { + attempts++; + Thread.Sleep(retryDelay); + + continue; + } + + break; + } + } +} diff --git a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/UtilitiesTests.cs b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/UtilitiesTests.cs new file mode 100644 index 00000000000..9fc225c3847 --- /dev/null +++ b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/UtilitiesTests.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Private.Windows.Ole; + +public class UtilitiesTests +{ + [Fact] + public void ExecuteWithRetry_ActionReturnsFalse_CallsOnce() + { + int calls = 0; + + Utilities.ExecuteWithRetry(() => + { + calls++; + + return false; + }, + retryCount: 3, + retryDelay: 0); + + Assert.Equal(1, calls); + } + + [Fact] + public void ExecuteWithRetry_ActionReturnsTrueThenFalse_RetriesUntilFalse() + { + int calls = 0; + + Utilities.ExecuteWithRetry(() => + { + calls++; + + return calls < 3; + }, + retryCount: 5, + retryDelay: 0); + + Assert.Equal(3, calls); + } + + [Fact] + public void ExecuteWithRetry_ActionAlwaysReturnsTrue_StopsAtRetryCount() + { + int calls = 0; + + Utilities.ExecuteWithRetry(() => + { + calls++; + + return true; + }, + retryCount: 3, + retryDelay: 0); + + Assert.Equal(3, calls); + } +} diff --git a/src/System.Windows.Forms/System/Windows/Forms/OLE/WinFormsOleServices.cs b/src/System.Windows.Forms/System/Windows/Forms/OLE/WinFormsOleServices.cs index 7bf810195bd..3e091d9558a 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/OLE/WinFormsOleServices.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/OLE/WinFormsOleServices.cs @@ -85,19 +85,34 @@ static unsafe bool TryGetBitmapData(Com.IDataObject* dataObject, [NotNullWhen(tr tymed = (uint)TYMED.TYMED_GDI }; - HRESULT result = dataObject->QueryGetData(formatEtc); + HRESULT result = HRESULT.S_OK; + + Utilities.ExecuteWithRetry(() => + { + result = dataObject->QueryGetData(formatEtc); + return result == HRESULT.CLIPBRD_E_CANT_OPEN; + }); + if (result.Failed) { return false; } - result = dataObject->GetData(formatEtc, out STGMEDIUM medium); + result = HRESULT.S_OK; + STGMEDIUM medium = default; + + Utilities.ExecuteWithRetry(() => + { + result = dataObject->GetData(formatEtc, out medium); + return result == HRESULT.CLIPBRD_E_CANT_OPEN; + }); // One of the ways this can happen is when we attempt to put binary formatted data onto the // clipboard, which will succeed as Windows ignores all errors when putting data on the clipboard. // The data state, however, is not good, and this error will be returned by Windows when asking to // get the data out. Debug.WriteLineIf(result == HRESULT.CLIPBRD_E_BAD_DATA, "CLIPBRD_E_BAD_DATA returned when trying to get clipboard data."); + Debug.WriteLineIf(result == HRESULT.CLIPBRD_E_CANT_OPEN, "CLIPBRD_E_CANT_OPEN returned when clipboard was in locked state."); try {