From 9679c94b91446ca23db18adedc3a0c0932bcd0fd Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 14 Aug 2026 14:42:44 +1000 Subject: [PATCH] [idd] helper: synchronize clipboard files --- idd/LGIddHelper/CClipboardManager.cpp | 1943 ++++++++++++++++++++++++- idd/LGIddHelper/CClipboardManager.h | 111 +- idd/LGIddHelper/LGIddHelper.vcxproj | 2 +- idd/LGIddHelper/main.cpp | 23 + 4 files changed, 2054 insertions(+), 25 deletions(-) diff --git a/idd/LGIddHelper/CClipboardManager.cpp b/idd/LGIddHelper/CClipboardManager.cpp index aa3b0c31..698e699a 100644 --- a/idd/LGIddHelper/CClipboardManager.cpp +++ b/idd/LGIddHelper/CClipboardManager.cpp @@ -21,10 +21,14 @@ #include "CClipboardManager.h" #include +#include +#include #include +#include #include #include +#include #include #include #include @@ -36,13 +40,58 @@ namespace 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(&seeds), static_cast(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 ClipboardDataObjectScope + { + IDataObject * object = nullptr; + ~ClipboardDataObjectScope() { if (object) object->Release(); } + }; + + struct ClipboardStorageScope + { + STGMEDIUM medium = {}; + bool acquired = false; + ~ClipboardStorageScope() + { if (acquired) ReleaseStgMedium(&medium); } + }; + struct ClipboardOrigin { uint32_t magic; @@ -159,6 +208,120 @@ namespace } } +struct CClipboardManager::MarshaledDataObject +{ + std::mutex lock; + IStream * stream = nullptr; + + ~MarshaledDataObject() + { + std::lock_guard guard(lock); + if (stream) + { + CoReleaseMarshalData(stream); + stream->Release(); + } + } + + IStream * Take() + { + std::lock_guard 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 lock(m_lock); + m_accepting = false; + } + + void Detach() + { + std::unique_lock 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 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 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 lock(m_lock); + if (!m_manager || !m_accepting) + return; + manager = m_manager; + ++m_active; + } + try + { + manager->ReleaseRemoteFileDataset(dataset, acquisition); + } + catch (...) + { + } + { + std::lock_guard lock(m_lock); + if (!--m_active) + m_idle.notify_all(); + } + } +}; + class CClipboardSpool { private: @@ -480,6 +643,12 @@ CClipboardManager::IncomingTransfer::~IncomingTransfer() CloseHandle(event); } +CClipboardManager::IncomingFileRequest::~IncomingFileRequest() +{ + if (event) + CloseHandle(event); +} + CClipboardManager::CClipboardManager(HWND hwnd, CClipboardChannel& channel) : m_hwnd(hwnd), m_channel(channel) { @@ -492,17 +661,48 @@ CClipboardManager::~CClipboardManager() 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(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) @@ -542,9 +742,25 @@ void CClipboardManager::Shutdown() 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) @@ -564,9 +780,30 @@ void CClipboardManager::Shutdown() m_recordWorkCount = 0; m_sendWorkCount = 0; } + { + std::lock_guard lock(m_uiLock); + while (m_uiWorkCount) + m_uiWork[--m_uiWorkCount] = UIWork {}; + } m_thread = nullptr; m_wake = nullptr; m_stop = nullptr; + + { + std::lock_guard 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) @@ -577,11 +814,21 @@ DWORD WINAPI CClipboardManager::ThreadProc(void * context) 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, INFINITE); + ARRAYSIZE(events), events, FALSE, RemoteFileManifestWait()); + if (result == WAIT_TIMEOUT) + { + CheckRemoteFileManifestTimeout(); + continue; + } if (result == WAIT_OBJECT_0) return; if (result != WAIT_OBJECT_0 + 1) @@ -637,6 +884,7 @@ void CClipboardManager::Thread() if (!haveWork) break; ProcessWork(std::move(work)); + CheckRemoteFileManifestTimeout(); if (WaitForSingleObject(m_stop, 0) == WAIT_OBJECT_0) return; } @@ -709,6 +957,12 @@ bool CClipboardManager::QueueCancel( 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; { @@ -719,11 +973,16 @@ bool CClipboardManager::QueueCancel( 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)))) + 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]); @@ -840,12 +1099,15 @@ bool CClipboardManager::QueueUI(UIWork&& work) ++i; } } - else if (work.type == UIType::OFFER || work.type == UIType::CLEAR) + 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::CLEAR || + m_uiWork[i].type == UIType::FILE_OFFER || + m_uiWork[i].type == UIType::FILES) erase(i); else ++i; @@ -877,11 +1139,15 @@ bool CClipboardManager::QueueUI(UIWork&& work) same = queued.available == work.available && queued.epoch == work.epoch; else if (same && (work.type == UIType::OFFER || - work.type == UIType::CLEAR)) + 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) + 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) @@ -902,6 +1168,32 @@ bool CClipboardManager::QueueUI(UIWork&& work) return !removed; } +void CClipboardManager::DiscardPendingRemoteUI() +{ + std::array discarded; + size_t discardedCount = 0; + { + std::lock_guard 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) @@ -912,6 +1204,13 @@ void CClipboardManager::ProcessWork(Work&& work) { 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 lock(m_fileLock); + m_localFileAcquisitions.clear(); + m_remoteFileAcquisitions.clear(); + m_outgoingFileRequests.clear(); + PruneLocalFileDatasets(); } UIWork ui; ui.type = UIType::STATE; @@ -925,6 +1224,15 @@ void CClipboardManager::ProcessWork(Work&& work) { 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 lock(m_fileLock); + m_localFileAcquisitions.clear(); + m_remoteFileAcquisitions.clear(); + m_outgoingFileRequests.clear(); + PruneLocalFileDatasets(); + } UIWork ui; ui.type = UIType::STATE; ui.available = false; @@ -953,18 +1261,38 @@ void CClipboardManager::ProcessWork(Work&& work) 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; @@ -977,6 +1305,7 @@ void CClipboardManager::ProcessRecord( { CancelIncoming(ERROR_OPERATION_ABORTED); InvalidateOutgoing(ERROR_OPERATION_ABORTED); + FailRemoteFileManifest(KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED); UIWork ui; ui.type = UIType::CLEAR; ui.record = record; @@ -1089,6 +1418,82 @@ 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 request; + bool manifest = false; + { + std::lock_guard 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 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 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 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; @@ -1124,7 +1529,26 @@ void CClipboardManager::ProcessSend(Work&& work) 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 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( + work.record.clipboardGeneration), + work.record.token, roots); + } return; + } const KVMFRClipboardMessageType publicationType = work.record.type; const uint64_t publicationGeneration = work.record.clipboardGeneration; @@ -1145,6 +1569,8 @@ void CClipboardManager::ProcessSend(Work&& work) { 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; @@ -1162,8 +1588,15 @@ void CClipboardManager::ProcessSend(Work&& work) if (live && !queued) CancelIncoming(ERROR_BUSY, record.transfer); } - else if (!QueueWork(std::move(work))) - failLocalPublication(); + else + { + const KVMFRClipboardMessage record = work.record; + if (!QueueWork(std::move(work))) + { + failLocalPublication(); + failFileControl(record, KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE); + } + } return; } @@ -1174,7 +1607,13 @@ void CClipboardManager::ProcessSend(Work&& work) 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) @@ -1318,6 +1757,1053 @@ void CClipboardManager::ProcessSendData(Work&& work) } } +bool CClipboardManager::QueueFileCancel( + const KVMFRClipboardMessage& record, KVMFRClipboardFileError error) +{ + if (!error) + error = KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED; + { + std::lock_guard 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 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; + } + const size_t length = static_cast((std::min)( + KVMFR_CLIPBOARD_DATA_BYTES, total - work.record.offset)); + KVMFRClipboardMessage message = work.record; + message.version = KVMFR_CLIPBOARD_VERSION; + message.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA; + message.length = static_cast(length); + message.flags = 0; + if (!message.offset) + message.flags |= KVMFR_CLIPBOARD_FLAG_BEGIN; + if (message.offset + length == total) + message.flags |= KVMFR_CLIPBOARD_FLAG_END; + message.size = message.flags & KVMFR_CLIPBOARD_FLAG_END ? total : + (message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN ? total : + KVMFR_CLIPBOARD_SIZE_UNKNOWN); + + const uint8_t * data = length ? + work.fileData->data() + static_cast(message.offset) : nullptr; + const ClipboardChannelResult result = m_channel.Send(message, data); + if (result == ClipboardChannelResult::BUSY && + GetTickCount64() < work.deadline && + WaitForSingleObject(m_stop, CHANNEL_RETRY_MS) != WAIT_OBJECT_0) + { + if (!QueueWork(std::move(work))) + QueueFileCancel(message, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return; + } + if (result != ClipboardChannelResult::ACCEPTED) + { + QueueFileCancel(message, + result == ClipboardChannelResult::BUSY ? + KVMFR_CLIPBOARD_FILE_ERROR_IO : + KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED); + return; + } + + if (message.flags & KVMFR_CLIPBOARD_FLAG_END) + { + std::lock_guard lock(m_fileLock); + m_outgoingFileRequests.erase(message.transfer); + return; + } + work.record.offset += length; + ++work.record.sequence; + work.deadline = GetTickCount64() + SEND_TIMEOUT_MS; + if (!QueueWork(std::move(work))) + QueueFileCancel(message, 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 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 incoming; + bool manifest = false; + bool acquisition = false; + { + std::lock_guard 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 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 winError=%lu", + static_cast(record.clipboardGeneration), + static_cast(record.transfer), + static_cast(error), + static_cast(ERROR_SUCCESS)); + else + DEBUG_INFO("Local clipboard file acquire accepted: dataset=%llu " + "acquisition=%llu", + static_cast(record.clipboardGeneration), + static_cast(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 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 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 dataset; + KVMFRClipboardFileError error = KVMFR_CLIPBOARD_FILE_ERROR_NONE; + { + std::lock_guard 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 winError=%lu", + static_cast(record.clipboardGeneration), + static_cast(record.transfer), + static_cast(failure), + static_cast(ERROR_SUCCESS)); + QueueFileCancel(record, failure); + return; + } + + std::shared_ptr> data; + try + { + data = std::make_shared>(); + } + catch (const std::bad_alloc&) + { + if (initialList) + DEBUG_WARN("Initial local clipboard file LIST allocation failed: " + "dataset=%llu request=%llu fileError=%u winError=%lu", + static_cast(record.clipboardGeneration), + static_cast(record.transfer), + static_cast(KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY), + static_cast(ERROR_OUTOFMEMORY)); + QueueFileCancel(record, KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY); + return; + } + bool success = false; + SetLastError(ERROR_SUCCESS); + 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; + SetLastError(ERROR_OUTOFMEMORY); + } + const DWORD winError = success ? ERROR_SUCCESS : GetLastError(); + if (!success) + { + if (initialList) + DEBUG_WARN("Initial local clipboard file LIST failed: dataset=%llu " + "request=%llu fileError=%u winError=%lu", + static_cast(record.clipboardGeneration), + static_cast(record.transfer), + static_cast(error), + static_cast(winError)); + QueueFileCancel(record, error); + return; + } + if (initialList) + DEBUG_INFO("Initial local clipboard file LIST completed: dataset=%llu " + "request=%llu bytes=%zu", + static_cast(record.clipboardGeneration), + static_cast(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 request; + bool finishManifest = false; + KVMFRClipboardFileError cancel = KVMFR_CLIPBOARD_FILE_ERROR_NONE; + { + std::lock_guard 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) + { + 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 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(); + 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 request; + try + { + request = std::make_shared(); + } + 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 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& 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 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 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 marshaled; + try + { + marshaled = std::make_shared(); + } + 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 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( + 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((std::min)(remaining, + static_cast(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 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; + uint8_t * destination = static_cast(output); + while (length) + { + const uint32_t wanted = static_cast((std::min)(length, + KVMFR_CLIPBOARD_FILE_READ_BYTES)); + std::shared_ptr request; + try + { + request = std::make_shared(); + } + 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; + KVMFRClipboardFileError insertError = + KVMFR_CLIPBOARD_FILE_ERROR_NONE; + { + std::lock_guard 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 lock(m_fileLock); + m_incomingFileRequests.erase(request->transfer); + return STG_E_READFAULT; + } + + bool timedOut = false; + for (;;) + { + { + std::lock_guard 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 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->data.size() > wanted) + return STG_E_READFAULT; + if (!request->data.empty()) + memcpy(destination, request->data.data(), request->data.size()); + const ULONG actual = static_cast(request->data.size()); + 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 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 lock(m_fileLock); + m_localFileDataset = 0; + PruneLocalFileDatasets(); +} + void CClipboardManager::CancelIncoming(uint32_t reason, uint64_t transferID) { std::lock_guard lock(m_transferLock); @@ -1364,7 +2850,8 @@ ClipboardChannelResult CClipboardManager::ClipboardRecord( case KVMFR_CLIPBOARD_MESSAGE_REQUEST: if (!record.clipboardGeneration || !record.transfer || !kvmfrClipboardTransferFromClient(record.transfer) || - !kvmfrClipboardFormatValid(record.format) || record.length || + !kvmfrClipboardRepresentationFormatValid(record.format) || + record.length || record.flags || record.offset || record.size || record.sequence || record.token) return ClipboardChannelResult::FAILED; @@ -1373,13 +2860,36 @@ ClipboardChannelResult CClipboardManager::ClipboardRecord( case KVMFR_CLIPBOARD_MESSAGE_DATA: if (!record.clipboardGeneration || !record.transfer || !kvmfrClipboardTransferFromHelper(record.transfer) || - !kvmfrClipboardFormatValid(record.format) || + !kvmfrClipboardRepresentationFormatValid(record.format) || (record.length && !data)) return ClipboardChannelResult::FAILED; break; case KVMFR_CLIPBOARD_MESSAGE_CANCEL: - if (!record.transfer || record.length || record.flags) + 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)) || + (record.length && !data)) return ClipboardChannelResult::FAILED; break; default: @@ -1437,6 +2947,12 @@ bool CClipboardManager::HandleMessage(UINT message, WPARAM wParam, return true; case WM_TIMER: + if (wParam == LOCAL_RETRY_TIMER) + { + RetryLocalClipboard(); + result = 0; + return true; + } if (wParam == REMOTE_RETRY_TIMER) { RetryRemoteOffer(); @@ -1481,6 +2997,12 @@ void CClipboardManager::DrainUI() case UIType::REQUEST: HandleRequest(work.record); break; + case UIType::FILE_OFFER: + HandlePendingFileOffer(work.record); + break; + case UIType::FILES: + HandleFileDataObject(work); + break; } } } @@ -1491,6 +3013,7 @@ void CClipboardManager::HandleState(bool available, uint64_t epoch) m_epoch = available ? epoch : 0; if (!available) { + ClearLocalRetry(); ClearRemoteRetry(); InvalidateLocalClipboard(ERROR_DEVICE_NOT_CONNECTED); ClearOwnedClipboard(); @@ -1510,6 +3033,7 @@ void CClipboardManager::HandleOffer( // 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; @@ -1520,6 +3044,7 @@ void CClipboardManager::HandleOffer( void CClipboardManager::HandleClear( const KVMFRClipboardMessage&) { + ClearLocalRetry(); ClearRemoteRetry(); InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); ClearOwnedClipboard(); @@ -1529,7 +3054,7 @@ void CClipboardManager::HandleRequest( const KVMFRClipboardMessage& record) { if (!m_available || !record.transfer || - !kvmfrClipboardFormatValid(record.format) || + !kvmfrClipboardRepresentationFormatValid(record.format) || record.clipboardGeneration != m_localGeneration || record.clipboardGeneration != Atomic::Load( m_liveLocalGeneration, std::memory_order_acquire) || @@ -1576,6 +3101,55 @@ void CClipboardManager::HandleRequest( } } +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(&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(); +} + bool CClipboardManager::OpenClipboardRetry() const { for (unsigned int attempt = 0; attempt != 8; ++attempt) @@ -1589,6 +3163,11 @@ bool CClipboardManager::OpenClipboardRetry() const bool CClipboardManager::IsOurClipboard() { + if (m_oleClipboard && OleIsCurrentClipboard(m_oleClipboard) == S_OK) + { + m_ownedSequence = GetClipboardSequenceNumber(); + return true; + } if (GetClipboardOwner() == m_hwnd) { m_ownedSequence = GetClipboardSequenceNumber(); @@ -1630,20 +3209,153 @@ uint32_t CClipboardManager::EnumerateFormats() const 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 +CClipboardManager::CaptureClipboardFiles(DWORD sequence, bool& viaOLE, + bool& openedOLE, KVMFRClipboardFileError& error, DWORD& winError, + HRESULT& oleError) +{ + viaOLE = false; + openedOLE = false; + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + winError = ERROR_SUCCESS; + oleError = S_FALSE; + + const bool win32Files = IsClipboardFormatAvailable(CF_HDROP) != FALSE; + if (win32Files) + { + if (GetClipboardSequenceNumber() != sequence) + { + winError = ERROR_RETRY; + return nullptr; + } + if (OpenClipboardRetry()) + { + std::shared_ptr files; + if (GetClipboardSequenceNumber() == sequence) + { + SetLastError(ERROR_SUCCESS); + HDROP drop = static_cast(GetClipboardData(CF_HDROP)); + if (drop) + { + SetLastError(ERROR_SUCCESS); + files = CLocalClipboardFiles::Capture(drop, error); + if (!files) + winError = GetLastError(); + } + else + { + winError = GetLastError(); + if (!winError) + winError = ERROR_INVALID_DATA; + } + } + else + winError = ERROR_RETRY; + CloseClipboard(); + if (files) + return files; + } + else + { + winError = GetLastError(); + error = winError == ERROR_ACCESS_DENIED ? + KVMFR_CLIPBOARD_FILE_ERROR_ACCESS : + KVMFR_CLIPBOARD_FILE_ERROR_IO; + } + } + + if (GetClipboardSequenceNumber() != sequence) + { + if (!winError) + winError = ERROR_RETRY; + return nullptr; + } + + ClipboardDataObjectScope object; + oleError = OleGetClipboard(&object.object); + if (FAILED(oleError) || !object.object) + { + if (SUCCEEDED(oleError)) + oleError = E_UNEXPECTED; + return nullptr; + } + openedOLE = true; + + FORMATETC format = { + static_cast(CF_HDROP), + nullptr, + DVASPECT_CONTENT, + -1, + TYMED_HGLOBAL, + }; + ClipboardStorageScope storage; + oleError = object.object->GetData(&format, &storage.medium); + if (FAILED(oleError)) + return nullptr; + storage.acquired = true; + + std::shared_ptr files; + if (storage.medium.tymed == TYMED_HGLOBAL && storage.medium.hGlobal) + { + SetLastError(ERROR_SUCCESS); + try + { + files = CLocalClipboardFiles::Capture( + static_cast(storage.medium.hGlobal), error); + if (!files) + winError = GetLastError(); + } + catch (const std::bad_alloc&) + { + winError = ERROR_OUTOFMEMORY; + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + } + catch (const std::length_error&) + { + winError = ERROR_OUTOFMEMORY; + error = KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY; + } + } + else + { + oleError = DV_E_TYMED; + error = KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + } + if (files) + viaOLE = true; + 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(); } @@ -1651,18 +3363,28 @@ 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(); @@ -1670,18 +3392,104 @@ void CClipboardManager::PublishLocalClipboard() } const DWORD before = GetClipboardSequenceNumber(); - const uint32_t formats = EnumerateFormats(); + uint32_t formats = EnumerateFormats(); + const uint32_t recognizedFormats = formats; + const int rawFormatCount = CountClipboardFormats(); const DWORD after = GetClipboardSequenceNumber(); if (before != after) { + ClearLocalRetry(); PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0); return; } + KVMFRClipboardFileError fileError = + KVMFR_CLIPBOARD_FILE_ERROR_INVALID; + DWORD fileWinError = ERROR_SUCCESS; + HRESULT fileOleError = S_FALSE; + bool fileViaOLE = false; + bool fileOpenedOLE = false; + std::shared_ptr files = CaptureClipboardFiles( + after, fileViaOLE, fileOpenedOLE, fileError, fileWinError, + fileOleError); + if (GetClipboardSequenceNumber() != after) + { + ClearLocalRetry(); + PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0); + return; + } + if (fileOleError == CLIPBRD_E_CANT_OPEN) + { + DeferLocalClipboard(after, fileOpenedOLE); + 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 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(after), + fileViaOLE ? "ole" : "win32", roots); + } + else + { + DEBUG_WARN("Failed to retain local clipboard file selection: " + "sequence=%lu fileError=%u winError=%lu", + static_cast(after), + static_cast(KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY), + static_cast(ERROR_OUTOFMEMORY)); + formats &= ~KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + } + } + else + { + formats &= ~KVMFR_CLIPBOARD_FORMAT_MASK_FILES; + if ((recognizedFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) || + SUCCEEDED(fileOleError)) + { + DEBUG_WARN("Failed to capture local clipboard file selection: " + "sequence=%lu fileError=%u winError=%lu oleError=0x%08lx", + static_cast(after), + static_cast(fileError), + static_cast(fileWinError), + static_cast(fileOleError)); + } + else if (FAILED(fileOleError) && fileOleError != DV_E_FORMATETC && + fileOleError != DATA_E_FORMATETC) + { + DEBUG_TRACE_HR(fileOleError, + "OLE clipboard does not expose local files: sequence=%lu " + "rawFormats=%d", static_cast(after), + rawFormatCount); + } + } + DEBUG_INFO("Local clipboard update: sequence=%lu rawFormats=%d " + "recognized=0x%08x effective=0x%08x", + static_cast(after), rawFormatCount, + recognizedFormats, formats); if (!formats) { PublishClear(generation); @@ -1752,9 +3560,92 @@ void CClipboardManager::InvalidateOutgoing(uint32_t 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, bool openedOLE) +{ + 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(sequence), + openedOLE ? "IDataObject::GetData" : "OleGetClipboard"); + } + + 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(sequence)); + else + DEBUG_WARN("Failed to schedule local clipboard retry: sequence=%lu", + static_cast(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(sequence)); + ExpireLocalRetry(sequence); + return; + } + + PublishLocalClipboard(); +} + void CClipboardManager::ClearRemoteRetry() { if (m_hwnd) @@ -1813,6 +3704,13 @@ bool CClipboardManager::SetOriginMarker(uint64_t generation) const 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 (!OpenClipboardRetry()) { DEBUG_WARN_HR(GetLastError(), "Failed to open the clipboard"); @@ -1891,6 +3789,13 @@ bool CClipboardManager::ApplyRemoteOffer(uint32_t formats, 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 && OpenClipboardRetry()) { m_applyingRemote = true; @@ -2162,7 +4067,7 @@ void CClipboardManager::RenderFormat(UINT windowsFormat, uint64_t deadline) { const KVMFRClipboardFormat format = ToWireFormat(windowsFormat); if (!m_available || !m_remoteGeneration || - !kvmfrClipboardFormatValid(format) || + !kvmfrClipboardRepresentationFormatValid(format) || !(m_remoteFormats & kvmfrClipboardFormatFlag(format))) return; if (deadline && GetTickCount64() >= deadline) @@ -2186,15 +4091,7 @@ void CClipboardManager::RenderFormat(UINT windowsFormat, uint64_t deadline) transfer->generation = m_remoteGeneration; transfer->format = format; - transfer->transfer = Atomic::FetchAdd(m_nextTransfer, UINT64_C(1), - std::memory_order_relaxed); - if (!kvmfrClipboardTransferFromHelper(transfer->transfer)) - { - Atomic::Store(m_nextTransfer, - KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(2), - std::memory_order_relaxed); - transfer->transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1); - } + transfer->transfer = NextHelperTransfer(); { std::lock_guard lock(m_transferLock); diff --git a/idd/LGIddHelper/CClipboardManager.h b/idd/LGIddHelper/CClipboardManager.h index 26a5b2ad..bc4756fd 100644 --- a/idd/LGIddHelper/CClipboardManager.h +++ b/idd/LGIddHelper/CClipboardManager.h @@ -21,16 +21,20 @@ #pragma once #include "CClipboardChannel.h" +#include "CClipboardFiles.h" #include #include #include #include +#include #include #include #include #include +#include +#include #include class CClipboardSpool; @@ -38,6 +42,9 @@ class CClipboardSpool; class CClipboardManager final : private IClipboardChannelHandler { private: + struct MarshaledDataObject; + class RemoteFileProvider; + enum class WorkType { STATE, @@ -45,6 +52,8 @@ private: RECORD, SEND, SEND_DATA, + SEND_FILE_DATA, + CANCEL_FILE_MANIFEST, }; enum class UIType @@ -53,6 +62,8 @@ private: OFFER, CLEAR, REQUEST, + FILE_OFFER, + FILES, }; struct Work @@ -64,6 +75,7 @@ private: KVMFRClipboardMessage record = {}; std::vector data; std::shared_ptr spool; + std::shared_ptr> fileData; uint64_t deadline = 0; }; @@ -73,6 +85,7 @@ private: bool available = false; uint64_t epoch = 0; KVMFRClipboardMessage record = {}; + std::shared_ptr dataObject; }; struct PendingCancel @@ -110,13 +123,55 @@ private: ~IncomingTransfer(); }; + struct IncomingFileRequest + { + uint64_t dataset = 0; + uint64_t transfer = 0; + uint64_t node = 0; + KVMFRClipboardFileOperation operation = 0; + uint64_t nextOffset = 0; + uint32_t nextSequence = 0; + uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + uint32_t requestedBytes = 0; + bool began = false; + bool complete = false; + bool manifest = false; + KVMFRClipboardFileError error = KVMFR_CLIPBOARD_FILE_ERROR_NONE; + HANDLE event = nullptr; + std::vector data; + + ~IncomingFileRequest(); + }; + + struct RemoteFileManifest + { + uint64_t dataset = 0; + uint64_t acquisition = 0; + uint64_t currentRequest = 0; + uint64_t currentParent = 0; + uint64_t deadline = 0; + KVMFRClipboardMessage offer = {}; + std::deque directories; + std::vector entries; + std::unordered_set nodes; + }; + + struct OutgoingFileRequest + { + uint64_t dataset = 0; + bool cancelled = false; + }; + static constexpr UINT WM_CLIPBOARD_WORK = WM_APP + 0x4c; static constexpr size_t MAX_WORK = 16; static constexpr size_t MAX_UI_WORK = 16; - static constexpr size_t MAX_PENDING_CANCEL = 8; + static constexpr size_t MAX_PENDING_CANCEL = + KVMFR_CLIPBOARD_FILE_MAX_REQUESTS + + KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS + 1U; static constexpr size_t MAX_PENDING_CONTROL = 16; static constexpr DWORD RENDER_TIMEOUT_MS = 15000; static constexpr DWORD SEND_TIMEOUT_MS = RENDER_TIMEOUT_MS; + static constexpr DWORD FILE_MANIFEST_TIMEOUT_MS = 30000; HWND m_hwnd; CClipboardChannel& m_channel; @@ -142,6 +197,20 @@ private: std::shared_ptr m_incoming; std::mutex m_outgoingLock; + std::mutex m_fileLock; + std::unordered_map> + m_localFileDatasets; + std::unordered_map m_localFileAcquisitions; + std::unordered_map> + m_incomingFileRequests; + std::unordered_map m_remoteFileAcquisitions; + std::unordered_map m_outgoingFileRequests; + std::unique_ptr m_remoteFileManifest; + std::shared_ptr m_remoteFileProvider; + uint64_t m_localFileDataset = 0; + IDataObject * m_oleClipboard = nullptr; + bool m_oleInitialized = false; + UINT m_formatPNG = 0; UINT m_formatJPEG = 0; UINT m_formatOrigin = 0; @@ -152,6 +221,8 @@ private: uint64_t m_epoch = 0; uint64_t m_localGeneration = 0; DWORD m_localSequence = 0; + DWORD m_localRetrySequence = 0; + uint64_t m_localRetryDeadline = 0; uint64_t m_remoteGeneration = 0; uint32_t m_remoteFormats = 0; DWORD m_ownedSequence = 0; @@ -166,11 +237,14 @@ private: void Thread(); bool QueueWork(Work&& work); + bool QueueCancellation(const KVMFRClipboardMessage& record, + uint64_t deadline); bool QueueCancel(const KVMFRClipboardMessage& record, uint32_t reason, uint64_t deadline = 0); void QueueControl(WorkType type, bool available, uint64_t epoch, uint32_t reason); bool QueueUI(UIWork&& work); + void DiscardPendingRemoteUI(); void DrainUI(); void ProcessWork(Work&& work); void ProcessRecord(const KVMFRClipboardMessage& record, @@ -179,6 +253,32 @@ private: const uint8_t * data); void ProcessSend(Work&& work); void ProcessSendData(Work&& work); + void ProcessSendFileData(Work&& work); + void ProcessFileRecord(const KVMFRClipboardMessage& record, + const uint8_t * data); + void ProcessFileData(const KVMFRClipboardMessage& record, + const uint8_t * data); + bool QueueFileCancel(const KVMFRClipboardMessage& record, + KVMFRClipboardFileError error); + void HandleFileAcquire(const KVMFRClipboardMessage& record); + void HandleFileRelease(const KVMFRClipboardMessage& record); + void HandleFileRequest(const KVMFRClipboardMessage& record); + void StartRemoteFileOffer(const KVMFRClipboardMessage& record); + void StartRemoteFileList(uint64_t parent); + void ContinueRemoteFileManifest( + const std::shared_ptr& request); + void FinishRemoteFileManifest(); + void FailRemoteFileManifest(KVMFRClipboardFileError error); + DWORD RemoteFileManifestWait() const; + void CheckRemoteFileManifestTimeout(); + void CancelFileRequests(KVMFRClipboardFileError error, + uint64_t transfer = 0); + HRESULT ReadRemoteFile(uint64_t dataset, uint64_t acquisition, + uint64_t node, uint64_t offset, void * data, ULONG length, ULONG& read); + void ReleaseRemoteFileDataset(uint64_t dataset, uint64_t acquisition); + uint64_t NextHelperTransfer(); + void PruneLocalFileDatasets(); + void RetireLocalFileDataset(); void CancelIncoming(uint32_t reason, uint64_t transfer = 0); void ReleaseOutgoing(uint64_t transfer); @@ -186,21 +286,30 @@ private: void HandleOffer(const KVMFRClipboardMessage& record); void HandleClear(const KVMFRClipboardMessage& record); void HandleRequest(const KVMFRClipboardMessage& record); + void HandlePendingFileOffer(const KVMFRClipboardMessage& record); + void HandleFileDataObject(UIWork& work); void HandleClipboardUpdate(); void HandleDestroyClipboard(); void RenderFormat(UINT format, uint64_t deadline = 0); void RenderAllFormats(); + void RetryLocalClipboard(); void RetryRemoteOffer(); bool OpenClipboardRetry() const; bool IsOurClipboard(); uint32_t EnumerateFormats() const; + std::shared_ptr CaptureClipboardFiles( + DWORD sequence, bool& viaOLE, bool& openedOLE, + KVMFRClipboardFileError& error, DWORD& winError, HRESULT& oleError); void PublishLocalClipboard(); void PublishClear(uint64_t generation); bool ApplyRemoteOffer(uint32_t formats, uint64_t generation); void ClearOwnedClipboard(); void InvalidateOutgoing(uint32_t reason); void InvalidateLocalClipboard(uint32_t reason); + void ClearLocalRetry(); + void DeferLocalClipboard(DWORD sequence, bool openedOLE); + void ExpireLocalRetry(DWORD sequence); void ClearRemoteRetry(); bool SetOriginMarker(uint64_t generation) const; diff --git a/idd/LGIddHelper/LGIddHelper.vcxproj b/idd/LGIddHelper/LGIddHelper.vcxproj index 87f6ed0a..8ae4b0d5 100644 --- a/idd/LGIddHelper/LGIddHelper.vcxproj +++ b/idd/LGIddHelper/LGIddHelper.vcxproj @@ -188,7 +188,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd StdCall - %(AdditionalDependencies);Ole32.lib;Shell32.lib;Wtsapi32.lib + %(AdditionalDependencies);Bcrypt.lib;Ole32.lib;Shell32.lib;Wtsapi32.lib diff --git a/idd/LGIddHelper/main.cpp b/idd/LGIddHelper/main.cpp index 416b3366..7343e7aa 100644 --- a/idd/LGIddHelper/main.cpp +++ b/idd/LGIddHelper/main.cpp @@ -19,6 +19,7 @@ */ #include +#include #include #include @@ -58,6 +59,11 @@ static HandleT l_childStopEvent; static DWORD l_desiredSession = NO_CONSOLE_SESSION; static DWORD l_childSession = NO_CONSOLE_SESSION; +struct OleScope +{ + ~OleScope() { OleUninitialize(); } +}; + static bool Launch(DWORD sessionId); static bool StopChild(); @@ -122,6 +128,23 @@ int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _ } } + const HRESULT ole = OleInitialize(nullptr); + if (FAILED(ole)) + { + DEBUG_ERROR_HR(ole, "Failed to initialize OLE"); + return EXIT_FAILURE; + } + const OleScope oleScope; + + const HRESULT security = CoInitializeSecurity(nullptr, 0, nullptr, nullptr, + RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IDENTIFY, nullptr, EOAC_NONE, + nullptr); + if (FAILED(security)) + { + DEBUG_ERROR_HR(security, "Failed to initialize COM security"); + return EXIT_FAILURE; + } + if (!CNotifyWindow::registerClass()) { DEBUG_ERROR("Failed to register message window class");