Files
LookingGlass/idd/LGIddHelper/CClipboardManager.cpp
Geoffrey McRae 7ca94e8959 [idd] helper: expose PNG clipboard images as DIB
Advertise PNG offers as delayed CF_DIBV5 data while retaining the
registered PNG representation. Decode requests with the native WIC
PNG decoder so applications that only accept DIB formats can paste
the image.
2026-08-21 13:40:34 +10:00

4829 lines
143 KiB
C++

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "CClipboardManager.h"
#include <CDebug.h>
#include <bcrypt.h>
#include <Ole2.h>
#include <ShlObj.h>
#include <wincodec.h>
#include <algorithm>
#include <condition_variable>
#include <limits>
#include <new>
#include <stdexcept>
#include <string.h>
#include <strsafe.h>
#include <utility>
namespace
{
static constexpr size_t MEMORY_SPOOL_LIMIT = 1024U * 1024U;
static constexpr size_t TEXT_CONVERSION_CHUNK = 16384;
static constexpr uint64_t MAX_SPOOL_BYTES =
UINT64_C(512) * 1024U * 1024U;
static constexpr DWORD CHANNEL_RETRY_MS = 10;
static constexpr DWORD LOCAL_RETRY_MS = 50;
static constexpr DWORD LOCAL_RETRY_TIMEOUT_MS = 5000;
static constexpr DWORD REMOTE_RETRY_MS = 250;
static constexpr DWORD REMOTE_RETRY_TIMEOUT_MS = 5000;
static constexpr UINT_PTR LOCAL_RETRY_TIMER = 0x4c48;
static constexpr UINT_PTR REMOTE_RETRY_TIMER = 0x4c47;
static constexpr uint32_t ORIGIN_MAGIC = 0x4c47434fU;
// BI_ALPHABITFIELDS is a serialized DIB value, but desktop SDKs omit it.
static constexpr DWORD DIB_ALPHA_BITFIELDS = 6U;
struct ClipboardRandomSeeds
{
uint64_t generation;
uint64_t transfer;
};
bool ClipboardSeeds(ClipboardRandomSeeds& seeds)
{
const NTSTATUS status = BCryptGenRandom(nullptr,
reinterpret_cast<PUCHAR>(&seeds), static_cast<ULONG>(sizeof(seeds)),
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (status < 0)
return false;
if (!seeds.generation)
seeds.generation = UINT64_C(1);
seeds.transfer &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER;
if (!seeds.transfer)
seeds.transfer = UINT64_C(1);
seeds.transfer |= KVMFR_CLIPBOARD_TRANSFER_HELPER;
return true;
}
struct ComScope
{
bool initialized;
~ComScope() { if (initialized) CoUninitialize(); }
};
struct ClipboardStorageScope
{
STGMEDIUM medium = {};
bool acquired = false;
~ClipboardStorageScope()
{ if (acquired) ReleaseStgMedium(&medium); }
};
template<typename T>
struct ClipboardComScope
{
T * object = nullptr;
~ClipboardComScope() { if (object) object->Release(); }
};
struct ClipboardTaskStringScope
{
PWSTR value = nullptr;
~ClipboardTaskStringScope() { CoTaskMemFree(value); }
};
enum ClipboardFileCandidate : uint32_t
{
CLIPBOARD_FILE_CANDIDATE_NONE = 0,
CLIPBOARD_FILE_CANDIDATE_HDROP = 1U << 0,
CLIPBOARD_FILE_CANDIDATE_SHELL = 1U << 1,
};
uint32_t ClipboardDataObjectFileCandidates(IDataObject * object,
DWORD sequence, HRESULT& enumError)
{
enumError = E_INVALIDARG;
if (!object)
{
DEBUG_ERROR_HR(E_POINTER,
"Failed to inspect local clipboard file formats: "
"stage=IDataObject sequence=%lu",
static_cast<unsigned long>(sequence));
return CLIPBOARD_FILE_CANDIDATE_NONE;
}
ClipboardComScope<IEnumFORMATETC> formats;
enumError = object->EnumFormatEtc(DATADIR_GET, &formats.object);
if (FAILED(enumError) || !formats.object)
{
if (SUCCEEDED(enumError))
enumError = E_UNEXPECTED;
DEBUG_ERROR_HR(enumError,
"Failed to inspect local clipboard file formats: "
"stage=IDataObject::EnumFormatEtc sequence=%lu",
static_cast<unsigned long>(sequence));
return CLIPBOARD_FILE_CANDIDATE_NONE;
}
HRESULT registrationError = S_OK;
const UINT shellIDList = RegisterClipboardFormatW(CFSTR_SHELLIDLIST);
if (!shellIDList)
{
const DWORD formatError = GetLastError();
registrationError = formatError ? HRESULT_FROM_WIN32(formatError) :
E_UNEXPECTED;
DEBUG_ERROR_HR(registrationError,
"Failed to inspect local clipboard file formats: "
"stage=RegisterClipboardFormatW(CFSTR_SHELLIDLIST) sequence=%lu",
static_cast<unsigned long>(sequence));
}
const UINT fileDescriptor =
RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW);
if (!fileDescriptor)
{
const DWORD formatError = GetLastError();
const HRESULT formatHRESULT = formatError ?
HRESULT_FROM_WIN32(formatError) : E_UNEXPECTED;
if (SUCCEEDED(registrationError))
registrationError = formatHRESULT;
DEBUG_ERROR_HR(formatHRESULT,
"Failed to inspect local clipboard file formats: "
"stage=RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW) sequence=%lu",
static_cast<unsigned long>(sequence));
}
uint32_t candidates = CLIPBOARD_FILE_CANDIDATE_NONE;
for (;;)
{
FORMATETC format = {};
ULONG fetched = 0;
enumError = formats.object->Next(1, &format, &fetched);
if (FAILED(enumError))
{
DEBUG_ERROR_HR(enumError,
"Failed to inspect local clipboard file formats: "
"stage=IEnumFORMATETC::Next sequence=%lu",
static_cast<unsigned long>(sequence));
CoTaskMemFree(format.ptd);
return candidates;
}
if (fetched == 1U)
{
if (format.cfFormat == CF_HDROP)
candidates |= CLIPBOARD_FILE_CANDIDATE_HDROP;
if ((shellIDList && format.cfFormat == shellIDList) ||
(fileDescriptor && format.cfFormat == fileDescriptor))
candidates |= CLIPBOARD_FILE_CANDIDATE_SHELL;
}
CoTaskMemFree(format.ptd);
if (enumError == S_FALSE)
{
enumError = registrationError;
return candidates;
}
if (!fetched)
{
enumError = E_UNEXPECTED;
DEBUG_ERROR_HR(enumError,
"Failed to inspect local clipboard file formats: "
"stage=IEnumFORMATETC::Next sequence=%lu fetched=0",
static_cast<unsigned long>(sequence));
return candidates;
}
}
}
int CountClipboardFormatsLogged(const char * stage, DWORD sequence)
{
SetLastError(ERROR_SUCCESS);
const int count = CountClipboardFormats();
if (count)
return count;
const DWORD error = GetLastError();
if (error)
DEBUG_ERROR_HR(HRESULT_FROM_WIN32(error),
"Failed to count clipboard formats: stage=%s sequence=%lu",
stage, static_cast<unsigned long>(sequence));
return 0;
}
void FreeClipboardDrop(HGLOBAL memory, DWORD sequence)
{
SetLastError(ERROR_SUCCESS);
const HGLOBAL result = GlobalFree(memory);
if (!result)
return;
const DWORD error = GetLastError();
DEBUG_ERROR_HR(error ? HRESULT_FROM_WIN32(error) : E_UNEXPECTED,
"Failed to capture local clipboard files: "
"stage=GlobalFree(DROPFILES) sequence=%lu",
static_cast<unsigned long>(sequence));
}
HRESULT ClipboardFileHRESULT(KVMFRClipboardFileError error)
{
switch (error)
{
case KVMFR_CLIPBOARD_FILE_ERROR_NONE:
return S_OK;
case KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND:
return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
case KVMFR_CLIPBOARD_FILE_ERROR_ACCESS:
return E_ACCESSDENIED;
case KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY:
case KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY:
return HRESULT_FROM_WIN32(ERROR_DIRECTORY);
case KVMFR_CLIPBOARD_FILE_ERROR_IO:
return HRESULT_FROM_WIN32(ERROR_READ_FAULT);
case KVMFR_CLIPBOARD_FILE_ERROR_INVALID:
return HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
case KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY:
return E_OUTOFMEMORY;
case KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE:
return HRESULT_FROM_WIN32(ERROR_DISK_FULL);
case KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED:
return HRESULT_FROM_WIN32(ERROR_DEVICE_NOT_CONNECTED);
case KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED:
return HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED);
case KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED:
return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
case KVMFR_CLIPBOARD_FILE_ERROR_STALE:
return HRESULT_FROM_WIN32(ERROR_RETRY);
default:
return E_UNEXPECTED;
}
}
struct ClipboardOrigin
{
uint32_t magic;
uint32_t reserved;
uint64_t epoch;
uint64_t generation;
};
bool WriteAll(HANDLE file, const void * data, size_t size)
{
const uint8_t * current = static_cast<const uint8_t *>(data);
while (size)
{
const DWORD chunk = static_cast<DWORD>((std::min<size_t>)(
size, (std::numeric_limits<DWORD>::max)()));
DWORD written = 0;
if (!WriteFile(file, current, chunk, &written, nullptr))
return false;
if (written != chunk)
{
SetLastError(ERROR_WRITE_FAULT);
return false;
}
current += written;
size -= written;
}
return true;
}
bool ReadAll(HANDLE file, void * data, size_t size)
{
uint8_t * current = static_cast<uint8_t *>(data);
while (size)
{
const DWORD chunk = static_cast<DWORD>((std::min<size_t>)(
size, (std::numeric_limits<DWORD>::max)()));
DWORD read = 0;
if (!ReadFile(file, current, chunk, &read, nullptr))
return false;
if (read != chunk)
{
SetLastError(ERROR_HANDLE_EOF);
return false;
}
current += read;
size -= read;
}
return true;
}
void AppendUTF8(std::vector<uint8_t>& output, uint32_t codepoint)
{
if (codepoint <= 0x7f)
output.push_back(static_cast<uint8_t>(codepoint));
else if (codepoint <= 0x7ff)
{
output.push_back(static_cast<uint8_t>(0xc0 | (codepoint >> 6)));
output.push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3f)));
}
else if (codepoint <= 0xffff)
{
output.push_back(static_cast<uint8_t>(0xe0 | (codepoint >> 12)));
output.push_back(static_cast<uint8_t>(0x80 |
((codepoint >> 6) & 0x3f)));
output.push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3f)));
}
else
{
output.push_back(static_cast<uint8_t>(0xf0 | (codepoint >> 18)));
output.push_back(static_cast<uint8_t>(0x80 |
((codepoint >> 12) & 0x3f)));
output.push_back(static_cast<uint8_t>(0x80 |
((codepoint >> 6) & 0x3f)));
output.push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3f)));
}
}
uint32_t DIBPixelOffset(const uint8_t * data, size_t size)
{
if (size < sizeof(DWORD))
return 0;
const DWORD headerSize = *reinterpret_cast<const DWORD *>(data);
if (headerSize < sizeof(BITMAPCOREHEADER) || headerSize > size)
return 0;
uint64_t offset = headerSize;
if (headerSize == sizeof(BITMAPCOREHEADER))
{
const BITMAPCOREHEADER * header =
reinterpret_cast<const BITMAPCOREHEADER *>(data);
if (header->bcBitCount <= 8)
offset += (1ULL << header->bcBitCount) * sizeof(RGBTRIPLE);
}
else if (headerSize >= sizeof(BITMAPINFOHEADER))
{
const BITMAPINFOHEADER * header =
reinterpret_cast<const BITMAPINFOHEADER *>(data);
if (headerSize == sizeof(BITMAPINFOHEADER) &&
(header->biCompression == BI_BITFIELDS ||
header->biCompression == DIB_ALPHA_BITFIELDS))
offset += header->biCompression == DIB_ALPHA_BITFIELDS ? 16U : 12U;
const uint32_t colors = header->biClrUsed ? header->biClrUsed :
(header->biBitCount <= 8 ? 1U << header->biBitCount : 0U);
offset += static_cast<uint64_t>(colors) * sizeof(RGBQUAD);
}
else
return 0;
return offset <= size &&
offset <= (std::numeric_limits<uint32_t>::max)() ?
static_cast<uint32_t>(offset) : 0;
}
}
struct CClipboardManager::MarshaledDataObject
{
std::mutex lock;
IStream * stream = nullptr;
~MarshaledDataObject()
{
std::lock_guard<std::mutex> guard(lock);
if (stream)
{
CoReleaseMarshalData(stream);
stream->Release();
}
}
IStream * Take()
{
std::lock_guard<std::mutex> guard(lock);
IStream * result = stream;
stream = nullptr;
return result;
}
};
class CClipboardManager::RemoteFileProvider final :
public IRemoteClipboardFileProvider
{
private:
std::mutex m_lock;
std::condition_variable m_idle;
CClipboardManager * m_manager;
size_t m_active = 0;
bool m_accepting = true;
public:
explicit RemoteFileProvider(CClipboardManager * manager) :
m_manager(manager)
{
}
void StopAccepting()
{
std::lock_guard<std::mutex> lock(m_lock);
m_accepting = false;
}
void Detach()
{
std::unique_lock<std::mutex> lock(m_lock);
m_accepting = false;
m_idle.wait(lock, [this]() { return !m_active; });
m_manager = nullptr;
}
HRESULT ReadClipboardFile(uint64_t dataset, uint64_t acquisition,
uint64_t node, uint64_t offset, void * data, ULONG length,
ULONG& read) override
{
CClipboardManager * manager = nullptr;
{
std::lock_guard<std::mutex> lock(m_lock);
if (!m_manager || !m_accepting)
return STG_E_READFAULT;
manager = m_manager;
++m_active;
}
HRESULT result = STG_E_READFAULT;
try
{
result = manager->ReadRemoteFile(dataset, acquisition,
node, offset, data, length, read);
}
catch (const std::bad_alloc&)
{
result = E_OUTOFMEMORY;
}
catch (...)
{
result = STG_E_READFAULT;
}
{
std::lock_guard<std::mutex> lock(m_lock);
if (!--m_active)
m_idle.notify_all();
}
return result;
}
void ReleaseClipboardFileDataset(uint64_t dataset,
uint64_t acquisition) override
{
CClipboardManager * manager = nullptr;
{
std::lock_guard<std::mutex> lock(m_lock);
if (!m_manager || !m_accepting)
return;
manager = m_manager;
++m_active;
}
try
{
manager->ReleaseRemoteFileDataset(dataset, acquisition);
}
catch (...)
{
}
{
std::lock_guard<std::mutex> lock(m_lock);
if (!--m_active)
m_idle.notify_all();
}
}
};
class CClipboardSpool
{
private:
std::vector<uint8_t> m_memory;
HANDLE m_file = INVALID_HANDLE_VALUE;
uint64_t m_size = 0;
bool Spill()
{
WCHAR path[MAX_PATH];
const DWORD length = GetTempPathW(ARRAYSIZE(path), path);
if (!length)
return false;
if (length >= ARRAYSIZE(path))
{
SetLastError(ERROR_INSUFFICIENT_BUFFER);
return false;
}
for (unsigned int attempt = 0;
attempt != 32 && m_file == INVALID_HANDLE_VALUE; ++attempt)
{
WCHAR name[MAX_PATH];
const uint64_t nonce = GetTickCount64() ^
(static_cast<uint64_t>(GetCurrentProcessId()) << 32) ^ attempt;
if (FAILED(StringCchPrintfW(name, ARRAYSIZE(name),
L"%sLookingGlassClipboard-%08x-%016llx.tmp", path,
GetCurrentProcessId(), nonce)))
{
SetLastError(ERROR_INSUFFICIENT_BUFFER);
return false;
}
m_file = CreateFileW(name, GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE |
FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
}
if (m_file == INVALID_HANDLE_VALUE)
return false;
if (!m_memory.empty() && !WriteAll(m_file,
m_memory.data(), m_memory.size()))
{
const DWORD error = GetLastError();
CloseHandle(m_file);
m_file = INVALID_HANDLE_VALUE;
SetLastError(error ? error : ERROR_WRITE_FAULT);
return false;
}
std::vector<uint8_t>().swap(m_memory);
return true;
}
public:
~CClipboardSpool()
{
if (m_file != INVALID_HANDLE_VALUE)
CloseHandle(m_file);
}
bool Append(const void * data, size_t size)
{
if (!size)
return true;
if (size > MAX_SPOOL_BYTES || m_size > MAX_SPOOL_BYTES - size)
{
SetLastError(ERROR_FILE_TOO_LARGE);
return false;
}
const uint64_t nextSize = m_size + size;
if (m_file == INVALID_HANDLE_VALUE && nextSize <= MEMORY_SPOOL_LIMIT)
{
try
{
const uint8_t * bytes = static_cast<const uint8_t *>(data);
m_memory.insert(m_memory.end(), bytes, bytes + size);
}
catch (const std::bad_alloc&)
{
SetLastError(ERROR_OUTOFMEMORY);
return false;
}
m_size = nextSize;
return true;
}
if (m_file == INVALID_HANDLE_VALUE && !Spill())
return false;
LARGE_INTEGER position = {};
position.QuadPart = m_size;
if (!SetFilePointerEx(m_file, position, nullptr, FILE_BEGIN) ||
!WriteAll(m_file, data, size))
return false;
m_size = nextSize;
return true;
}
bool Read(uint64_t offset, void * data, size_t size)
{
if (offset > m_size || size > m_size - offset)
{
SetLastError(ERROR_INVALID_DATA);
return false;
}
if (!size)
return true;
if (m_file == INVALID_HANDLE_VALUE)
{
memcpy(data, m_memory.data() + static_cast<size_t>(offset), size);
return true;
}
LARGE_INTEGER position = {};
position.QuadPart = offset;
return SetFilePointerEx(m_file, position, nullptr, FILE_BEGIN) &&
ReadAll(m_file, data, size);
}
uint64_t Size() const { return m_size; }
};
namespace
{
template<typename Callback>
bool ForEachUTF8(CClipboardSpool& spool, Callback callback)
{
uint32_t codepoint = 0;
uint32_t minimum = 0;
unsigned int remaining = 0;
std::vector<uint8_t> buffer;
try
{
buffer.resize(KVMFR_CLIPBOARD_REPRESENTATION_BYTES);
}
catch (const std::bad_alloc&)
{
SetLastError(ERROR_OUTOFMEMORY);
return false;
}
for (uint64_t offset = 0; offset < spool.Size();)
{
const size_t length = static_cast<size_t>((std::min<uint64_t>)(
buffer.size(), spool.Size() - offset));
if (!spool.Read(offset, buffer.data(), length))
return false;
offset += length;
for (size_t index = 0; index < length; ++index)
{
uint8_t value = buffer[index];
if (!remaining)
{
if (value < 0x80)
{
if (!callback(value))
return false;
}
else if (value >= 0xc2 && value <= 0xdf)
{
codepoint = value & 0x1f;
minimum = 0x80;
remaining = 1;
}
else if (value >= 0xe0 && value <= 0xef)
{
codepoint = value & 0x0f;
minimum = 0x800;
remaining = 2;
}
else if (value >= 0xf0 && value <= 0xf4)
{
codepoint = value & 0x07;
minimum = 0x10000;
remaining = 3;
}
else if (!callback(0xfffd))
return false;
continue;
}
if ((value & 0xc0) != 0x80)
{
if (!callback(0xfffd))
return false;
codepoint = 0;
minimum = 0;
remaining = 0;
--index;
continue;
}
codepoint = (codepoint << 6) | (value & 0x3f);
if (--remaining)
continue;
if (codepoint < minimum || codepoint > 0x10ffff ||
(codepoint >= 0xd800 && codepoint <= 0xdfff))
codepoint = 0xfffd;
if (!callback(codepoint))
return false;
codepoint = 0;
minimum = 0;
}
}
return !remaining || callback(0xfffd);
}
HGLOBAL UnicodeFromUTF8(CClipboardSpool& spool)
{
uint64_t units = 0;
uint32_t previous = 0;
SetLastError(ERROR_SUCCESS);
if (!ForEachUTF8(spool, [&units, &previous](uint32_t codepoint) {
if (codepoint == '\n' && previous != '\r')
++units;
units += codepoint > 0xffff ? 2U : 1U;
previous = codepoint;
return units < (std::numeric_limits<SIZE_T>::max)() /
sizeof(wchar_t);
}))
{
if (!GetLastError())
SetLastError(ERROR_FILE_TOO_LARGE);
return nullptr;
}
++units;
if (units > (std::numeric_limits<SIZE_T>::max)() / sizeof(wchar_t))
{
SetLastError(ERROR_FILE_TOO_LARGE);
return nullptr;
}
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE,
static_cast<SIZE_T>(units * sizeof(wchar_t)));
if (!memory)
return nullptr;
wchar_t * output = static_cast<wchar_t *>(GlobalLock(memory));
if (!output)
{
GlobalFree(memory);
return nullptr;
}
uint64_t index = 0;
previous = 0;
const bool valid = ForEachUTF8(spool,
[&output, &index, &previous](uint32_t codepoint) {
const uint32_t original = codepoint;
if (codepoint == '\n' && previous != '\r')
output[index++] = L'\r';
if (codepoint > 0xffff)
{
codepoint -= 0x10000;
output[index++] = static_cast<wchar_t>(0xd800 +
(codepoint >> 10));
output[index++] = static_cast<wchar_t>(0xdc00 +
(codepoint & 0x3ff));
}
else
output[index++] = static_cast<wchar_t>(codepoint);
previous = original;
return true;
});
if (valid)
output[index] = L'\0';
GlobalUnlock(memory);
if (!valid)
{
const DWORD error = GetLastError();
GlobalFree(memory);
SetLastError(error ? error : ERROR_INVALID_DATA);
return nullptr;
}
return memory;
}
HGLOBAL CopySpoolToGlobal(CClipboardSpool& spool, uint64_t offset)
{
if (offset > spool.Size())
{
SetLastError(ERROR_INVALID_DATA);
return nullptr;
}
const uint64_t length = spool.Size() - offset;
if (length > (std::numeric_limits<SIZE_T>::max)())
{
SetLastError(ERROR_FILE_TOO_LARGE);
return nullptr;
}
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE,
static_cast<SIZE_T>(length));
if (!memory)
return nullptr;
void * output = GlobalLock(memory);
if (!output || !spool.Read(offset, output, static_cast<size_t>(length)))
{
const DWORD error = GetLastError();
if (output)
GlobalUnlock(memory);
GlobalFree(memory);
SetLastError(error ? error : ERROR_READ_FAULT);
return nullptr;
}
GlobalUnlock(memory);
return memory;
}
HGLOBAL PNGToDIBV5(CClipboardSpool& spool)
{
if (!spool.Size())
{
SetLastError(ERROR_INVALID_DATA);
return nullptr;
}
HGLOBAL source = CopySpoolToGlobal(spool, 0);
if (!source)
return nullptr;
ClipboardComScope<IStream> stream;
HRESULT result = CreateStreamOnHGlobal(source, TRUE, &stream.object);
if (FAILED(result))
{
GlobalFree(source);
SetLastError(result == E_OUTOFMEMORY ? ERROR_OUTOFMEMORY :
ERROR_INVALID_DATA);
return nullptr;
}
ClipboardComScope<IWICImagingFactory> factory;
ClipboardComScope<IWICBitmapDecoder> decoder;
ClipboardComScope<IWICBitmapFrameDecode> frame;
ClipboardComScope<IWICFormatConverter> converter;
result = CoCreateInstance(CLSID_WICImagingFactory, nullptr,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory.object));
if (SUCCEEDED(result))
result = CoCreateInstance(CLSID_WICPngDecoder, nullptr,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&decoder.object));
if (SUCCEEDED(result))
result = decoder.object->Initialize(stream.object,
WICDecodeMetadataCacheOnLoad);
if (SUCCEEDED(result))
result = decoder.object->GetFrame(0, &frame.object);
if (SUCCEEDED(result))
result = factory.object->CreateFormatConverter(&converter.object);
if (SUCCEEDED(result))
result = converter.object->Initialize(frame.object,
GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, nullptr,
0.0, WICBitmapPaletteTypeCustom);
if (FAILED(result))
{
SetLastError(result == E_OUTOFMEMORY ? ERROR_OUTOFMEMORY :
ERROR_INVALID_DATA);
return nullptr;
}
UINT width = 0;
UINT height = 0;
result = converter.object->GetSize(&width, &height);
const UINT maxDimension = static_cast<UINT>(
(std::numeric_limits<LONG>::max)());
if (FAILED(result) || !width || !height || width > maxDimension ||
height > maxDimension || width > UINT_MAX / 4U)
{
SetLastError(result == E_OUTOFMEMORY ? ERROR_OUTOFMEMORY :
ERROR_INVALID_DATA);
return nullptr;
}
const UINT stride = width * 4U;
if (height > UINT64_MAX / stride)
{
SetLastError(ERROR_FILE_TOO_LARGE);
return nullptr;
}
const uint64_t pixelBytes = static_cast<uint64_t>(stride) * height;
if (pixelBytes > UINT_MAX ||
pixelBytes > MAX_SPOOL_BYTES - sizeof(BITMAPV5HEADER) ||
pixelBytes > (std::numeric_limits<SIZE_T>::max)() -
sizeof(BITMAPV5HEADER))
{
SetLastError(ERROR_FILE_TOO_LARGE);
return nullptr;
}
const SIZE_T outputBytes = sizeof(BITMAPV5HEADER) +
static_cast<SIZE_T>(pixelBytes);
HGLOBAL output = GlobalAlloc(GMEM_MOVEABLE, outputBytes);
if (!output)
return nullptr;
uint8_t * data = static_cast<uint8_t *>(GlobalLock(output));
if (!data)
{
GlobalFree(output);
return nullptr;
}
BITMAPV5HEADER header = {};
header.bV5Size = static_cast<DWORD>(sizeof(header));
header.bV5Width = static_cast<LONG>(width);
header.bV5Height = -static_cast<LONG>(height);
header.bV5Planes = 1;
header.bV5BitCount = 32;
header.bV5Compression = BI_BITFIELDS;
header.bV5SizeImage = static_cast<DWORD>(pixelBytes);
header.bV5RedMask = 0x00ff0000U;
header.bV5GreenMask = 0x0000ff00U;
header.bV5BlueMask = 0x000000ffU;
header.bV5AlphaMask = 0xff000000U;
header.bV5CSType = LCS_sRGB;
header.bV5Intent = LCS_GM_IMAGES;
memcpy(data, &header, sizeof(header));
result = converter.object->CopyPixels(nullptr, stride,
static_cast<UINT>(pixelBytes), data + sizeof(header));
GlobalUnlock(output);
if (FAILED(result))
{
GlobalFree(output);
SetLastError(result == E_OUTOFMEMORY ? ERROR_OUTOFMEMORY :
ERROR_INVALID_DATA);
return nullptr;
}
return output;
}
}
CClipboardManager::IncomingTransfer::~IncomingTransfer()
{
if (event)
CloseHandle(event);
}
CClipboardManager::IncomingFileRequest::~IncomingFileRequest()
{
if (event)
CloseHandle(event);
}
CClipboardManager::CClipboardManager(HWND hwnd,
CClipboardChannel& channel) : m_hwnd(hwnd), m_channel(channel)
{
}
CClipboardManager::~CClipboardManager()
{
Shutdown();
}
bool CClipboardManager::Initialize()
{
ClipboardRandomSeeds seeds = {};
if (!ClipboardSeeds(seeds))
{
DEBUG_ERROR("Failed to initialize clipboard file identifiers");
return false;
}
m_localGeneration = seeds.generation;
Atomic::Store(m_nextTransfer, seeds.transfer,
std::memory_order_relaxed);
const HRESULT ole = OleInitialize(nullptr);
if (FAILED(ole))
{
DEBUG_ERROR_HR(ole, "Failed to initialize OLE for clipboard files");
return false;
}
m_oleInitialized = true;
m_formatPNG = RegisterClipboardFormatW(L"PNG");
m_formatJPEG = RegisterClipboardFormatW(L"JFIF");
m_formatOrigin = RegisterClipboardFormatW(L"LookingGlassClipboardOrigin");
if (!m_formatPNG || !m_formatJPEG || !m_formatOrigin)
{
DEBUG_ERROR_HR(GetLastError(), "Failed to register clipboard formats");
OleUninitialize();
m_oleInitialized = false;
return false;
}
Atomic::Store(m_shutdown, false);
try
{
m_remoteFileProvider = std::make_shared<RemoteFileProvider>(this);
}
catch (const std::bad_alloc&)
{
DEBUG_ERROR("Failed to create the clipboard file provider");
Shutdown();
return false;
}
m_stop = CreateEventW(nullptr, TRUE, FALSE, nullptr);
m_wake = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!m_stop || !m_wake)
{
DEBUG_ERROR_HR(GetLastError(), "Failed to create clipboard events");
Shutdown();
return false;
}
m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
if (!m_thread)
{
DEBUG_ERROR_HR(GetLastError(), "Failed to create clipboard worker");
Shutdown();
return false;
}
if (!AddClipboardFormatListener(m_hwnd))
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to register the clipboard listener");
Shutdown();
return false;
}
m_listener = true;
m_channel.SetHandler(this);
return true;
}
void CClipboardManager::Shutdown()
{
if (Atomic::Swap(m_shutdown, true))
return;
m_channel.ClearHandler(this);
if (m_listener && m_hwnd)
RemoveClipboardFormatListener(m_hwnd);
m_listener = false;
if (m_hwnd)
{
KillTimer(m_hwnd, LOCAL_RETRY_TIMER);
KillTimer(m_hwnd, REMOTE_RETRY_TIMER);
}
if (m_oleClipboard)
{
if (OleIsCurrentClipboard(m_oleClipboard) == S_OK)
OleSetClipboard(nullptr);
m_oleClipboard->Release();
m_oleClipboard = nullptr;
}
if (m_remoteFileProvider)
m_remoteFileProvider->StopAccepting();
CancelIncoming(ERROR_OPERATION_ABORTED);
CancelFileRequests(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
if (m_remoteFileProvider)
m_remoteFileProvider->Detach();
if (m_stop)
SetEvent(m_stop);
if (m_thread)
WaitForSingleObject(m_thread, INFINITE);
if (m_thread)
CloseHandle(m_thread);
if (m_wake)
CloseHandle(m_wake);
if (m_stop)
CloseHandle(m_stop);
{
std::lock_guard<std::mutex> lock(m_workLock);
for (size_t i = 0; i < m_recordWorkCount; ++i)
m_recordWork[i].reset();
for (size_t i = 0; i < m_sendWorkCount; ++i)
m_sendWork[i].reset();
m_recordWorkCount = 0;
m_sendWorkCount = 0;
}
{
std::lock_guard<std::recursive_mutex> lock(m_uiLock);
while (m_uiWorkCount)
m_uiWork[--m_uiWorkCount] = UIWork {};
}
m_thread = nullptr;
m_wake = nullptr;
m_stop = nullptr;
{
std::lock_guard<std::mutex> lock(m_fileLock);
m_remoteFileManifest.reset();
m_incomingFileRequests.clear();
m_localFileAcquisitions.clear();
m_remoteFileAcquisitions.clear();
m_localFileDatasets.clear();
m_outgoingFileRequests.clear();
}
m_remoteFileProvider.reset();
if (m_oleInitialized)
{
OleUninitialize();
m_oleInitialized = false;
}
}
DWORD WINAPI CClipboardManager::ThreadProc(void * context)
{
static_cast<CClipboardManager *>(context)->Thread();
return 0;
}
void CClipboardManager::Thread()
{
const HRESULT com = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
ComScope comScope { SUCCEEDED(com) };
if (FAILED(com))
DEBUG_WARN_HR(com, "Failed to initialize clipboard worker COM");
HANDLE events[] = { m_stop, m_wake };
for (;;)
{
const DWORD result = WaitForMultipleObjects(
ARRAYSIZE(events), events, FALSE, RemoteFileManifestWait());
if (result == WAIT_TIMEOUT)
{
CheckRemoteFileManifestTimeout();
continue;
}
if (result == WAIT_OBJECT_0)
return;
if (result != WAIT_OBJECT_0 + 1)
return;
for (;;)
{
Work work;
bool haveWork = false;
{
std::lock_guard<std::mutex> lock(m_workLock);
if (m_pendingControlCount)
{
work.type = m_pendingControl[0].type;
work.available = m_pendingControl[0].available;
work.epoch = m_pendingControl[0].epoch;
work.reason = m_pendingControl[0].reason;
for (size_t i = 1; i < m_pendingControlCount; ++i)
m_pendingControl[i - 1] = m_pendingControl[i];
--m_pendingControlCount;
haveWork = true;
}
if (!haveWork && m_recordWorkCount)
{
work = std::move(*m_recordWork[0]);
for (size_t i = 1; i < m_recordWorkCount; ++i)
m_recordWork[i - 1] = std::move(m_recordWork[i]);
m_recordWork[m_recordWorkCount - 1].reset();
--m_recordWorkCount;
haveWork = true;
}
if (!haveWork)
for (PendingCancel& pending : m_pendingCancel)
if (pending.valid)
{
work.type = WorkType::SEND;
work.record = pending.record;
work.deadline = pending.deadline;
pending.valid = false;
haveWork = true;
break;
}
if (!haveWork && m_sendWorkCount)
{
work = std::move(*m_sendWork[0]);
for (size_t i = 1; i < m_sendWorkCount; ++i)
m_sendWork[i - 1] = std::move(m_sendWork[i]);
m_sendWork[m_sendWorkCount - 1].reset();
--m_sendWorkCount;
haveWork = true;
}
}
if (!haveWork)
break;
ProcessWork(std::move(work));
CheckRemoteFileManifestTimeout();
if (WaitForSingleObject(m_stop, 0) == WAIT_OBJECT_0)
return;
}
}
}
bool CClipboardManager::QueueWork(Work&& work)
{
if (Atomic::Load(m_shutdown))
return false;
const bool publication = work.type == WorkType::SEND &&
(work.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
work.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR);
{
std::lock_guard<std::mutex> lock(m_workLock);
// A BUSY publication is retried through this same path. Validate it while
// holding the queue lock so an older retry cannot erase a newer clipboard
// publication which became live before it entered the queue.
if (publication && work.record.clipboardGeneration != Atomic::Load(
m_liveLocalGeneration, std::memory_order_acquire))
return false;
std::array<std::optional<Work>, MAX_WORK>& queue =
work.type == WorkType::RECORD ?
m_recordWork : m_sendWork;
size_t& count = work.type == WorkType::RECORD ?
m_recordWorkCount : m_sendWorkCount;
auto erase = [&queue, &count](size_t index) {
for (size_t i = index + 1; i < count; ++i)
queue[i - 1] = std::move(queue[i]);
queue[count - 1].reset();
--count;
};
if (publication)
{
for (size_t i = 0; i < count;)
{
const Work& queued = *queue[i];
if (queued.type == WorkType::SEND &&
(queued.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
queued.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR))
erase(i);
else
++i;
}
}
if (count == MAX_WORK)
return false;
queue[count++].emplace(std::move(work));
}
SetEvent(m_wake);
return true;
}
bool CClipboardManager::QueueCancel(
const KVMFRClipboardMessage& record, uint32_t reason, uint64_t deadline)
{
KVMFRClipboardMessage cancel = record;
cancel.version = KVMFR_CLIPBOARD_VERSION;
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
cancel.token = reason;
cancel.length = 0;
cancel.flags = 0;
cancel.offset = 0;
cancel.size = 0;
cancel.sequence = 0;
if (!deadline)
deadline = GetTickCount64() + SEND_TIMEOUT_MS;
return QueueCancellation(cancel, deadline);
}
bool CClipboardManager::QueueCancellation(
const KVMFRClipboardMessage& cancel, uint64_t deadline)
{
if (Atomic::Load(m_shutdown) || !cancel.transfer)
return false;
{
std::lock_guard<std::mutex> lock(m_workLock);
for (size_t i = 0; i < m_sendWorkCount;)
{
const bool sameTransfer =
m_sendWork[i]->record.transfer == cancel.transfer;
if (sameTransfer &&
(m_sendWork[i]->type == WorkType::SEND_DATA ||
m_sendWork[i]->type == WorkType::SEND_FILE_DATA ||
(m_sendWork[i]->type == WorkType::SEND &&
(m_sendWork[i]->record.type ==
KVMFR_CLIPBOARD_MESSAGE_REQUEST ||
m_sendWork[i]->record.type ==
KVMFR_CLIPBOARD_MESSAGE_CANCEL ||
m_sendWork[i]->record.type ==
KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST ||
m_sendWork[i]->record.type ==
KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL))))
{
for (size_t j = i + 1; j < m_sendWorkCount; ++j)
m_sendWork[j - 1] = std::move(m_sendWork[j]);
m_sendWork[m_sendWorkCount - 1].reset();
--m_sendWorkCount;
}
else
++i;
}
PendingCancel * free = nullptr;
for (PendingCancel& pending : m_pendingCancel)
{
if (pending.valid &&
pending.record.transfer == cancel.transfer)
{
pending.record = cancel;
pending.deadline = deadline;
SetEvent(m_wake);
return true;
}
if (!pending.valid && !free)
free = &pending;
}
if (!free)
{
// CANCELs are idempotent and bounded by their transfer ID. Under a
// pathological peer stall retain the newest cancellation instead of
// allocating or failing a producer-side error path.
free = &m_pendingCancel[
m_pendingCancelCursor++ % MAX_PENDING_CANCEL];
}
free->record = cancel;
free->deadline = deadline;
free->valid = true;
}
SetEvent(m_wake);
return true;
}
void CClipboardManager::QueueControl(WorkType type, bool available,
uint64_t epoch, uint32_t reason)
{
if (Atomic::Load(m_shutdown))
return;
{
std::lock_guard<std::mutex> lock(m_workLock);
if (m_pendingControlCount)
{
PendingControl& last = m_pendingControl[m_pendingControlCount - 1];
if (last.type == type &&
(type == WorkType::RESET || last.available == available))
{
last.available = available;
last.epoch = epoch;
last.reason = reason;
SetEvent(m_wake);
return;
}
}
if (m_pendingControlCount == MAX_PENDING_CONTROL)
{
// Collapse a pathologically noisy lifecycle into one reset. RESET's
// worker path republishes the channel's current state after teardown.
m_pendingControlCount = 1;
m_pendingControl[0].type = WorkType::RESET;
m_pendingControl[0].available = false;
m_pendingControl[0].epoch = epoch;
m_pendingControl[0].reason = reason ? reason :
ERROR_OPERATION_ABORTED;
}
else
{
PendingControl& pending = m_pendingControl[m_pendingControlCount++];
pending.type = type;
pending.available = available;
pending.epoch = epoch;
pending.reason = reason;
}
}
SetEvent(m_wake);
}
bool CClipboardManager::QueueUI(UIWork&& work)
{
if (Atomic::Load(m_shutdown))
return false;
std::array<KVMFRClipboardMessage, MAX_UI_WORK> dropped = {};
size_t droppedCount = 0;
{
std::lock_guard<std::recursive_mutex> lock(m_uiLock);
auto erase = [this, &dropped, &droppedCount](size_t index) {
if (m_uiWork[index].type == UIType::REQUEST)
dropped[droppedCount++] = m_uiWork[index].record;
for (size_t i = index + 1; i < m_uiWorkCount; ++i)
m_uiWork[i - 1] = m_uiWork[i];
--m_uiWorkCount;
m_uiWork[m_uiWorkCount] = UIWork {};
};
if (work.type == UIType::STATE && !work.available)
while (m_uiWorkCount)
erase(m_uiWorkCount - 1);
else if (work.type == UIType::STATE)
{
for (size_t i = 0; i < m_uiWorkCount;)
{
if (m_uiWork[i].type == UIType::STATE &&
m_uiWork[i].available == work.available)
erase(i);
else
++i;
}
}
else if (work.type == UIType::OFFER || work.type == UIType::CLEAR ||
work.type == UIType::FILE_OFFER || work.type == UIType::FILES)
{
for (size_t i = 0; i < m_uiWorkCount;)
{
if (m_uiWork[i].type == UIType::OFFER ||
m_uiWork[i].type == UIType::CLEAR ||
m_uiWork[i].type == UIType::FILE_OFFER ||
m_uiWork[i].type == UIType::FILES)
erase(i);
else
++i;
}
}
if (m_uiWorkCount == MAX_UI_WORK)
{
if (work.type == UIType::REQUEST)
return false;
erase(0);
}
m_uiWork[m_uiWorkCount++] = work;
}
for (size_t i = 0; i < droppedCount; ++i)
QueueCancel(dropped[i], ERROR_BUSY);
if (PostMessageW(m_hwnd, WM_CLIPBOARD_WORK, 0, 0))
return true;
bool removed = false;
{
std::lock_guard<std::recursive_mutex> lock(m_uiLock);
for (size_t i = m_uiWorkCount; i; --i)
{
UIWork& queued = m_uiWork[i - 1];
bool same = queued.type == work.type;
if (same && work.type == UIType::STATE)
same = queued.available == work.available &&
queued.epoch == work.epoch;
else if (same && (work.type == UIType::OFFER ||
work.type == UIType::CLEAR ||
work.type == UIType::FILE_OFFER ||
work.type == UIType::FILES))
{
same = queued.record.clipboardGeneration ==
work.record.clipboardGeneration;
if (same && (work.type == UIType::OFFER ||
work.type == UIType::FILE_OFFER ||
work.type == UIType::FILES))
same = queued.record.token == work.record.token;
}
else if (same && work.type == UIType::REQUEST)
same = queued.record.transfer == work.record.transfer;
if (!same)
continue;
for (size_t j = i; j < m_uiWorkCount; ++j)
m_uiWork[j - 1] = m_uiWork[j];
--m_uiWorkCount;
m_uiWork[m_uiWorkCount] = UIWork {};
removed = true;
break;
}
}
// A previously posted drain may already have consumed the record.
return !removed;
}
void CClipboardManager::DiscardPendingRemoteUI()
{
std::array<UIWork, MAX_UI_WORK> discarded;
size_t discardedCount = 0;
{
std::lock_guard<std::recursive_mutex> lock(m_uiLock);
size_t retained = 0;
for (size_t i = 0; i < m_uiWorkCount; ++i)
{
UIWork& work = m_uiWork[i];
if (work.type == UIType::OFFER || work.type == UIType::CLEAR ||
work.type == UIType::FILE_OFFER || work.type == UIType::FILES)
{
discarded[discardedCount++] = std::move(work);
continue;
}
if (retained != i)
m_uiWork[retained] = std::move(work);
++retained;
}
for (size_t i = retained; i < m_uiWorkCount; ++i)
m_uiWork[i] = UIWork {};
m_uiWorkCount = retained;
}
}
void CClipboardManager::ProcessWork(Work&& work)
{
switch (work.type)
{
case WorkType::STATE:
{
if (!work.available)
{
Atomic::Store(m_outgoingTransfer, 0, std::memory_order_release);
CancelIncoming(ERROR_DEVICE_NOT_CONNECTED);
CancelFileRequests(KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED);
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED);
std::lock_guard<std::mutex> lock(m_fileLock);
m_localFileAcquisitions.clear();
m_remoteFileAcquisitions.clear();
m_outgoingFileRequests.clear();
PruneLocalFileDatasets();
}
UIWork ui;
ui.type = UIType::STATE;
ui.available = work.available;
ui.epoch = work.epoch;
QueueUI(std::move(ui));
break;
}
case WorkType::RESET:
{
Atomic::Store(m_outgoingTransfer, 0, std::memory_order_release);
CancelIncoming(work.reason ? work.reason : ERROR_OPERATION_ABORTED);
CancelFileRequests(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
{
std::lock_guard<std::mutex> lock(m_fileLock);
m_localFileAcquisitions.clear();
m_remoteFileAcquisitions.clear();
m_outgoingFileRequests.clear();
PruneLocalFileDatasets();
}
UIWork ui;
ui.type = UIType::STATE;
ui.available = false;
ui.epoch = work.epoch;
QueueUI(std::move(ui));
if (m_channel.Available())
{
UIWork resume;
resume.type = UIType::STATE;
resume.available = true;
resume.epoch = m_channel.Epoch();
QueueUI(std::move(resume));
}
break;
}
case WorkType::RECORD:
ProcessRecord(work.record,
work.data.empty() ? nullptr : work.data.data());
break;
case WorkType::SEND:
ProcessSend(std::move(work));
break;
case WorkType::SEND_DATA:
ProcessSendData(std::move(work));
break;
case WorkType::SEND_FILE_DATA:
ProcessSendFileData(std::move(work));
break;
case WorkType::CANCEL_FILE_MANIFEST:
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
break;
}
}
void CClipboardManager::ProcessRecord(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
if (kvmfrClipboardFileMessageType(record.type))
{
ProcessFileRecord(record, data);
return;
}
switch (record.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
{
CancelIncoming(ERROR_OPERATION_ABORTED);
InvalidateOutgoing(ERROR_OPERATION_ABORTED);
if (record.token & KVMFR_CLIPBOARD_FORMAT_MASK_FILES)
{
StartRemoteFileOffer(record);
break;
}
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
UIWork ui;
ui.type = UIType::OFFER;
ui.record = record;
if (!QueueUI(std::move(ui)))
DEBUG_WARN("Failed to queue remote clipboard offer");
break;
}
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
{
CancelIncoming(ERROR_OPERATION_ABORTED);
InvalidateOutgoing(ERROR_OPERATION_ABORTED);
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
UIWork ui;
ui.type = UIType::CLEAR;
ui.record = record;
if (!QueueUI(std::move(ui)))
DEBUG_WARN("Failed to queue remote clipboard clear");
break;
}
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
{
UIWork ui;
ui.type = UIType::REQUEST;
ui.record = record;
if (!QueueUI(std::move(ui)))
QueueCancel(record, ERROR_BUSY);
break;
}
case KVMFR_CLIPBOARD_MESSAGE_DATA:
ProcessData(record, data);
break;
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
{
ReleaseOutgoing(record.transfer);
CancelIncoming(record.token ? record.token : ERROR_OPERATION_ABORTED,
record.transfer);
break;
}
}
}
void CClipboardManager::ProcessData(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
bool cancel = false;
uint32_t cancelReason = ERROR_INVALID_DATA;
{
std::lock_guard<std::mutex> lock(m_transferLock);
const std::shared_ptr<IncomingTransfer> transfer = m_incoming;
if (!transfer || transfer->complete ||
transfer->transfer != record.transfer ||
transfer->generation != record.clipboardGeneration ||
transfer->format != record.format)
cancel = true;
else
{
bool valid = record.offset == transfer->nextOffset &&
record.sequence == transfer->nextSequence;
if (!transfer->began)
{
valid = valid && record.offset == 0 &&
(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN);
transfer->began = true;
transfer->sizeHint = record.size;
if (record.size != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
record.size > MAX_SPOOL_BYTES)
{
transfer->error = ERROR_FILE_TOO_LARGE;
valid = false;
}
}
else if (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)
valid = false;
if (valid && record.length &&
!transfer->spool->Append(data, record.length))
{
const DWORD error = GetLastError();
transfer->error = error ? error : ERROR_DISK_FULL;
valid = false;
}
if (valid)
{
transfer->nextOffset += record.length;
++transfer->nextSequence;
if (record.flags & KVMFR_CLIPBOARD_FLAG_END)
{
valid = record.size == transfer->nextOffset &&
(transfer->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
transfer->sizeHint == transfer->nextOffset);
if (valid)
{
transfer->complete = true;
transfer->error = ERROR_SUCCESS;
}
}
}
if (!valid)
{
transfer->complete = true;
if (transfer->error == ERROR_SUCCESS)
transfer->error = ERROR_INVALID_DATA;
cancel = true;
cancelReason = transfer->error;
}
if (transfer->complete)
SetEvent(transfer->event);
}
}
if (cancel)
QueueCancel(record, cancelReason);
}
void CClipboardManager::ProcessSend(Work&& work)
{
if (!work.deadline)
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
auto failFileControl = [this](const KVMFRClipboardMessage& record,
KVMFRClipboardFileError error) {
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE)
{
if (m_remoteFileManifest &&
m_remoteFileManifest->dataset == record.clipboardGeneration &&
m_remoteFileManifest->acquisition == record.transfer)
FailRemoteFileManifest(error);
return;
}
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
{
std::shared_ptr<IncomingFileRequest> request;
bool manifest = false;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto found = m_incomingFileRequests.find(record.transfer);
if (found == m_incomingFileRequests.end() ||
found->second->dataset != record.clipboardGeneration)
return;
request = found->second;
request->complete = true;
request->error = error;
manifest = request->manifest;
if (request->event)
SetEvent(request->event);
if (manifest)
m_incomingFileRequests.erase(found);
}
if (manifest)
FailRemoteFileManifest(error);
return;
}
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED)
{
if (record.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE)
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto found = m_localFileAcquisitions.find(record.transfer);
if (found != m_localFileAcquisitions.end() &&
found->second == record.clipboardGeneration)
m_localFileAcquisitions.erase(found);
PruneLocalFileDatasets();
}
QueueFileCancel(record, error);
return;
}
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE)
QueueFileCancel(record, error);
};
if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE)
{
if (!m_remoteFileManifest ||
m_remoteFileManifest->dataset != work.record.clipboardGeneration ||
m_remoteFileManifest->acquisition != work.record.transfer)
return;
}
else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto found = m_incomingFileRequests.find(work.record.transfer);
if (found == m_incomingFileRequests.end() || found->second->complete ||
found->second->dataset != work.record.clipboardGeneration)
return;
}
else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED &&
work.record.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE)
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto found = m_localFileAcquisitions.find(work.record.transfer);
if (found == m_localFileAcquisitions.end() ||
found->second != work.record.clipboardGeneration)
return;
}
const bool publication =
work.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
work.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR;
ClipboardChannelResult result;
if (publication)
{
// Serialize the final generation check with invalidation. Once a remote
// offer has invalidated this generation, no queued retry may enter the
// channel after that invalidation point.
std::lock_guard<std::mutex> lock(m_outgoingLock);
if (work.record.clipboardGeneration != Atomic::Load(
m_liveLocalGeneration, std::memory_order_acquire))
return;
result = m_channel.Send(work.record, nullptr);
}
else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
{
std::lock_guard<std::mutex> lock(m_transferLock);
if (!m_incoming || m_incoming->complete ||
m_incoming->transfer != work.record.transfer ||
m_incoming->generation != work.record.clipboardGeneration ||
m_incoming->format != work.record.format)
return;
if (GetTickCount64() >= work.deadline)
{
m_incoming->complete = true;
m_incoming->error = ERROR_TIMEOUT;
SetEvent(m_incoming->event);
return;
}
result = m_channel.Send(work.record, nullptr);
}
else
result = m_channel.Send(work.record, nullptr);
if (result == ClipboardChannelResult::ACCEPTED)
{
if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER &&
(work.record.token & KVMFR_CLIPBOARD_FORMAT_MASK_FILES))
{
size_t roots = 0;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto dataset = m_localFileDatasets.find(
work.record.clipboardGeneration);
if (dataset != m_localFileDatasets.end())
roots = dataset->second->RootCount();
}
DEBUG_INFO("Published local clipboard files: dataset=%llu "
"formats=0x%08x roots=%zu",
static_cast<unsigned long long>(
work.record.clipboardGeneration),
work.record.token, roots);
}
return;
}
const KVMFRClipboardMessageType publicationType = work.record.type;
const uint64_t publicationGeneration = work.record.clipboardGeneration;
auto failLocalPublication = [this, publicationType,
publicationGeneration]() {
if (publicationType != KVMFR_CLIPBOARD_MESSAGE_OFFER &&
publicationType != KVMFR_CLIPBOARD_MESSAGE_CLEAR)
return;
uint64_t generation = publicationGeneration;
if (Atomic::CAS(m_liveLocalGeneration, generation, UINT64_C(0),
std::memory_order_acq_rel))
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
};
if (result == ClipboardChannelResult::BUSY &&
GetTickCount64() < work.deadline &&
m_channel.WaitWritable(m_stop, CHANNEL_RETRY_MS))
{
if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL)
QueueCancel(work.record, work.record.token, work.deadline);
else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL)
QueueCancellation(work.record, work.deadline);
else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
{
const KVMFRClipboardMessage record = work.record;
bool live = false;
bool queued = false;
{
std::lock_guard<std::mutex> lock(m_transferLock);
live = m_incoming && !m_incoming->complete &&
m_incoming->transfer == record.transfer &&
m_incoming->generation == record.clipboardGeneration &&
m_incoming->format == record.format;
if (live)
queued = QueueWork(std::move(work));
}
if (live && !queued)
CancelIncoming(ERROR_BUSY, record.transfer);
}
else
{
const KVMFRClipboardMessage record = work.record;
if (!QueueWork(std::move(work)))
{
failLocalPublication();
failFileControl(record, KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE);
}
}
return;
}
if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
{
const uint32_t reason = result == ClipboardChannelResult::BUSY ?
ERROR_TIMEOUT : ERROR_DEVICE_NOT_CONNECTED;
CancelIncoming(reason, work.record.transfer);
}
else
{
failLocalPublication();
failFileControl(work.record,
result == ClipboardChannelResult::BUSY ?
KVMFR_CLIPBOARD_FILE_ERROR_IO :
KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED);
}
}
void CClipboardManager::ProcessSendData(Work&& work)
{
if (!work.spool)
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_INVALID_DATA);
return;
}
if (Atomic::Load(m_outgoingTransfer, std::memory_order_acquire) !=
work.record.transfer)
return;
if (!work.deadline)
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
if (GetTickCount64() >= work.deadline)
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_TIMEOUT);
return;
}
if (work.record.clipboardGeneration !=
Atomic::Load(m_liveLocalGeneration, std::memory_order_acquire))
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_OPERATION_ABORTED);
return;
}
const uint64_t total = work.spool->Size();
if (work.record.offset > total)
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_INVALID_DATA);
return;
}
const uint64_t batchBytes = (std::min<uint64_t>)(
static_cast<uint64_t>(KVMFR_CLIPBOARD_REPRESENTATION_BYTES) *
KVMFR_CLIPBOARD_SLOT_COUNT,
total - work.record.offset);
std::vector<uint8_t> data;
if (batchBytes)
{
try
{
data.resize(static_cast<size_t>(batchBytes));
}
catch (const std::bad_alloc&)
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_OUTOFMEMORY);
return;
}
if (!work.spool->Read(work.record.offset, data.data(), data.size()))
{
const DWORD error = GetLastError();
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, error ? error : ERROR_READ_FAULT);
return;
}
}
std::array<ClipboardChannelWrite, KVMFR_CLIPBOARD_SLOT_COUNT> batch;
size_t batchCount = 0;
uint64_t offset = work.record.offset;
uint32_t sequence = work.record.sequence;
do
{
ClipboardChannelWrite& write = batch[batchCount++];
const size_t length = static_cast<size_t>((std::min<uint64_t>)(
KVMFR_CLIPBOARD_REPRESENTATION_BYTES, total - offset));
write.record = work.record;
write.record.type = KVMFR_CLIPBOARD_MESSAGE_DATA;
write.record.token = 0;
write.record.offset = offset;
write.record.sequence = sequence;
write.record.length = static_cast<uint32_t>(length);
write.record.flags = 0;
if (!offset)
write.record.flags |= KVMFR_CLIPBOARD_FLAG_BEGIN;
if (offset + length == total)
write.record.flags |= KVMFR_CLIPBOARD_FLAG_END;
write.record.size =
write.record.flags & KVMFR_CLIPBOARD_FLAG_END ? total :
(write.record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN ? total :
KVMFR_CLIPBOARD_SIZE_UNKNOWN);
write.data = length ? data.data() +
static_cast<size_t>(offset - work.record.offset) : nullptr;
offset += length;
++sequence;
}
while (batchCount < batch.size() && offset < total);
ClipboardChannelResult result = ClipboardChannelResult::FAILED;
size_t accepted = 0;
bool stale = false;
bool timedOut = false;
{
std::lock_guard<std::mutex> lock(m_outgoingLock);
stale = Atomic::Load(m_outgoingTransfer, std::memory_order_acquire) !=
work.record.transfer ||
work.record.clipboardGeneration != Atomic::Load(
m_liveLocalGeneration, std::memory_order_acquire);
timedOut = GetTickCount64() >= work.deadline;
if (!stale && !timedOut)
result = m_channel.SendBatch(batch.data(), batchCount, accepted);
}
if (stale)
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_OPERATION_ABORTED);
return;
}
if (timedOut)
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_TIMEOUT);
return;
}
KVMFRClipboardMessage progress = work.record;
if (accepted)
{
progress = batch[accepted - 1U].record;
work.record.offset = progress.offset + progress.length;
work.record.sequence = progress.sequence + 1U;
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
if (progress.flags & KVMFR_CLIPBOARD_FLAG_END)
{
ReleaseOutgoing(progress.transfer);
return;
}
}
if (result == ClipboardChannelResult::BUSY)
{
if (GetTickCount64() < work.deadline &&
m_channel.WaitWritable(m_stop, CHANNEL_RETRY_MS))
{
const KVMFRClipboardMessage record = work.record;
if (!QueueWork(std::move(work)))
{
ReleaseOutgoing(record.transfer);
QueueCancel(record, ERROR_BUSY);
}
}
else
{
ReleaseOutgoing(work.record.transfer);
QueueCancel(work.record, ERROR_TIMEOUT);
}
return;
}
if (result != ClipboardChannelResult::ACCEPTED)
{
ReleaseOutgoing(progress.transfer);
QueueCancel(progress, ERROR_DEVICE_NOT_CONNECTED);
return;
}
const KVMFRClipboardMessage record = work.record;
if (!QueueWork(std::move(work)))
{
ReleaseOutgoing(record.transfer);
QueueCancel(record, ERROR_BUSY);
}
}
bool CClipboardManager::QueueFileCancel(
const KVMFRClipboardMessage& record, KVMFRClipboardFileError error)
{
if (!error)
error = KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED;
{
std::lock_guard<std::mutex> lock(m_fileLock);
m_outgoingFileRequests.erase(record.transfer);
}
Work work;
work.type = WorkType::SEND;
work.record.version = KVMFR_CLIPBOARD_VERSION;
work.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
work.record.clipboardGeneration = record.clipboardGeneration;
work.record.transfer = record.transfer;
work.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
work.record.token = error;
return QueueCancellation(work.record,
GetTickCount64() + SEND_TIMEOUT_MS);
}
void CClipboardManager::ProcessSendFileData(Work&& work)
{
if (!work.fileData)
{
QueueFileCancel(work.record, KVMFR_CLIPBOARD_FILE_ERROR_INVALID);
return;
}
if (!work.deadline)
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto active = m_outgoingFileRequests.find(work.record.transfer);
if (active == m_outgoingFileRequests.end() ||
active->second.dataset != work.record.clipboardGeneration)
return;
if (active->second.cancelled)
{
m_outgoingFileRequests.erase(active);
return;
}
}
if (GetTickCount64() >= work.deadline)
{
QueueFileCancel(work.record, KVMFR_CLIPBOARD_FILE_ERROR_IO);
return;
}
const uint64_t total = work.fileData->size();
if (work.record.offset > total)
{
QueueFileCancel(work.record, KVMFR_CLIPBOARD_FILE_ERROR_INVALID);
return;
}
std::array<ClipboardChannelWrite, KVMFR_CLIPBOARD_SLOT_COUNT> batch;
size_t batchCount = 0;
uint64_t offset = work.record.offset;
uint32_t sequence = work.record.sequence;
do
{
ClipboardChannelWrite& write = batch[batchCount++];
const size_t length = static_cast<size_t>((std::min<uint64_t>)(
KVMFR_CLIPBOARD_DATA_BYTES, total - offset));
write.record = work.record;
write.record.version = KVMFR_CLIPBOARD_VERSION;
write.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
write.record.offset = offset;
write.record.sequence = sequence;
write.record.length = static_cast<uint32_t>(length);
write.record.flags = 0;
if (!offset)
write.record.flags |= KVMFR_CLIPBOARD_FLAG_BEGIN;
if (offset + length == total)
write.record.flags |= KVMFR_CLIPBOARD_FLAG_END;
write.record.size =
write.record.flags & KVMFR_CLIPBOARD_FLAG_END ? total :
(write.record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN ? total :
KVMFR_CLIPBOARD_SIZE_UNKNOWN);
write.data = length ? work.fileData->data() +
static_cast<size_t>(offset) : nullptr;
offset += length;
++sequence;
}
while (batchCount < batch.size() && offset < total);
size_t accepted = 0;
const ClipboardChannelResult result =
m_channel.SendBatch(batch.data(), batchCount, accepted);
KVMFRClipboardMessage progress = work.record;
if (accepted)
{
progress = batch[accepted - 1U].record;
work.record.offset = progress.offset + progress.length;
work.record.sequence = progress.sequence + 1U;
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
if (progress.flags & KVMFR_CLIPBOARD_FLAG_END)
{
std::lock_guard<std::mutex> lock(m_fileLock);
m_outgoingFileRequests.erase(progress.transfer);
return;
}
}
if (result == ClipboardChannelResult::BUSY &&
GetTickCount64() < work.deadline &&
m_channel.WaitWritable(m_stop, CHANNEL_RETRY_MS))
{
if (!QueueWork(std::move(work)))
QueueFileCancel(progress, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
if (result != ClipboardChannelResult::ACCEPTED)
{
QueueFileCancel(progress,
result == ClipboardChannelResult::BUSY ?
KVMFR_CLIPBOARD_FILE_ERROR_IO :
KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED);
return;
}
if (!QueueWork(std::move(work)))
QueueFileCancel(progress, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
}
void CClipboardManager::ProcessFileRecord(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
switch (record.type)
{
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
HandleFileAcquire(record);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
{
if (!m_remoteFileManifest ||
m_remoteFileManifest->dataset != record.clipboardGeneration ||
m_remoteFileManifest->acquisition != record.transfer)
break;
if (record.token != KVMFR_CLIPBOARD_FILE_ERROR_NONE)
{
FailRemoteFileManifest(record.token);
break;
}
KVMFRClipboardFileError acquisitionError =
KVMFR_CLIPBOARD_FILE_ERROR_NONE;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto existing = m_remoteFileAcquisitions.find(
record.clipboardGeneration);
if (existing != m_remoteFileAcquisitions.end() &&
existing->second != record.transfer)
acquisitionError = KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE;
else if (existing == m_remoteFileAcquisitions.end())
try
{
m_remoteFileAcquisitions.emplace(record.clipboardGeneration,
record.transfer);
}
catch (const std::bad_alloc&)
{
acquisitionError = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
}
if (acquisitionError)
{
FailRemoteFileManifest(acquisitionError);
break;
}
if (m_remoteFileManifest->currentRequest)
break;
StartRemoteFileList(KVMFR_CLIPBOARD_FILE_ROOT_NODE);
break;
}
case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
HandleFileRelease(record);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
HandleFileRequest(record);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
ProcessFileData(record, data);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
{
std::shared_ptr<IncomingFileRequest> incoming;
bool manifest = false;
bool acquisition = false;
{
std::lock_guard<std::mutex> lock(m_fileLock);
if (m_remoteFileManifest &&
m_remoteFileManifest->acquisition == record.transfer &&
m_remoteFileManifest->dataset == record.clipboardGeneration)
acquisition = true;
const auto found = m_incomingFileRequests.find(record.transfer);
if (!acquisition && found != m_incomingFileRequests.end() &&
found->second->dataset == record.clipboardGeneration)
{
incoming = found->second;
incoming->complete = true;
incoming->error = record.token;
manifest = incoming->manifest;
if (incoming->event)
SetEvent(incoming->event);
if (manifest)
m_incomingFileRequests.erase(found);
}
else if (!acquisition)
{
const auto lease = m_localFileAcquisitions.find(record.transfer);
if (lease != m_localFileAcquisitions.end())
{
m_localFileAcquisitions.erase(lease);
PruneLocalFileDatasets();
}
else
{
bool remoteAcquisition = false;
const auto remote = m_remoteFileAcquisitions.find(
record.clipboardGeneration);
if (remote != m_remoteFileAcquisitions.end() &&
remote->second == record.transfer)
{
m_remoteFileAcquisitions.erase(remote);
remoteAcquisition = true;
for (auto& pending : m_incomingFileRequests)
if (pending.second->dataset == record.clipboardGeneration)
{
pending.second->complete = true;
pending.second->error = record.token;
if (pending.second->event)
SetEvent(pending.second->event);
}
}
if (!remoteAcquisition)
{
const auto outgoing = m_outgoingFileRequests.find(
record.transfer);
if (outgoing != m_outgoingFileRequests.end() &&
outgoing->second.dataset == record.clipboardGeneration)
outgoing->second.cancelled = true;
}
}
}
}
if (manifest || acquisition)
FailRemoteFileManifest(record.token);
break;
}
}
}
void CClipboardManager::HandleFileAcquire(
const KVMFRClipboardMessage& record)
{
KVMFRClipboardFileError error = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
bool added = false;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto dataset = m_localFileDatasets.find(
record.clipboardGeneration);
const auto existing = m_localFileAcquisitions.find(record.transfer);
if (dataset == m_localFileDatasets.end())
error = KVMFR_CLIPBOARD_FILE_ERROR_STALE;
else if (existing != m_localFileAcquisitions.end() &&
existing->second != record.clipboardGeneration)
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
else if (existing == m_localFileAcquisitions.end())
{
if (m_outgoingFileRequests.find(record.transfer) !=
m_outgoingFileRequests.end())
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
size_t acquisitions = m_localFileAcquisitions.size() +
m_remoteFileAcquisitions.size();
if (!error && m_remoteFileManifest)
{
const auto pending = m_remoteFileAcquisitions.find(
m_remoteFileManifest->dataset);
if (pending == m_remoteFileAcquisitions.end() ||
pending->second != m_remoteFileManifest->acquisition)
++acquisitions;
}
if (!error && acquisitions >= KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS)
error = KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE;
else if (!error)
try
{
m_localFileAcquisitions.emplace(record.transfer,
record.clipboardGeneration);
added = true;
}
catch (const std::bad_alloc&)
{
error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
}
}
if (error)
DEBUG_WARN("Local clipboard file acquire failed: dataset=%llu "
"acquisition=%llu fileError=%u",
static_cast<unsigned long long>(record.clipboardGeneration),
static_cast<unsigned long long>(record.transfer),
static_cast<unsigned int>(error));
else
DEBUG_INFO("Local clipboard file acquire accepted: dataset=%llu "
"acquisition=%llu",
static_cast<unsigned long long>(record.clipboardGeneration),
static_cast<unsigned long long>(record.transfer));
Work response;
response.type = WorkType::SEND;
response.record.version = KVMFR_CLIPBOARD_VERSION;
response.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED;
response.record.clipboardGeneration = record.clipboardGeneration;
response.record.transfer = record.transfer;
response.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
response.record.token = error;
if (!QueueWork(std::move(response)))
{
if (added)
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto acquisition = m_localFileAcquisitions.find(record.transfer);
if (acquisition != m_localFileAcquisitions.end() &&
acquisition->second == record.clipboardGeneration)
m_localFileAcquisitions.erase(acquisition);
PruneLocalFileDatasets();
}
QueueFileCancel(record, error ? error :
KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE);
}
}
void CClipboardManager::HandleFileRelease(
const KVMFRClipboardMessage& record)
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto acquisition = m_localFileAcquisitions.find(record.transfer);
if (acquisition != m_localFileAcquisitions.end() &&
acquisition->second == record.clipboardGeneration)
m_localFileAcquisitions.erase(acquisition);
PruneLocalFileDatasets();
}
void CClipboardManager::HandleFileRequest(
const KVMFRClipboardMessage& record)
{
const bool initialList =
record.token == KVMFR_CLIPBOARD_FILE_OP_LIST &&
record.size == KVMFR_CLIPBOARD_FILE_ROOT_NODE;
std::shared_ptr<CLocalClipboardFiles> dataset;
KVMFRClipboardFileError error = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
{
std::lock_guard<std::mutex> lock(m_fileLock);
bool acquired = false;
for (const auto& lease : m_localFileAcquisitions)
if (lease.second == record.clipboardGeneration)
{
acquired = true;
break;
}
const auto found = m_localFileDatasets.find(record.clipboardGeneration);
if (acquired && found != m_localFileDatasets.end())
dataset = found->second;
if (dataset && m_localFileAcquisitions.find(record.transfer) !=
m_localFileAcquisitions.end())
{
dataset.reset();
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
}
else if (dataset && m_outgoingFileRequests.find(record.transfer) !=
m_outgoingFileRequests.end())
{
dataset.reset();
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
}
else if (dataset && m_outgoingFileRequests.size() +
m_incomingFileRequests.size() >=
KVMFR_CLIPBOARD_FILE_MAX_REQUESTS)
{
dataset.reset();
error = KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE;
}
else if (dataset)
{
try
{
m_outgoingFileRequests.emplace(record.transfer,
OutgoingFileRequest { record.clipboardGeneration, false });
}
catch (const std::bad_alloc&)
{
dataset.reset();
error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
}
}
if (!dataset)
{
const KVMFRClipboardFileError failure = error ? error :
KVMFR_CLIPBOARD_FILE_ERROR_STALE;
if (initialList)
DEBUG_WARN("Initial local clipboard file LIST rejected: "
"dataset=%llu request=%llu fileError=%u",
static_cast<unsigned long long>(record.clipboardGeneration),
static_cast<unsigned long long>(record.transfer),
static_cast<unsigned int>(failure));
QueueFileCancel(record, failure);
return;
}
std::shared_ptr<std::vector<uint8_t>> data;
try
{
data = std::make_shared<std::vector<uint8_t>>();
}
catch (const std::bad_alloc&)
{
if (initialList)
DEBUG_ERROR_HR(E_OUTOFMEMORY,
"Initial local clipboard file LIST allocation failed: "
"dataset=%llu request=%llu fileError=%u",
static_cast<unsigned long long>(record.clipboardGeneration),
static_cast<unsigned long long>(record.transfer),
static_cast<unsigned int>(KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY));
QueueFileCancel(record, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
bool success = false;
try
{
if (record.token == KVMFR_CLIPBOARD_FILE_OP_LIST)
success = dataset->List(record.size, *data, error);
else if (record.token == KVMFR_CLIPBOARD_FILE_OP_READ)
success = dataset->Read(record.size, record.offset, record.flags,
*data, error);
}
catch (const std::bad_alloc&)
{
error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
if (!success)
{
if (initialList)
DEBUG_ERROR_HR(ClipboardFileHRESULT(error),
"Initial local clipboard file LIST failed: dataset=%llu "
"request=%llu fileError=%u",
static_cast<unsigned long long>(record.clipboardGeneration),
static_cast<unsigned long long>(record.transfer),
static_cast<unsigned int>(error));
QueueFileCancel(record, error);
return;
}
if (initialList)
DEBUG_INFO("Initial local clipboard file LIST completed: dataset=%llu "
"request=%llu bytes=%zu",
static_cast<unsigned long long>(record.clipboardGeneration),
static_cast<unsigned long long>(record.transfer), data->size());
Work response;
response.type = WorkType::SEND_FILE_DATA;
response.record.version = KVMFR_CLIPBOARD_VERSION;
response.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
response.record.clipboardGeneration = record.clipboardGeneration;
response.record.transfer = record.transfer;
response.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
response.record.token = record.token;
response.fileData = std::move(data);
if (!QueueWork(std::move(response)))
QueueFileCancel(record, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
}
void CClipboardManager::ProcessFileData(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
std::shared_ptr<IncomingFileRequest> request;
bool finishManifest = false;
KVMFRClipboardFileError cancel = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto found = m_incomingFileRequests.find(record.transfer);
if (found == m_incomingFileRequests.end() ||
found->second->dataset != record.clipboardGeneration)
cancel = KVMFR_CLIPBOARD_FILE_ERROR_STALE;
else
{
request = found->second;
bool valid = !request->complete &&
request->dataset == record.clipboardGeneration &&
request->operation == record.token &&
request->nextOffset == record.offset &&
request->nextSequence == record.sequence;
if (!request->began)
{
valid = valid && !record.offset &&
(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN);
request->began = true;
request->sizeHint = record.size;
if (request->operation == KVMFR_CLIPBOARD_FILE_OP_READ &&
record.size != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
record.size > request->requestedBytes)
valid = false;
}
else if (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)
valid = false;
if (valid && request->operation == KVMFR_CLIPBOARD_FILE_OP_READ &&
(request->nextOffset > request->requestedBytes ||
record.length > request->requestedBytes -
request->nextOffset))
valid = false;
if (valid && record.length)
{
if (request->operation == KVMFR_CLIPBOARD_FILE_OP_READ &&
request->output)
{
if (request->nextOffset > request->outputCapacity ||
record.length >
request->outputCapacity - request->nextOffset)
valid = false;
else
memcpy(request->output +
static_cast<size_t>(request->nextOffset), data,
record.length);
}
else
{
try
{
request->data.insert(request->data.end(), data,
data + record.length);
}
catch (const std::bad_alloc&)
{
valid = false;
request->error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
}
}
if (valid)
{
request->nextOffset += record.length;
++request->nextSequence;
if (request->manifest && m_remoteFileManifest &&
m_remoteFileManifest->currentRequest == request->transfer)
m_remoteFileManifest->deadline =
GetTickCount64() + FILE_MANIFEST_TIMEOUT_MS;
if (record.flags & KVMFR_CLIPBOARD_FLAG_END)
{
valid = record.size == request->nextOffset &&
(request->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
request->sizeHint == request->nextOffset) &&
(request->operation != KVMFR_CLIPBOARD_FILE_OP_READ ||
request->nextOffset <= request->requestedBytes);
if (valid)
{
request->complete = true;
request->error = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
}
}
}
if (!valid)
{
request->complete = true;
if (!request->error)
request->error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
cancel = request->error;
}
if (request->event)
SetEvent(request->event);
if (request->complete && request->manifest)
{
finishManifest = true;
m_incomingFileRequests.erase(found);
}
}
}
if (cancel)
QueueFileCancel(record, cancel);
if (finishManifest)
ContinueRemoteFileManifest(request);
}
uint64_t CClipboardManager::NextHelperTransfer()
{
uint64_t current = Atomic::Load(m_nextTransfer,
std::memory_order_relaxed);
for (;;)
{
uint64_t transfer = current;
if (!kvmfrClipboardTransferFromHelper(transfer) ||
transfer == KVMFR_CLIPBOARD_TRANSFER_HELPER)
transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1);
uint64_t next = transfer + UINT64_C(1);
if (!kvmfrClipboardTransferFromHelper(next) ||
next == KVMFR_CLIPBOARD_TRANSFER_HELPER)
next = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1);
if (Atomic::CASWeak(m_nextTransfer, current, next,
std::memory_order_relaxed))
return transfer;
}
}
void CClipboardManager::StartRemoteFileOffer(
const KVMFRClipboardMessage& record)
{
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
DiscardPendingRemoteUI();
UIWork pending;
pending.type = UIType::FILE_OFFER;
pending.record = record;
if (!QueueUI(std::move(pending)))
{
KVMFRClipboardMessage denied = {};
denied.clipboardGeneration = record.clipboardGeneration;
denied.transfer = NextHelperTransfer();
QueueFileCancel(denied, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
const uint64_t acquisition = NextHelperTransfer();
bool full = false;
{
std::lock_guard<std::mutex> lock(m_fileLock);
if (m_localFileAcquisitions.size() +
m_remoteFileAcquisitions.size() >=
KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS)
full = true;
}
if (full)
{
KVMFRClipboardMessage denied = {};
denied.clipboardGeneration = record.clipboardGeneration;
denied.transfer = acquisition;
QueueFileCancel(denied, KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE);
return;
}
try
{
m_remoteFileManifest = std::make_unique<RemoteFileManifest>();
m_remoteFileManifest->dataset = record.clipboardGeneration;
m_remoteFileManifest->acquisition = acquisition;
m_remoteFileManifest->deadline =
GetTickCount64() + FILE_MANIFEST_TIMEOUT_MS;
m_remoteFileManifest->offer = record;
}
catch (const std::bad_alloc&)
{
m_remoteFileManifest.reset();
KVMFRClipboardMessage denied = {};
denied.clipboardGeneration = record.clipboardGeneration;
denied.transfer = acquisition;
QueueFileCancel(denied, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
Work acquire;
acquire.type = WorkType::SEND;
acquire.record.version = KVMFR_CLIPBOARD_VERSION;
acquire.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE;
acquire.record.clipboardGeneration = record.clipboardGeneration;
acquire.record.transfer = m_remoteFileManifest->acquisition;
acquire.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
if (!QueueWork(std::move(acquire)))
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
}
void CClipboardManager::StartRemoteFileList(uint64_t parent)
{
if (!m_remoteFileManifest)
return;
std::shared_ptr<IncomingFileRequest> request;
try
{
request = std::make_shared<IncomingFileRequest>();
}
catch (const std::bad_alloc&)
{
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
request->dataset = m_remoteFileManifest->dataset;
request->transfer = NextHelperTransfer();
request->node = parent;
request->operation = KVMFR_CLIPBOARD_FILE_OP_LIST;
request->manifest = true;
bool full = false;
KVMFRClipboardFileError insertError =
KVMFR_CLIPBOARD_FILE_ERROR_NONE;
{
std::lock_guard<std::mutex> lock(m_fileLock);
if (m_incomingFileRequests.size() + m_outgoingFileRequests.size() >=
KVMFR_CLIPBOARD_FILE_MAX_REQUESTS)
full = true;
else
try
{
if (!m_incomingFileRequests.emplace(request->transfer,
request).second)
insertError = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
}
catch (const std::bad_alloc&)
{
insertError = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
}
if (full || insertError)
{
FailRemoteFileManifest(full ? KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE :
insertError);
return;
}
m_remoteFileManifest->currentRequest = request->transfer;
m_remoteFileManifest->currentParent = parent;
m_remoteFileManifest->deadline =
GetTickCount64() + FILE_MANIFEST_TIMEOUT_MS;
Work work;
work.type = WorkType::SEND;
work.record.version = KVMFR_CLIPBOARD_VERSION;
work.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST;
work.record.clipboardGeneration = request->dataset;
work.record.transfer = request->transfer;
work.record.size = parent;
work.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
work.record.token = KVMFR_CLIPBOARD_FILE_OP_LIST;
if (!QueueWork(std::move(work)))
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
}
void CClipboardManager::ContinueRemoteFileManifest(
const std::shared_ptr<IncomingFileRequest>& request)
{
if (!m_remoteFileManifest || !request ||
m_remoteFileManifest->currentRequest != request->transfer)
return;
if (request->error != KVMFR_CLIPBOARD_FILE_ERROR_NONE)
{
FailRemoteFileManifest(request->error);
return;
}
std::vector<ClipboardRemoteFileEntry> parsed;
const HRESULT result = ParseClipboardFileList(request->node,
request->data.empty() ? nullptr : request->data.data(),
request->data.size(), parsed);
if (FAILED(result))
{
FailRemoteFileManifest(result == E_OUTOFMEMORY ?
KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY :
KVMFR_CLIPBOARD_FILE_ERROR_INVALID);
return;
}
try
{
for (ClipboardRemoteFileEntry& entry : parsed)
{
if (!m_remoteFileManifest->nodes.insert(entry.node).second)
{
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_INVALID);
return;
}
if (entry.type == KVMFR_CLIPBOARD_FILE_TYPE_DIRECTORY)
m_remoteFileManifest->directories.push_back(entry.node);
m_remoteFileManifest->entries.emplace_back(std::move(entry));
}
}
catch (const std::bad_alloc&)
{
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY);
return;
}
m_remoteFileManifest->currentRequest = 0;
if (!m_remoteFileManifest->directories.empty())
{
const uint64_t parent = m_remoteFileManifest->directories.front();
m_remoteFileManifest->directories.pop_front();
StartRemoteFileList(parent);
return;
}
FinishRemoteFileManifest();
}
void CClipboardManager::FinishRemoteFileManifest()
{
if (!m_remoteFileManifest)
return;
const uint64_t dataset = m_remoteFileManifest->dataset;
const uint64_t acquisition = m_remoteFileManifest->acquisition;
const KVMFRClipboardMessage offer = m_remoteFileManifest->offer;
std::vector<ClipboardRemoteFileEntry> entries =
std::move(m_remoteFileManifest->entries);
m_remoteFileManifest.reset();
IDataObject * object = nullptr;
HRESULT result = CreateClipboardFileDataObject(dataset, acquisition,
std::move(entries), m_remoteFileProvider, &object);
if (FAILED(result))
{
ReleaseRemoteFileDataset(dataset, acquisition);
return;
}
IStream * stream = nullptr;
result = CoMarshalInterThreadInterfaceInStream(IID_IDataObject,
object, &stream);
object->Release();
if (FAILED(result))
return;
std::shared_ptr<MarshaledDataObject> marshaled;
try
{
marshaled = std::make_shared<MarshaledDataObject>();
}
catch (const std::bad_alloc&)
{
CoReleaseMarshalData(stream);
stream->Release();
return;
}
marshaled->stream = stream;
UIWork ui;
ui.type = UIType::FILES;
ui.record = offer;
ui.dataObject = std::move(marshaled);
if (!QueueUI(std::move(ui)))
DEBUG_WARN("Failed to queue clipboard file object");
}
void CClipboardManager::FailRemoteFileManifest(
KVMFRClipboardFileError error)
{
if (!m_remoteFileManifest)
return;
const uint64_t dataset = m_remoteFileManifest->dataset;
const uint64_t acquisition = m_remoteFileManifest->acquisition;
const uint64_t request = m_remoteFileManifest->currentRequest;
bool acquired = false;
{
std::lock_guard<std::mutex> lock(m_fileLock);
if (request)
m_incomingFileRequests.erase(request);
const auto found = m_remoteFileAcquisitions.find(dataset);
acquired = found != m_remoteFileAcquisitions.end() &&
found->second == acquisition;
if (acquired)
m_remoteFileAcquisitions.erase(found);
}
m_remoteFileManifest.reset();
Work work;
work.type = WorkType::SEND;
work.record.version = KVMFR_CLIPBOARD_VERSION;
work.record.type = acquired ? KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE :
KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
work.record.clipboardGeneration = dataset;
work.record.transfer = acquisition;
work.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
work.record.token = acquired ? 0 :
(error ? error : KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
const KVMFRClipboardMessage record = work.record;
if (acquired)
{
if (!QueueWork(std::move(work)))
QueueFileCancel(record, KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
}
else
QueueFileCancel(record, static_cast<KVMFRClipboardFileError>(
record.token));
}
DWORD CClipboardManager::RemoteFileManifestWait() const
{
if (!m_remoteFileManifest || !m_remoteFileManifest->deadline)
return INFINITE;
const uint64_t now = GetTickCount64();
if (now >= m_remoteFileManifest->deadline)
return 0;
const uint64_t remaining = m_remoteFileManifest->deadline - now;
return static_cast<DWORD>((std::min<uint64_t>)(remaining,
static_cast<uint64_t>(INFINITE - 1U)));
}
void CClipboardManager::CheckRemoteFileManifestTimeout()
{
if (m_remoteFileManifest && m_remoteFileManifest->deadline &&
GetTickCount64() >= m_remoteFileManifest->deadline)
FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_IO);
}
void CClipboardManager::CancelFileRequests(
KVMFRClipboardFileError error, uint64_t transfer)
{
std::lock_guard<std::mutex> lock(m_fileLock);
for (auto& item : m_incomingFileRequests)
{
if (transfer && item.first != transfer)
continue;
item.second->complete = true;
item.second->error = error;
if (item.second->event)
SetEvent(item.second->event);
}
}
HRESULT CClipboardManager::ReadRemoteFile(uint64_t dataset,
uint64_t acquisition, uint64_t node, uint64_t offset, void * output,
ULONG length, ULONG& read)
{
read = 0;
if (Atomic::Load(m_shutdown))
return STG_E_READFAULT;
if (!output && length)
return STG_E_INVALIDPOINTER;
uint8_t * destination = static_cast<uint8_t *>(output);
while (length)
{
const uint32_t wanted = static_cast<uint32_t>((std::min<ULONG>)(length,
KVMFR_CLIPBOARD_FILE_READ_BYTES));
std::shared_ptr<IncomingFileRequest> request;
try
{
request = std::make_shared<IncomingFileRequest>();
}
catch (const std::bad_alloc&)
{
return E_OUTOFMEMORY;
}
request->event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!request->event)
return HRESULT_FROM_WIN32(GetLastError());
request->dataset = dataset;
request->transfer = NextHelperTransfer();
request->node = node;
request->operation = KVMFR_CLIPBOARD_FILE_OP_READ;
request->requestedBytes = wanted;
request->output = destination;
request->outputCapacity = wanted;
KVMFRClipboardFileError insertError =
KVMFR_CLIPBOARD_FILE_ERROR_NONE;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto lease = m_remoteFileAcquisitions.find(dataset);
if (Atomic::Load(m_shutdown) ||
lease == m_remoteFileAcquisitions.end() ||
lease->second != acquisition ||
m_incomingFileRequests.size() + m_outgoingFileRequests.size() >=
KVMFR_CLIPBOARD_FILE_MAX_REQUESTS)
return STG_E_READFAULT;
try
{
if (!m_incomingFileRequests.emplace(request->transfer,
request).second)
insertError = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
}
catch (const std::bad_alloc&)
{
insertError = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
}
}
if (insertError)
return insertError == KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY ?
E_OUTOFMEMORY : STG_E_READFAULT;
Work work;
work.type = WorkType::SEND;
work.record.version = KVMFR_CLIPBOARD_VERSION;
work.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST;
work.record.clipboardGeneration = dataset;
work.record.transfer = request->transfer;
work.record.offset = offset;
work.record.size = node;
work.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
work.record.flags = wanted;
work.record.token = KVMFR_CLIPBOARD_FILE_OP_READ;
if (!QueueWork(std::move(work)))
{
std::lock_guard<std::mutex> lock(m_fileLock);
m_incomingFileRequests.erase(request->transfer);
return STG_E_READFAULT;
}
bool timedOut = false;
for (;;)
{
{
std::lock_guard<std::mutex> lock(m_fileLock);
if (request->complete)
break;
ResetEvent(request->event);
}
if (WaitForSingleObject(request->event, SEND_TIMEOUT_MS) !=
WAIT_OBJECT_0)
{
timedOut = true;
break;
}
}
{
std::lock_guard<std::mutex> lock(m_fileLock);
m_incomingFileRequests.erase(request->transfer);
}
if (timedOut)
{
KVMFRClipboardMessage cancel = {};
cancel.clipboardGeneration = dataset;
cancel.transfer = request->transfer;
QueueFileCancel(cancel, KVMFR_CLIPBOARD_FILE_ERROR_IO);
return STG_E_READFAULT;
}
if (request->error != KVMFR_CLIPBOARD_FILE_ERROR_NONE)
return request->error == KVMFR_CLIPBOARD_FILE_ERROR_ACCESS ?
STG_E_ACCESSDENIED : STG_E_READFAULT;
if (request->nextOffset > wanted)
return STG_E_READFAULT;
const ULONG actual = static_cast<ULONG>(request->nextOffset);
destination += actual;
read += actual;
offset += actual;
length -= actual;
if (actual < wanted)
break;
}
return S_OK;
}
void CClipboardManager::ReleaseRemoteFileDataset(uint64_t dataset,
uint64_t expectedAcquisition)
{
uint64_t acquisition = 0;
{
std::lock_guard<std::mutex> lock(m_fileLock);
const auto found = m_remoteFileAcquisitions.find(dataset);
if (found == m_remoteFileAcquisitions.end() ||
found->second != expectedAcquisition)
return;
acquisition = found->second;
m_remoteFileAcquisitions.erase(found);
}
Work work;
work.type = WorkType::SEND;
work.record.version = KVMFR_CLIPBOARD_VERSION;
work.record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE;
work.record.clipboardGeneration = dataset;
work.record.transfer = acquisition;
work.record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
const KVMFRClipboardMessage release = work.record;
if (!QueueWork(std::move(work)))
QueueFileCancel(release, KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED);
}
void CClipboardManager::PruneLocalFileDatasets()
{
for (auto dataset = m_localFileDatasets.begin();
dataset != m_localFileDatasets.end();)
{
bool leased = false;
for (const auto& acquisition : m_localFileAcquisitions)
if (acquisition.second == dataset->first)
{
leased = true;
break;
}
if (dataset->first != m_localFileDataset && !leased)
dataset = m_localFileDatasets.erase(dataset);
else
++dataset;
}
}
void CClipboardManager::RetireLocalFileDataset()
{
std::lock_guard<std::mutex> lock(m_fileLock);
m_localFileDataset = 0;
PruneLocalFileDatasets();
}
void CClipboardManager::CancelIncoming(uint32_t reason, uint64_t transferID)
{
std::lock_guard<std::mutex> lock(m_transferLock);
const std::shared_ptr<IncomingTransfer> transfer = m_incoming;
if (!transfer || (transferID && transferID != transfer->transfer))
return;
transfer->complete = true;
transfer->error = reason;
SetEvent(transfer->event);
}
void CClipboardManager::ReleaseOutgoing(uint64_t transfer)
{
std::lock_guard<std::mutex> lock(m_outgoingLock);
Atomic::CAS(m_outgoingTransfer, transfer, UINT64_C(0),
std::memory_order_acq_rel);
}
void CClipboardManager::ClipboardState(bool available, uint64_t epoch)
{
QueueControl(WorkType::STATE, available, epoch, 0);
}
ClipboardChannelResult CClipboardManager::ClipboardRecord(
const KVMFRClipboardMessage& record, std::vector<uint8_t>&& data)
{
if (data.size() != record.length)
return ClipboardChannelResult::FAILED;
switch (record.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
if (!record.clipboardGeneration || !record.token ||
(record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) ||
record.format || record.transfer || record.length || record.flags ||
record.offset || record.size || record.sequence)
return ClipboardChannelResult::FAILED;
break;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
if (!record.clipboardGeneration || record.format || record.transfer ||
record.length || record.flags || record.offset || record.size ||
record.sequence || record.token)
return ClipboardChannelResult::FAILED;
break;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
if (!record.clipboardGeneration || !record.transfer ||
!kvmfrClipboardTransferFromClient(record.transfer) ||
!kvmfrClipboardRepresentationFormatValid(record.format) ||
record.length ||
record.flags || record.offset || record.size || record.sequence ||
record.token)
return ClipboardChannelResult::FAILED;
break;
case KVMFR_CLIPBOARD_MESSAGE_DATA:
if (!record.clipboardGeneration || !record.transfer ||
!kvmfrClipboardTransferFromHelper(record.transfer) ||
!kvmfrClipboardRepresentationFormatValid(record.format))
return ClipboardChannelResult::FAILED;
break;
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
if (!record.transfer || record.length || record.flags ||
(record.format &&
!kvmfrClipboardRepresentationFormatValid(record.format)))
return ClipboardChannelResult::FAILED;
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
if (!kvmfrClipboardFileMessageValid(&record) ||
(record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE &&
!kvmfrClipboardTransferFromClient(record.transfer)) ||
(record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED &&
!kvmfrClipboardTransferFromHelper(record.transfer)) ||
(record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE &&
!kvmfrClipboardTransferFromClient(record.transfer)) ||
(record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST &&
!kvmfrClipboardTransferFromClient(record.transfer)) ||
(record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA &&
!kvmfrClipboardTransferFromHelper(record.transfer)))
return ClipboardChannelResult::FAILED;
break;
default:
return ClipboardChannelResult::FAILED;
}
Work work;
work.type = WorkType::RECORD;
work.record = record;
if (record.length)
work.data = std::move(data);
if (QueueWork(std::move(work)))
return ClipboardChannelResult::ACCEPTED;
return Atomic::Load(m_shutdown) ? ClipboardChannelResult::FAILED :
ClipboardChannelResult::BUSY;
}
void CClipboardManager::ClipboardReset(uint64_t epoch, uint32_t reason)
{
QueueControl(WorkType::RESET, false, epoch, reason);
}
bool CClipboardManager::HandleMessage(UINT message, WPARAM wParam,
LPARAM, LRESULT& result)
{
switch (message)
{
case WM_CLIPBOARD_WORK:
DrainUI();
result = 0;
return true;
case WM_CLIPBOARDUPDATE:
HandleClipboardUpdate();
result = 0;
return true;
case WM_RENDERFORMAT:
RenderFormat(static_cast<UINT>(wParam));
result = 0;
return true;
case WM_RENDERALLFORMATS:
RenderAllFormats();
result = 0;
return true;
case WM_TIMER:
if (wParam == LOCAL_RETRY_TIMER)
{
RetryLocalClipboard();
result = 0;
return true;
}
if (wParam == REMOTE_RETRY_TIMER)
{
RetryRemoteOffer();
result = 0;
return true;
}
break;
case WM_DESTROYCLIPBOARD:
HandleDestroyClipboard();
result = 0;
return true;
}
return false;
}
void CClipboardManager::DrainUI()
{
std::lock_guard<std::recursive_mutex> dispatchLock(m_uiLock);
for (;;)
{
UIWork work;
if (!m_uiWorkCount)
return;
work = m_uiWork[0];
for (size_t i = 1; i < m_uiWorkCount; ++i)
m_uiWork[i - 1] = m_uiWork[i];
--m_uiWorkCount;
m_uiWork[m_uiWorkCount] = UIWork {};
switch (work.type)
{
case UIType::STATE:
HandleState(work.available, work.epoch);
break;
case UIType::OFFER:
HandleOffer(work.record);
break;
case UIType::CLEAR:
HandleClear(work.record);
break;
case UIType::REQUEST:
HandleRequest(work.record);
break;
case UIType::FILE_OFFER:
HandlePendingFileOffer(work.record);
break;
case UIType::FILES:
HandleFileDataObject(work);
break;
}
}
}
void CClipboardManager::HandleState(bool available, uint64_t epoch)
{
m_available = available;
m_epoch = available ? epoch : 0;
if (!available)
{
ClearLocalRetry();
ClearRemoteRetry();
InvalidateLocalClipboard(ERROR_DEVICE_NOT_CONNECTED);
ClearOwnedClipboard();
return;
}
PublishLocalClipboard();
}
void CClipboardManager::HandleOffer(
const KVMFRClipboardMessage& record)
{
if (!m_available || !record.clipboardGeneration ||
!record.token || (record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL))
return;
// Clipboard generations identify content within one publisher process;
// they restart when the client does. Serialized channel order determines
// which publication is current.
ClearLocalRetry();
ClearRemoteRetry();
m_pendingRemoteOffer = record;
m_remoteRetryDeadline = GetTickCount64() + REMOTE_RETRY_TIMEOUT_MS;
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
RetryRemoteOffer();
}
void CClipboardManager::HandleClear(
const KVMFRClipboardMessage&)
{
ClearLocalRetry();
ClearRemoteRetry();
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
ClearOwnedClipboard();
}
void CClipboardManager::HandleRequest(
const KVMFRClipboardMessage& record)
{
if (!m_available || !record.transfer ||
!kvmfrClipboardRepresentationFormatValid(record.format) ||
record.clipboardGeneration != m_localGeneration ||
record.clipboardGeneration != Atomic::Load(
m_liveLocalGeneration, std::memory_order_acquire) ||
GetClipboardSequenceNumber() != m_localSequence)
{
QueueCancel(record, ERROR_NOT_FOUND);
return;
}
uint64_t noTransfer = 0;
if (!Atomic::CAS(m_outgoingTransfer, noTransfer,
record.transfer, std::memory_order_acq_rel))
{
QueueCancel(record, ERROR_BUSY);
return;
}
std::shared_ptr<CClipboardSpool> spool =
CaptureFormat(record.format, m_localSequence);
if (!spool)
{
const DWORD error = GetLastError();
ReleaseOutgoing(record.transfer);
QueueCancel(record, error ? error : ERROR_NOT_FOUND);
return;
}
Work data;
data.type = WorkType::SEND_DATA;
data.record = record;
data.record.version = KVMFR_CLIPBOARD_VERSION;
data.record.type = KVMFR_CLIPBOARD_MESSAGE_DATA;
data.record.sequence = 0;
data.record.offset = 0;
data.record.size = 0;
data.record.token = 0;
data.record.length = 0;
data.record.flags = 0;
data.spool = std::move(spool);
if (!QueueWork(std::move(data)))
{
ReleaseOutgoing(record.transfer);
QueueCancel(record, ERROR_BUSY);
}
}
void CClipboardManager::HandlePendingFileOffer(
const KVMFRClipboardMessage& record)
{
if (!m_available || !record.clipboardGeneration ||
!(record.token & KVMFR_CLIPBOARD_FORMAT_MASK_FILES))
return;
ClearLocalRetry();
ClearRemoteRetry();
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
}
void CClipboardManager::HandleFileDataObject(UIWork& work)
{
if (!m_available || !work.dataObject ||
!work.record.clipboardGeneration ||
!(work.record.token & KVMFR_CLIPBOARD_FORMAT_MASK_FILES))
return;
IStream * stream = work.dataObject->Take();
if (!stream)
return;
IDataObject * object = nullptr;
const HRESULT unmarshal = CoGetInterfaceAndReleaseStream(stream,
IID_IDataObject, reinterpret_cast<void **>(&object));
if (FAILED(unmarshal) || !object)
{
DEBUG_WARN_HR(unmarshal, "Failed to unmarshal clipboard file object");
return;
}
ClearLocalRetry();
ClearRemoteRetry();
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
m_applyingRemote = true;
const HRESULT applied = OleSetClipboard(object);
m_applyingRemote = false;
if (FAILED(applied))
{
DEBUG_WARN_HR(applied, "Failed to publish clipboard file object");
object->Release();
return;
}
if (m_oleClipboard)
m_oleClipboard->Release();
m_oleClipboard = object;
m_remoteGeneration = work.record.clipboardGeneration;
m_remoteFormats = work.record.token;
m_ownedSequence = GetClipboardSequenceNumber();
}
HRESULT CClipboardManager::OpenClipboardRetry(const char * stage) const
{
HRESULT result = CLIPBRD_E_CANT_OPEN;
for (unsigned int attempt = 0; attempt != 8; ++attempt)
{
if (OpenClipboard(m_hwnd))
return S_OK;
const DWORD openError = GetLastError();
result = openError ? HRESULT_FROM_WIN32(openError) :
CLIPBRD_E_CANT_OPEN;
DEBUG_ERROR_HR(result,
"OpenClipboard failed: stage=%s attempt=%u",
stage, attempt + 1U);
if (attempt + 1U == 8U)
return result;
Sleep(5U << (std::min)(attempt, 5U));
}
return result;
}
bool CClipboardManager::IsOurClipboard()
{
if (m_oleClipboard && OleIsCurrentClipboard(m_oleClipboard) == S_OK)
{
m_ownedSequence = GetClipboardSequenceNumber();
return true;
}
if (GetClipboardOwner() == m_hwnd)
{
m_ownedSequence = GetClipboardSequenceNumber();
return true;
}
if (!m_formatOrigin || !m_remoteGeneration ||
!IsClipboardFormatAvailable(m_formatOrigin) ||
FAILED(OpenClipboardRetry("IsOurClipboard")))
return false;
bool ours = false;
HANDLE data = GetClipboardData(m_formatOrigin);
if (data && GlobalSize(data) >= sizeof(ClipboardOrigin))
{
const ClipboardOrigin * origin =
static_cast<const ClipboardOrigin *>(GlobalLock(data));
if (origin)
{
ours = origin->magic == ORIGIN_MAGIC && origin->epoch == m_epoch &&
origin->generation == m_remoteGeneration;
GlobalUnlock(data);
}
}
CloseClipboard();
return ours;
}
uint32_t CClipboardManager::EnumerateFormats() const
{
uint32_t formats = 0;
if (IsClipboardFormatAvailable(CF_UNICODETEXT))
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_TEXT;
if (m_formatPNG && IsClipboardFormatAvailable(m_formatPNG))
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_PNG;
if (IsClipboardFormatAvailable(CF_DIBV5) ||
IsClipboardFormatAvailable(CF_DIB))
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_BMP;
if (IsClipboardFormatAvailable(CF_TIFF))
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_TIFF;
if (m_formatJPEG && IsClipboardFormatAvailable(m_formatJPEG))
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_JPEG;
if (IsClipboardFormatAvailable(CF_HDROP))
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
return formats;
}
std::shared_ptr<CLocalClipboardFiles>
CClipboardManager::CaptureClipboardFiles(DWORD sequence, int rawFormatCount,
bool& viaOLE, const char *& retryStage,
KVMFRClipboardFileError& error, HRESULT& oleError)
{
viaOLE = false;
retryStage = nullptr;
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
oleError = S_FALSE;
const DWORD initialSequence = GetClipboardSequenceNumber();
if (initialSequence != sequence)
{
DEBUG_ERROR_HR(HRESULT_FROM_WIN32(ERROR_RETRY),
"Failed to capture local clipboard files: "
"stage=sequence-before-capture expected=%lu actual=%lu",
static_cast<unsigned long>(sequence),
static_cast<unsigned long>(initialSequence));
return nullptr;
}
// Acquire the clipboard before probing CF_HDROP. Explorer can still hold
// the clipboard when WM_CLIPBOARDUPDATE is delivered, in which case an
// unlocked IsClipboardFormatAvailable probe can observe no formats.
if (FAILED(OpenClipboardRetry("CaptureClipboardFiles(CF_HDROP)")))
{
oleError = CLIPBRD_E_CANT_OPEN;
retryStage = "OpenClipboard";
return nullptr;
}
std::shared_ptr<CLocalClipboardFiles> files;
bool win32Candidate = false;
bool sequenceChanged = false;
int openedRawFormatCount = 0;
if (GetClipboardSequenceNumber() == sequence)
{
openedRawFormatCount = CountClipboardFormatsLogged(
"CaptureClipboardFiles(CF_HDROP)", sequence);
win32Candidate = IsClipboardFormatAvailable(CF_HDROP) != FALSE;
if (win32Candidate)
{
SetLastError(ERROR_SUCCESS);
HDROP drop = static_cast<HDROP>(GetClipboardData(CF_HDROP));
if (drop)
{
files = CLocalClipboardFiles::Capture(drop, error);
if (!files)
{
DEBUG_ERROR_HR(ClipboardFileHRESULT(error),
"Failed to capture local clipboard files: "
"stage=CLocalClipboardFiles::Capture(CF_HDROP/win32) "
"sequence=%lu rawFormats=%d openedFormats=%d fileError=%u",
static_cast<unsigned long>(sequence), rawFormatCount,
openedRawFormatCount, static_cast<unsigned int>(error));
}
}
else
{
DWORD dataError = GetLastError();
if (!dataError)
dataError = ERROR_INVALID_DATA;
DEBUG_ERROR_HR(HRESULT_FROM_WIN32(dataError),
"Failed to capture local clipboard files: "
"stage=GetClipboardData(CF_HDROP) sequence=%lu rawFormats=%d "
"openedFormats=%d",
static_cast<unsigned long>(sequence), rawFormatCount,
openedRawFormatCount);
}
}
else
DEBUG_TRACE("Local clipboard does not expose Win32 CF_HDROP: "
"sequence=%lu rawFormats=%d openedFormats=%d",
static_cast<unsigned long>(sequence), rawFormatCount,
openedRawFormatCount);
}
else
{
sequenceChanged = true;
DEBUG_ERROR_HR(HRESULT_FROM_WIN32(ERROR_RETRY),
"Failed to capture local clipboard files: "
"stage=sequence-after-OpenClipboard expected=%lu actual=%lu",
static_cast<unsigned long>(sequence),
static_cast<unsigned long>(GetClipboardSequenceNumber()));
}
const BOOL closed = CloseClipboard();
if (!closed)
{
const DWORD closeError = GetLastError();
oleError = closeError ? HRESULT_FROM_WIN32(closeError) :
CLIPBRD_E_CANT_CLOSE;
error = closeError == ERROR_ACCESS_DENIED ?
KVMFR_CLIPBOARD_FILE_ERROR_ACCESS : KVMFR_CLIPBOARD_FILE_ERROR_IO;
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: stage=CloseClipboard "
"sequence=%lu rawFormats=%d",
static_cast<unsigned long>(sequence), rawFormatCount);
return nullptr;
}
if (files)
return files;
if (sequenceChanged)
return nullptr;
const DWORD oleSequence = GetClipboardSequenceNumber();
if (oleSequence != sequence)
{
DEBUG_ERROR_HR(HRESULT_FROM_WIN32(ERROR_RETRY),
"Failed to capture local clipboard files: "
"stage=sequence-before-OleGetClipboard expected=%lu actual=%lu",
static_cast<unsigned long>(sequence),
static_cast<unsigned long>(oleSequence));
return nullptr;
}
ClipboardComScope<IDataObject> object;
oleError = OleGetClipboard(&object.object);
if (FAILED(oleError) || !object.object)
{
if (SUCCEEDED(oleError))
oleError = E_UNEXPECTED;
if (oleError == CLIPBRD_E_CANT_OPEN)
retryStage = "OleGetClipboard";
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=OleGetClipboard sequence=%lu rawFormats=%d",
static_cast<unsigned long>(sequence), rawFormatCount);
return nullptr;
}
HRESULT candidateError = S_OK;
const uint32_t candidates = ClipboardDataObjectFileCandidates(
object.object, sequence, candidateError);
const bool optimistic = FAILED(candidateError);
if (!optimistic && !win32Candidate &&
candidates == CLIPBOARD_FILE_CANDIDATE_NONE)
{
error = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
oleError = S_FALSE;
return nullptr;
}
HRESULT hdropError = S_FALSE;
bool hdropAttempted = false;
if (optimistic || win32Candidate ||
(candidates & CLIPBOARD_FILE_CANDIDATE_HDROP))
{
hdropAttempted = true;
FORMATETC format = {
static_cast<CLIPFORMAT>(CF_HDROP),
nullptr,
DVASPECT_CONTENT,
-1,
TYMED_HGLOBAL,
};
ClipboardStorageScope storage;
hdropError = object.object->GetData(&format, &storage.medium);
if (FAILED(hdropError))
{
DEBUG_ERROR_HR(hdropError,
"Failed to capture local clipboard files: "
"stage=IDataObject::GetData(CF_HDROP) sequence=%lu rawFormats=%d",
static_cast<unsigned long>(sequence), rawFormatCount);
if (hdropError == CLIPBRD_E_CANT_OPEN)
{
oleError = hdropError;
retryStage = "IDataObject::GetData(CF_HDROP)";
return nullptr;
}
}
else
{
storage.acquired = true;
if (storage.medium.tymed != TYMED_HGLOBAL ||
!storage.medium.hGlobal)
{
oleError = DV_E_TYMED;
error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=IDataObject::GetData(CF_HDROP)/STGMEDIUM sequence=%lu "
"rawFormats=%d tymed=0x%08lx hasHGlobal=%u",
static_cast<unsigned long>(sequence), rawFormatCount,
static_cast<unsigned long>(storage.medium.tymed),
storage.medium.hGlobal ? 1U : 0U);
return nullptr;
}
files = CLocalClipboardFiles::Capture(
static_cast<HDROP>(storage.medium.hGlobal), error);
if (!files)
{
oleError = ClipboardFileHRESULT(error);
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=CLocalClipboardFiles::Capture(CF_HDROP/ole) sequence=%lu "
"rawFormats=%d fileError=%u",
static_cast<unsigned long>(sequence), rawFormatCount,
static_cast<unsigned int>(error));
return nullptr;
}
viaOLE = true;
oleError = S_OK;
return files;
}
}
if (!optimistic &&
!(candidates & CLIPBOARD_FILE_CANDIDATE_SHELL))
{
if (hdropAttempted)
oleError = hdropError;
else
{
error = KVMFR_CLIPBOARD_FILE_ERROR_NONE;
oleError = S_FALSE;
}
return nullptr;
}
ClipboardComScope<IShellItemArray> items;
const HRESULT shellError = SHCreateShellItemArrayFromDataObject(object.object,
IID_IShellItemArray, reinterpret_cast<void **>(&items.object));
if (FAILED(shellError))
{
DEBUG_ERROR_HR(shellError,
"Failed to capture local clipboard files: "
"stage=SHCreateShellItemArrayFromDataObject sequence=%lu",
static_cast<unsigned long>(sequence));
}
else if (!items.object)
{
DEBUG_ERROR_HR(E_UNEXPECTED,
"Failed to capture local clipboard files: "
"stage=SHCreateShellItemArrayFromDataObject sequence=%lu "
"result=null",
static_cast<unsigned long>(sequence));
}
if (FAILED(shellError) || !items.object)
{
oleError = FAILED(shellError) ? shellError : E_UNEXPECTED;
if (oleError == CLIPBRD_E_CANT_OPEN)
retryStage = "SHCreateShellItemArrayFromDataObject";
return nullptr;
}
DWORD count = 0;
oleError = items.object->GetCount(&count);
if (FAILED(oleError) || !count)
{
if (SUCCEEDED(oleError))
oleError = HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=IShellItemArray::GetCount sequence=%lu count=%lu",
static_cast<unsigned long>(sequence),
static_cast<unsigned long>(count));
return nullptr;
}
std::vector<std::wstring> paths;
size_t characters = 1U;
try
{
paths.reserve(count);
for (DWORD index = 0; index < count; ++index)
{
ClipboardComScope<IShellItem> item;
oleError = items.object->GetItemAt(index, &item.object);
if (FAILED(oleError) || !item.object)
{
if (SUCCEEDED(oleError))
oleError = E_UNEXPECTED;
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=IShellItemArray::GetItemAt sequence=%lu item=%lu",
static_cast<unsigned long>(sequence),
static_cast<unsigned long>(index));
return nullptr;
}
ClipboardTaskStringScope path;
oleError = item.object->GetDisplayName(
SIGDN_FILESYSPATH, &path.value);
if (FAILED(oleError) || !path.value || !path.value[0])
{
if (SUCCEEDED(oleError))
oleError = HRESULT_FROM_WIN32(ERROR_INVALID_DATA);
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=IShellItem::GetDisplayName sequence=%lu item=%lu",
static_cast<unsigned long>(sequence),
static_cast<unsigned long>(index));
return nullptr;
}
const size_t length = wcslen(path.value);
if (length > (std::numeric_limits<size_t>::max)() - characters - 1U)
throw std::length_error("clipboard path list");
characters += length + 1U;
paths.emplace_back(path.value, length);
}
}
catch (const std::bad_alloc&)
{
oleError = E_OUTOFMEMORY;
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: stage=Shell-path-list "
"sequence=%lu", static_cast<unsigned long>(sequence));
return nullptr;
}
catch (const std::length_error&)
{
oleError = HRESULT_FROM_WIN32(ERROR_ARITHMETIC_OVERFLOW);
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: stage=Shell-path-list "
"sequence=%lu", static_cast<unsigned long>(sequence));
return nullptr;
}
if (characters > ((std::numeric_limits<SIZE_T>::max)() -
sizeof(DROPFILES)) / sizeof(wchar_t))
{
oleError = HRESULT_FROM_WIN32(ERROR_ARITHMETIC_OVERFLOW);
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: stage=DROPFILES-size "
"sequence=%lu", static_cast<unsigned long>(sequence));
return nullptr;
}
const SIZE_T dropBytes = sizeof(DROPFILES) +
characters * sizeof(wchar_t);
SetLastError(ERROR_SUCCESS);
HGLOBAL drop = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, dropBytes);
if (!drop)
{
const DWORD allocError = GetLastError();
oleError = allocError ? HRESULT_FROM_WIN32(allocError) : E_OUTOFMEMORY;
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: stage=GlobalAlloc(DROPFILES) "
"sequence=%lu", static_cast<unsigned long>(sequence));
return nullptr;
}
SetLastError(ERROR_SUCCESS);
DROPFILES * header = static_cast<DROPFILES *>(GlobalLock(drop));
if (!header)
{
const DWORD lockError = GetLastError();
oleError = lockError ? HRESULT_FROM_WIN32(lockError) : E_UNEXPECTED;
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: stage=GlobalLock(DROPFILES) "
"sequence=%lu", static_cast<unsigned long>(sequence));
FreeClipboardDrop(drop, sequence);
return nullptr;
}
header->pFiles = sizeof(*header);
header->fWide = TRUE;
wchar_t * output = reinterpret_cast<wchar_t *>(
reinterpret_cast<uint8_t *>(header) + sizeof(*header));
for (const std::wstring& path : paths)
{
memcpy(output, path.c_str(), (path.size() + 1U) * sizeof(wchar_t));
output += path.size() + 1U;
}
SetLastError(ERROR_SUCCESS);
const BOOL unlocked = GlobalUnlock(drop);
const DWORD unlockError = unlocked ? ERROR_SUCCESS : GetLastError();
if (!unlocked && unlockError != ERROR_SUCCESS)
{
oleError = HRESULT_FROM_WIN32(unlockError);
error = unlockError == ERROR_ACCESS_DENIED ?
KVMFR_CLIPBOARD_FILE_ERROR_ACCESS : KVMFR_CLIPBOARD_FILE_ERROR_IO;
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=GlobalUnlock(DROPFILES) sequence=%lu",
static_cast<unsigned long>(sequence));
FreeClipboardDrop(drop, sequence);
return nullptr;
}
files = CLocalClipboardFiles::Capture(static_cast<HDROP>(drop), error);
if (!files)
{
oleError = ClipboardFileHRESULT(error);
DEBUG_ERROR_HR(oleError,
"Failed to capture local clipboard files: "
"stage=CLocalClipboardFiles::Capture(Shell items) sequence=%lu "
"fileError=%u", static_cast<unsigned long>(sequence),
static_cast<unsigned int>(error));
}
FreeClipboardDrop(drop, sequence);
if (!files)
return nullptr;
viaOLE = true;
oleError = S_OK;
return files;
}
void CClipboardManager::HandleClipboardUpdate()
{
if (m_applyingRemote || IsOurClipboard())
{
ClearLocalRetry();
return;
}
if (m_localRetryDeadline &&
GetClipboardSequenceNumber() != m_localRetrySequence)
ClearLocalRetry();
ClearRemoteRetry();
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
m_remoteGeneration = 0;
m_remoteFormats = 0;
m_ownedSequence = 0;
CancelIncoming(ERROR_OPERATION_ABORTED);
Work cancelFiles;
cancelFiles.type = WorkType::CANCEL_FILE_MANIFEST;
QueueWork(std::move(cancelFiles));
if (m_oleClipboard)
{
m_oleClipboard->Release();
m_oleClipboard = nullptr;
}
PublishLocalClipboard();
}
void CClipboardManager::HandleDestroyClipboard()
{
if (m_applyingRemote)
return;
ClearLocalRetry();
ClearRemoteRetry();
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
m_remoteGeneration = 0;
m_remoteFormats = 0;
m_ownedSequence = 0;
CancelIncoming(ERROR_OPERATION_ABORTED);
Work cancelFiles;
cancelFiles.type = WorkType::CANCEL_FILE_MANIFEST;
QueueWork(std::move(cancelFiles));
if (m_oleClipboard && OleIsCurrentClipboard(m_oleClipboard) != S_OK)
{
m_oleClipboard->Release();
m_oleClipboard = nullptr;
}
}
void CClipboardManager::PublishLocalClipboard()
{
if (!m_available)
{
ClearLocalRetry();
Atomic::Store(m_liveLocalGeneration, UINT64_C(0),
std::memory_order_release);
m_localSequence = GetClipboardSequenceNumber();
return;
}
const DWORD before = GetClipboardSequenceNumber();
uint32_t formats = EnumerateFormats();
const uint32_t recognizedFormats = formats;
const int rawFormatCount = CountClipboardFormatsLogged(
"PublishLocalClipboard", before);
const DWORD after = GetClipboardSequenceNumber();
if (before != after)
{
ClearLocalRetry();
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
return;
}
KVMFRClipboardFileError fileError =
KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
HRESULT fileOleError = S_FALSE;
bool fileViaOLE = false;
const char * fileRetryStage = nullptr;
std::shared_ptr<CLocalClipboardFiles> files = CaptureClipboardFiles(
after, rawFormatCount, fileViaOLE, fileRetryStage, fileError,
fileOleError);
const DWORD capturedSequence = GetClipboardSequenceNumber();
if (capturedSequence != after)
{
DEBUG_ERROR_HR(HRESULT_FROM_WIN32(ERROR_RETRY),
"Failed to publish local clipboard: "
"stage=sequence-after-file-capture expected=%lu actual=%lu",
static_cast<unsigned long>(after),
static_cast<unsigned long>(capturedSequence));
ClearLocalRetry();
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
return;
}
if (fileOleError == CLIPBRD_E_CANT_OPEN)
{
DeferLocalClipboard(after, fileRetryStage);
return;
}
ClearLocalRetry();
uint64_t generation = ++m_localGeneration;
if (!generation)
generation = ++m_localGeneration;
m_localSequence = after;
if (files)
{
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
const size_t roots = files->RootCount();
bool stored = false;
try
{
std::lock_guard<std::mutex> lock(m_fileLock);
stored = m_localFileDatasets.emplace(generation,
std::move(files)).second;
if (stored)
{
m_localFileDataset = generation;
PruneLocalFileDatasets();
}
}
catch (const std::bad_alloc&)
{
}
if (stored)
{
DEBUG_INFO("Captured local clipboard files: sequence=%lu source=%s "
"roots=%zu", static_cast<unsigned long>(after),
fileViaOLE ? "ole" : "win32", roots);
}
else
{
DEBUG_ERROR_HR(E_OUTOFMEMORY,
"Failed to capture local clipboard files: stage=retain-dataset "
"sequence=%lu fileError=%u",
static_cast<unsigned long>(after), static_cast<unsigned int>(
KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY));
formats &= ~KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
}
}
else
formats &= ~KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
DEBUG_INFO("Local clipboard update: sequence=%lu rawFormats=%d "
"recognized=0x%08x effective=0x%08x",
static_cast<unsigned long>(after), rawFormatCount,
recognizedFormats, formats);
if (!formats)
{
PublishClear(generation);
return;
}
Work work;
work.type = WorkType::SEND;
work.record.version = KVMFR_CLIPBOARD_VERSION;
work.record.type = KVMFR_CLIPBOARD_MESSAGE_OFFER;
work.record.clipboardGeneration = generation;
work.record.token = formats;
{
std::lock_guard<std::mutex> lock(m_outgoingLock);
Atomic::Store(m_liveLocalGeneration, generation,
std::memory_order_release);
}
if (!QueueWork(std::move(work)))
{
uint64_t live = generation;
Atomic::CAS(m_liveLocalGeneration, live, UINT64_C(0),
std::memory_order_acq_rel);
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
}
}
void CClipboardManager::PublishClear(uint64_t generation)
{
Work work;
work.type = WorkType::SEND;
work.record.version = KVMFR_CLIPBOARD_VERSION;
work.record.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR;
work.record.clipboardGeneration = generation;
{
std::lock_guard<std::mutex> lock(m_outgoingLock);
Atomic::Store(m_liveLocalGeneration, generation,
std::memory_order_release);
}
if (!QueueWork(std::move(work)))
{
uint64_t live = generation;
Atomic::CAS(m_liveLocalGeneration, live, UINT64_C(0),
std::memory_order_acq_rel);
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
}
}
void CClipboardManager::InvalidateOutgoing(uint32_t reason)
{
uint64_t generation;
uint64_t transfer;
{
std::lock_guard<std::mutex> lock(m_outgoingLock);
generation = Atomic::Swap(m_liveLocalGeneration,
UINT64_C(0), std::memory_order_acq_rel);
transfer = Atomic::Swap(
m_outgoingTransfer, UINT64_C(0), std::memory_order_acq_rel);
}
if (!transfer)
return;
KVMFRClipboardMessage cancel = {};
cancel.clipboardGeneration = generation;
cancel.transfer = transfer;
QueueCancel(cancel, reason);
}
void CClipboardManager::InvalidateLocalClipboard(uint32_t reason)
{
m_localSequence = 0;
RetireLocalFileDataset();
InvalidateOutgoing(reason);
}
void CClipboardManager::ClearLocalRetry()
{
if (m_hwnd && m_localRetryDeadline)
KillTimer(m_hwnd, LOCAL_RETRY_TIMER);
m_localRetrySequence = 0;
m_localRetryDeadline = 0;
}
void CClipboardManager::DeferLocalClipboard(DWORD sequence,
const char * stage)
{
const uint64_t now = GetTickCount64();
if (!m_localRetryDeadline || m_localRetrySequence != sequence)
{
ClearLocalRetry();
m_localRetrySequence = sequence;
m_localRetryDeadline = now + LOCAL_RETRY_TIMEOUT_MS;
DEBUG_TRACE("Clipboard is busy; deferring local publication: "
"sequence=%lu stage=%s", static_cast<unsigned long>(sequence),
stage ? stage : "unknown");
}
if (now < m_localRetryDeadline &&
SetTimer(m_hwnd, LOCAL_RETRY_TIMER, LOCAL_RETRY_MS, nullptr))
return;
if (now >= m_localRetryDeadline)
DEBUG_WARN("Timed out waiting for local clipboard: sequence=%lu",
static_cast<unsigned long>(sequence));
else
DEBUG_WARN("Failed to schedule local clipboard retry: sequence=%lu",
static_cast<unsigned long>(sequence));
ExpireLocalRetry(sequence);
}
void CClipboardManager::ExpireLocalRetry(DWORD sequence)
{
ClearLocalRetry();
if (!m_available)
return;
if (GetClipboardSequenceNumber() != sequence)
{
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
return;
}
uint64_t generation = ++m_localGeneration;
if (!generation)
generation = ++m_localGeneration;
m_localSequence = sequence;
PublishClear(generation);
}
void CClipboardManager::RetryLocalClipboard()
{
if (m_hwnd)
KillTimer(m_hwnd, LOCAL_RETRY_TIMER);
if (!m_localRetryDeadline)
return;
if (!m_available)
{
ClearLocalRetry();
return;
}
const DWORD sequence = GetClipboardSequenceNumber();
if (sequence != m_localRetrySequence)
{
ClearLocalRetry();
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
return;
}
if (GetTickCount64() >= m_localRetryDeadline)
{
DEBUG_WARN("Timed out waiting for local clipboard: sequence=%lu",
static_cast<unsigned long>(sequence));
ExpireLocalRetry(sequence);
return;
}
PublishLocalClipboard();
}
void CClipboardManager::ClearRemoteRetry()
{
if (m_hwnd)
KillTimer(m_hwnd, REMOTE_RETRY_TIMER);
m_pendingRemoteOffer = {};
m_remoteRetryDeadline = 0;
}
void CClipboardManager::RetryRemoteOffer()
{
KillTimer(m_hwnd, REMOTE_RETRY_TIMER);
if (!m_pendingRemoteOffer.clipboardGeneration || !m_available)
return;
const KVMFRClipboardMessage offer = m_pendingRemoteOffer;
if (ApplyRemoteOffer(offer.token, offer.clipboardGeneration))
{
ClearRemoteRetry();
return;
}
if (GetTickCount64() < m_remoteRetryDeadline &&
SetTimer(m_hwnd, REMOTE_RETRY_TIMER, REMOTE_RETRY_MS, nullptr))
return;
DEBUG_WARN("Failed to apply remote clipboard offer");
ClearRemoteRetry();
PublishLocalClipboard();
}
bool CClipboardManager::SetOriginMarker(uint64_t generation) const
{
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(ClipboardOrigin));
if (!memory)
return false;
ClipboardOrigin * origin =
static_cast<ClipboardOrigin *>(GlobalLock(memory));
if (!origin)
{
const DWORD error = GetLastError();
GlobalFree(memory);
SetLastError(error ? error : ERROR_NOT_ENOUGH_MEMORY);
return false;
}
*origin = { ORIGIN_MAGIC, 0, m_epoch, generation };
GlobalUnlock(memory);
if (!SetClipboardData(m_formatOrigin, memory))
{
const DWORD error = GetLastError();
GlobalFree(memory);
SetLastError(error ? error : ERROR_INVALID_DATA);
return false;
}
return true;
}
bool CClipboardManager::ApplyRemoteOffer(uint32_t formats,
uint64_t generation)
{
if (m_oleClipboard)
{
if (OleIsCurrentClipboard(m_oleClipboard) == S_OK)
OleSetClipboard(nullptr);
m_oleClipboard->Release();
m_oleClipboard = nullptr;
}
if (FAILED(OpenClipboardRetry("ApplyRemoteOffer")))
return false;
m_applyingRemote = true;
const bool emptied = EmptyClipboard() != FALSE;
DWORD error = emptied ? ERROR_SUCCESS : GetLastError();
if (emptied)
{
auto setDelayed = [&error](UINT format) {
if (!format)
{
if (!error)
error = ERROR_INVALID_DATA;
return false;
}
SetLastError(ERROR_SUCCESS);
const HANDLE result = SetClipboardData(format, nullptr);
if (result || IsClipboardFormatAvailable(format))
return true;
error = GetLastError();
if (!error)
error = ERROR_INVALID_DATA;
return false;
};
bool complete = true;
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TEXT)
complete = setDelayed(CF_UNICODETEXT) && complete;
const bool havePNG =
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG) != 0;
const bool haveBMP =
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP) != 0;
if (havePNG)
complete = setDelayed(m_formatPNG) && complete;
if (haveBMP)
complete = setDelayed(CF_DIB) && complete;
else if (havePNG)
complete = setDelayed(CF_DIBV5) && complete;
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TIFF)
complete = setDelayed(CF_TIFF) && complete;
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_JPEG)
complete = setDelayed(m_formatJPEG) && complete;
if (!SetOriginMarker(generation))
{
if (!error)
error = GetLastError();
if (!error)
error = ERROR_INVALID_DATA;
complete = false;
}
if (complete)
{
m_remoteGeneration = generation;
m_remoteFormats = formats;
}
else
{
EmptyClipboard();
m_remoteGeneration = 0;
m_remoteFormats = 0;
}
}
CloseClipboard();
m_applyingRemote = false;
if (!emptied || error)
{
if (!error)
error = ERROR_INVALID_DATA;
SetLastError(error);
DEBUG_WARN_HR(HRESULT_FROM_WIN32(error),
"Failed to replace the clipboard");
return false;
}
m_ownedSequence = GetClipboardSequenceNumber();
return true;
}
void CClipboardManager::ClearOwnedClipboard()
{
CancelIncoming(ERROR_OPERATION_ABORTED);
if (m_oleClipboard)
{
if (OleIsCurrentClipboard(m_oleClipboard) == S_OK)
OleSetClipboard(nullptr);
m_oleClipboard->Release();
m_oleClipboard = nullptr;
}
if (m_hwnd && GetClipboardOwner() == m_hwnd &&
SUCCEEDED(OpenClipboardRetry("ClearOwnedClipboard")))
{
m_applyingRemote = true;
EmptyClipboard();
CloseClipboard();
m_applyingRemote = false;
}
m_remoteGeneration = 0;
m_remoteFormats = 0;
m_ownedSequence = 0;
}
KVMFRClipboardFormat CClipboardManager::ToWireFormat(UINT format) const
{
if (format == CF_UNICODETEXT)
return KVMFR_CLIPBOARD_FORMAT_TEXT;
if (format == m_formatPNG)
return KVMFR_CLIPBOARD_FORMAT_PNG;
if (format == CF_DIB || format == CF_DIBV5)
return KVMFR_CLIPBOARD_FORMAT_BMP;
if (format == CF_TIFF)
return KVMFR_CLIPBOARD_FORMAT_TIFF;
if (format == m_formatJPEG)
return KVMFR_CLIPBOARD_FORMAT_JPEG;
return KVMFR_CLIPBOARD_FORMAT_NONE;
}
UINT CClipboardManager::ToWindowsFormat(KVMFRClipboardFormat format) const
{
switch (format)
{
case KVMFR_CLIPBOARD_FORMAT_TEXT:
return CF_UNICODETEXT;
case KVMFR_CLIPBOARD_FORMAT_PNG:
return m_formatPNG;
case KVMFR_CLIPBOARD_FORMAT_BMP:
return IsClipboardFormatAvailable(CF_DIBV5) ? CF_DIBV5 : CF_DIB;
case KVMFR_CLIPBOARD_FORMAT_TIFF:
return CF_TIFF;
case KVMFR_CLIPBOARD_FORMAT_JPEG:
return m_formatJPEG;
default:
return 0;
}
}
std::shared_ptr<CClipboardSpool> CClipboardManager::CaptureFormat(
KVMFRClipboardFormat format, DWORD sequence)
{
if (GetClipboardSequenceNumber() != sequence)
{
SetLastError(ERROR_RETRY);
return nullptr;
}
const HRESULT openResult = OpenClipboardRetry("CaptureFormat");
if (FAILED(openResult))
{
const DWORD openError = HRESULT_FACILITY(openResult) == FACILITY_WIN32 ?
static_cast<DWORD>(HRESULT_CODE(openResult)) : ERROR_BUSY;
SetLastError(openError);
return nullptr;
}
if (GetClipboardSequenceNumber() != sequence)
{
CloseClipboard();
SetLastError(ERROR_RETRY);
return nullptr;
}
UINT windowsFormat = ToWindowsFormat(format);
HANDLE handle = windowsFormat ? GetClipboardData(windowsFormat) : nullptr;
if (!handle && format == KVMFR_CLIPBOARD_FORMAT_BMP &&
windowsFormat == CF_DIBV5)
{
windowsFormat = CF_DIB;
handle = GetClipboardData(windowsFormat);
}
if (!handle)
{
const DWORD error = GetLastError();
CloseClipboard();
SetLastError(error ? error : ERROR_NOT_FOUND);
return nullptr;
}
const SIZE_T sourceSize = GlobalSize(handle);
if (sourceSize > MAX_SPOOL_BYTES)
{
CloseClipboard();
SetLastError(ERROR_FILE_TOO_LARGE);
return nullptr;
}
const uint8_t * source = static_cast<const uint8_t *>(GlobalLock(handle));
if (!source || !sourceSize)
{
const DWORD error = GetLastError();
if (source)
GlobalUnlock(handle);
CloseClipboard();
SetLastError(error ? error : ERROR_INVALID_DATA);
return nullptr;
}
std::shared_ptr<CClipboardSpool> spool;
try
{
spool = std::make_shared<CClipboardSpool>();
}
catch (const std::bad_alloc&)
{
GlobalUnlock(handle);
CloseClipboard();
SetLastError(ERROR_OUTOFMEMORY);
return nullptr;
}
SetLastError(ERROR_SUCCESS);
bool success = true;
if (format == KVMFR_CLIPBOARD_FORMAT_TEXT)
{
if (sourceSize % sizeof(wchar_t))
{
SetLastError(ERROR_INVALID_DATA);
success = false;
}
else
{
const wchar_t * text = reinterpret_cast<const wchar_t *>(source);
const size_t capacity = sourceSize / sizeof(wchar_t);
size_t length = 0;
while (length < capacity && text[length])
++length;
std::vector<uint8_t> output;
try
{
output.reserve(TEXT_CONVERSION_CHUNK);
for (size_t index = 0; success && index < length; ++index)
{
uint32_t codepoint = static_cast<uint16_t>(text[index]);
if (codepoint == '\r' && index + 1 < length &&
text[index + 1] == L'\n')
continue;
if (codepoint >= 0xd800 && codepoint <= 0xdbff)
{
if (index + 1 < length)
{
const uint32_t low = static_cast<uint16_t>(text[index + 1]);
if (low >= 0xdc00 && low <= 0xdfff)
{
codepoint = 0x10000 + ((codepoint - 0xd800) << 10) +
(low - 0xdc00);
++index;
}
else
codepoint = 0xfffd;
}
else
codepoint = 0xfffd;
}
else if (codepoint >= 0xdc00 && codepoint <= 0xdfff)
codepoint = 0xfffd;
AppendUTF8(output, codepoint);
if (output.size() >= TEXT_CONVERSION_CHUNK)
{
success = spool->Append(output.data(), output.size());
output.clear();
}
}
}
catch (const std::bad_alloc&)
{
SetLastError(ERROR_OUTOFMEMORY);
success = false;
}
if (success && !output.empty())
success = spool->Append(output.data(), output.size());
}
}
else if (format == KVMFR_CLIPBOARD_FORMAT_BMP)
{
const uint32_t dibOffset = DIBPixelOffset(source, sourceSize);
if (!dibOffset || sourceSize >
(std::numeric_limits<uint32_t>::max)() - sizeof(BITMAPFILEHEADER))
{
SetLastError(sourceSize >
(std::numeric_limits<uint32_t>::max)() - sizeof(BITMAPFILEHEADER) ?
ERROR_FILE_TOO_LARGE : ERROR_INVALID_DATA);
success = false;
}
else
{
BITMAPFILEHEADER header = {};
header.bfType = 0x4d42;
header.bfSize = static_cast<DWORD>(
sizeof(BITMAPFILEHEADER) + sourceSize);
header.bfOffBits = sizeof(BITMAPFILEHEADER) + dibOffset;
success = spool->Append(&header, sizeof(header)) &&
spool->Append(source, sourceSize);
}
}
else
success = spool->Append(source, sourceSize);
DWORD error = success ? ERROR_SUCCESS : GetLastError();
if (!success && !error)
error = ERROR_NOT_ENOUGH_MEMORY;
GlobalUnlock(handle);
if (GetClipboardSequenceNumber() != sequence)
{
CloseClipboard();
SetLastError(ERROR_RETRY);
return nullptr;
}
CloseClipboard();
if (!success)
{
SetLastError(error);
return nullptr;
}
return spool;
}
bool CClipboardManager::MaterializeFormat(KVMFRClipboardFormat format,
UINT windowsFormat, CClipboardSpool& spool)
{
if (format == KVMFR_CLIPBOARD_FORMAT_BMP)
windowsFormat = CF_DIB;
if (!windowsFormat)
{
SetLastError(ERROR_INVALID_DATA);
return false;
}
HGLOBAL memory = nullptr;
if (format == KVMFR_CLIPBOARD_FORMAT_TEXT)
memory = UnicodeFromUTF8(spool);
else if (format == KVMFR_CLIPBOARD_FORMAT_PNG &&
(windowsFormat == CF_DIB || windowsFormat == CF_DIBV5))
memory = PNGToDIBV5(spool);
else if (format == KVMFR_CLIPBOARD_FORMAT_BMP)
{
BITMAPFILEHEADER header = {};
const bool haveHeader = spool.Size() >= sizeof(header) &&
spool.Read(0, &header, sizeof(header));
if (!haveHeader ||
header.bfType != 0x4d42 ||
header.bfOffBits < sizeof(header) ||
header.bfOffBits > spool.Size())
{
if (haveHeader && (header.bfType != 0x4d42 ||
header.bfOffBits < sizeof(header) ||
header.bfOffBits > spool.Size()))
SetLastError(ERROR_INVALID_DATA);
return false;
}
memory = CopySpoolToGlobal(spool, sizeof(header));
}
else
memory = CopySpoolToGlobal(spool, 0);
if (!memory)
return false;
if (!SetClipboardData(windowsFormat, memory))
{
const DWORD error = GetLastError();
GlobalFree(memory);
SetLastError(error ? error : ERROR_INVALID_DATA);
return false;
}
return true;
}
void CClipboardManager::RenderFormat(UINT windowsFormat, uint64_t deadline)
{
KVMFRClipboardFormat format = ToWireFormat(windowsFormat);
if ((windowsFormat == CF_DIB || windowsFormat == CF_DIBV5) &&
!(m_remoteFormats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP) &&
(m_remoteFormats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG))
format = KVMFR_CLIPBOARD_FORMAT_PNG;
if (!m_available || !m_remoteGeneration ||
!kvmfrClipboardRepresentationFormatValid(format) ||
!(m_remoteFormats & kvmfrClipboardFormatFlag(format)))
return;
if (deadline && GetTickCount64() >= deadline)
return;
std::shared_ptr<IncomingTransfer> transfer;
try
{
transfer = std::make_shared<IncomingTransfer>();
transfer->spool = std::make_shared<CClipboardSpool>();
}
catch (const std::bad_alloc&)
{
return;
}
transfer->event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!transfer->event)
return;
if (!deadline)
deadline = GetTickCount64() + RENDER_TIMEOUT_MS;
transfer->generation = m_remoteGeneration;
transfer->format = format;
transfer->transfer = NextHelperTransfer();
{
std::lock_guard<std::mutex> lock(m_transferLock);
if (m_incoming && !m_incoming->complete)
return;
m_incoming = transfer;
}
Work request;
request.type = WorkType::SEND;
request.record.version = KVMFR_CLIPBOARD_VERSION;
request.record.type = KVMFR_CLIPBOARD_MESSAGE_REQUEST;
request.record.clipboardGeneration = transfer->generation;
request.record.transfer = transfer->transfer;
request.record.format = transfer->format;
request.deadline = deadline;
if (!QueueWork(std::move(request)))
CancelIncoming(ERROR_BUSY, transfer->transfer);
const uint64_t now = GetTickCount64();
const DWORD waitMs = now >= deadline ? 0 : static_cast<DWORD>(
(std::min<uint64_t>)(deadline - now, MAXDWORD));
const DWORD wait = WaitForSingleObject(transfer->event, waitMs);
const DWORD waitError = wait == WAIT_FAILED ? GetLastError() :
ERROR_SUCCESS;
bool requestComplete = false;
uint32_t transferError = ERROR_SUCCESS;
{
std::lock_guard<std::mutex> lock(m_transferLock);
requestComplete = transfer->complete;
transferError = transfer->error;
}
if (!requestComplete || transferError != ERROR_SUCCESS)
{
const uint32_t reason = requestComplete ? transferError :
(wait == WAIT_TIMEOUT ? ERROR_TIMEOUT :
(wait == WAIT_FAILED ? waitError : ERROR_INVALID_DATA));
CancelIncoming(reason ? reason : ERROR_OPERATION_ABORTED,
transfer->transfer);
KVMFRClipboardMessage cancel = {};
cancel.clipboardGeneration = transfer->generation;
cancel.transfer = transfer->transfer;
cancel.format = transfer->format;
QueueCancel(cancel, reason ? reason : ERROR_OPERATION_ABORTED);
}
bool render = false;
{
std::lock_guard<std::mutex> lock(m_transferLock);
render = transfer->complete && transfer->error == ERROR_SUCCESS &&
transfer->generation == m_remoteGeneration &&
GetClipboardOwner() == m_hwnd;
if (m_incoming == transfer)
m_incoming.reset();
}
if (render &&
!MaterializeFormat(format, windowsFormat, *transfer->spool))
DEBUG_WARN_HR(GetLastError(),
"Failed to render clipboard format %u as %u", format,
windowsFormat);
}
void CClipboardManager::RenderAllFormats()
{
if (FAILED(OpenClipboardRetry("RenderAllFormats")))
return;
if (GetClipboardOwner() != m_hwnd)
{
CloseClipboard();
return;
}
const uint32_t formats = m_remoteFormats;
const uint64_t deadline = GetTickCount64() + RENDER_TIMEOUT_MS;
const bool havePNG =
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG) != 0;
const bool haveBMP =
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP) != 0;
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TEXT)
RenderFormat(CF_UNICODETEXT, deadline);
if (GetTickCount64() < deadline && (havePNG || haveBMP))
RenderFormat(haveBMP ? CF_DIB : CF_DIBV5, deadline);
if (GetTickCount64() < deadline && havePNG)
RenderFormat(m_formatPNG, deadline);
if (GetTickCount64() < deadline &&
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_TIFF))
RenderFormat(CF_TIFF, deadline);
if (GetTickCount64() < deadline &&
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_JPEG))
RenderFormat(m_formatJPEG, deadline);
CloseClipboard();
}