Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// The number of times to retry OLE clipboard operations.
/// </summary>
internal const int OleRetryCount = 10;

/// <summary>
/// The amount of time in milliseconds to sleep between retrying OLE clipboard operations.
/// </summary>
internal const int OleRetryDelay = 100;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,13 @@ namespace System.Private.Windows.Ole;
internal static unsafe class ClipboardCore<TOleServices>
where TOleServices : IOleServices
{
/// <summary>
/// The number of times to retry OLE clipboard operations.
/// </summary>
private const int OleRetryCount = 10;

/// <summary>
/// The amount of time in milliseconds to sleep between retrying OLE clipboard operations.
/// </summary>
private const int OleRetryDelay = 100;

/// <summary>
/// Removes all data from the Clipboard.
/// </summary>
/// <returns>An <see cref="HRESULT"/> indicating the success or failure of the operation.</returns>
internal static HRESULT Clear(
int retryTimes = OleRetryCount,
int retryDelay = OleRetryDelay)
int retryTimes = ClipboardConstants.OleRetryCount,
int retryDelay = ClipboardConstants.OleRetryDelay)
{
TOleServices.EnsureThreadState();

Expand All @@ -53,8 +43,8 @@ internal static HRESULT Clear(
/// </summary>
/// <returns>An <see cref="HRESULT"/> indicating the success or failure of the operation.</returns>
internal static HRESULT Flush(
int retryTimes = OleRetryCount,
int retryDelay = OleRetryDelay)
int retryTimes = ClipboardConstants.OleRetryCount,
int retryDelay = ClipboardConstants.OleRetryDelay)
{
TOleServices.EnsureThreadState();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -134,8 +124,8 @@ internal static HRESULT SetData(
internal static HRESULT TryGetData(
out ComScope<IDataObject> proxyDataObject,
out object? originalObject,
int retryTimes = OleRetryCount,
int retryDelay = OleRetryDelay)
int retryTimes = ClipboardConstants.OleRetryCount,
int retryDelay = ClipboardConstants.OleRetryDelay)
{
TOleServices.EnsureThreadState();

Expand Down Expand Up @@ -184,8 +174,8 @@ internal static HRESULT TryGetData(
/// <inheritdoc cref="SetData(IComVisibleDataObject, bool, int, int)"/>
internal static bool IsObjectOnClipboard(
object @object,
int retryTimes = OleRetryCount,
int retryDelay = OleRetryDelay)
int retryTimes = ClipboardConstants.OleRetryCount,
int retryDelay = ClipboardConstants.OleRetryDelay)
{
if (@object is null)
{
Expand All @@ -210,8 +200,8 @@ internal static bool IsObjectOnClipboard(
/// </summary>
internal static HRESULT GetDataObject<TDataObject, TIDataObject>(
out TIDataObject? dataObject,
int retryTimes = OleRetryCount,
int retryDelay = OleRetryDelay)
int retryTimes = ClipboardConstants.OleRetryCount,
int retryDelay = ClipboardConstants.OleRetryDelay)
where TDataObject : class, IDataObjectInternal<TDataObject, TIDataObject>, TIDataObject
where TIDataObject : class
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,12 +359,27 @@ private static bool TryGetHGLOBALData<T>(
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.
Expand All @@ -376,6 +391,7 @@ private static bool TryGetHGLOBALData<T>(
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.)
Expand Down Expand Up @@ -424,9 +440,30 @@ private static bool TryGetIStreamData<T>(
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Executes the given action with retry logic for OLE operations.
/// </summary>
/// <param name="action">Execute the action which returns bool value indicating whether to continue retries or stop.</param>
/// <param name="retryCount">Number of retry attempts.</param>
/// <param name="retryDelay">Delay in milliseconds between retries.</param>
internal static void ExecuteWithRetry(
Func<bool> action,
int retryCount = ClipboardConstants.OleRetryCount,
int retryDelay = ClipboardConstants.OleRetryDelay)
{
int attempts = 0;

while (attempts < retryCount)
{
if (action())
{
attempts++;
Thread.Sleep(retryDelay);

continue;
}

break;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down