diff --git a/common/include/common/KVMFRClipboard.h b/common/include/common/KVMFRClipboard.h index 5b5193ec..e5ed2b7c 100644 --- a/common/include/common/KVMFRClipboard.h +++ b/common/include/common/KVMFRClipboard.h @@ -31,6 +31,21 @@ #define KVMFR_CLIPBOARD_DATA_BYTES (64U * 1024U) #define KVMFR_CLIPBOARD_SIZE_UNKNOWN UINT64_MAX +/* Transfer IDs are selected by the side which sends REQUEST. Keeping the + * namespaces disjoint makes a simultaneous transfer in each direction + * unambiguous when CANCEL and DATA cross in flight. */ +#define KVMFR_CLIPBOARD_TRANSFER_HELPER (UINT64_C(1) << 63) + +static inline int kvmfrClipboardTransferFromHelper(uint64_t transfer) +{ + return (transfer & KVMFR_CLIPBOARD_TRANSFER_HELPER) != 0; +} + +static inline int kvmfrClipboardTransferFromClient(uint64_t transfer) +{ + return transfer != 0 && !kvmfrClipboardTransferFromHelper(transfer); +} + enum { KVMFR_CLIPBOARD_FORMAT_NONE = 0, @@ -120,6 +135,14 @@ typedef struct KVMFRClipboardMessage } KVMFRClipboardMessage; +/* Type-specific fields: + * OFFER: token is KVMFRClipboardFormatFlags. + * REQUEST: format and transfer identify the requested representation. + * DATA: size is an optional total hint on BEGIN and authoritative on END; + * offset/length describe this record's borrowed payload. + * CANCEL: token is a KVMFRClipboardCancelReason-compatible reason. + * ACK/GRANT: token identifies the acknowledged or writable slot. */ + enum { KVMFR_CLIPBOARD_STATUS_AVAILABLE = 1U << 0, diff --git a/idd/LGCommon/CClipboardChannel.cpp b/idd/LGCommon/CClipboardChannel.cpp new file mode 100644 index 00000000..f4e457d7 --- /dev/null +++ b/idd/LGCommon/CClipboardChannel.cpp @@ -0,0 +1,578 @@ +/** + * 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 "CClipboardChannel.h" + +#include "CClipboardRing.h" +#include "CDebug.h" + +#include +#include +#include + +namespace +{ + static constexpr DWORD WAIT_FIRST_OBJECT_VALUE = 0; + + struct ClipboardCallbackScope + { + CClipboardChannel * channel; + ClipboardCallbackScope * previous; + }; + + thread_local ClipboardCallbackScope * g_callbackScope = nullptr; + + bool InClipboardCallback(CClipboardChannel * channel) + { + for (ClipboardCallbackScope * scope = g_callbackScope; + scope; scope = scope->previous) + if (scope->channel == channel) + return true; + return false; + } + + class CClipboardCallbackScope + { + private: + ClipboardCallbackScope m_scope; + + public: + explicit CClipboardCallbackScope(CClipboardChannel * channel) : + m_scope { channel, g_callbackScope } + { + g_callbackScope = &m_scope; + } + + ~CClipboardCallbackScope() + { + g_callbackScope = m_scope.previous; + } + }; + + bool ValidRecord(const KVMFRClipboardMessage& record, + bool dataPresent) + { + if (record.version != KVMFR_CLIPBOARD_VERSION || + record.length > KVMFR_CLIPBOARD_DATA_BYTES || + (record.length != 0) != dataPresent) + return false; + + switch (record.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + return record.clipboardGeneration && record.token && + !(record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) && + !record.transfer && !record.offset && !record.size && + !record.format && !record.flags && !record.length && + !record.sequence; + + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + return record.clipboardGeneration && !record.token && + !record.transfer && !record.offset && !record.size && + !record.format && !record.flags && !record.length && + !record.sequence; + + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + return record.clipboardGeneration && record.transfer && + kvmfrClipboardFormatValid(record.format) && !record.offset && + !record.size && !record.flags && !record.token && + !record.length && !record.sequence; + + case KVMFR_CLIPBOARD_MESSAGE_DATA: + { + if (!record.clipboardGeneration || !record.transfer || + !kvmfrClipboardFormatValid(record.format) || record.token || + (record.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END)) || + (!record.length && !(record.flags & KVMFR_CLIPBOARD_FLAG_END)) || + record.offset > UINT64_MAX - record.length) + return false; + const uint64_t end = record.offset + record.length; + if (record.flags & KVMFR_CLIPBOARD_FLAG_END) + return record.size == end; + if (!(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) + return record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN; + return record.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN || + record.size >= end; + } + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + return record.transfer && !record.offset && !record.size && + (!record.format || kvmfrClipboardFormatValid(record.format)) && + !record.flags && !record.length && !record.sequence; + + default: + return false; + } + } +} + +CClipboardChannel::~CClipboardChannel() +{ + Detach(); +} + +bool CClipboardChannel::Attach(HANDLE mapping, uint64_t epoch, + bool helper, IClipboardChannelDoorbell& doorbell) +{ + if (!mapping || mapping == INVALID_HANDLE_VALUE || !epoch) + { + if (mapping && mapping != INVALID_HANDLE_VALUE) + CloseHandle(mapping); + return false; + } + + Detach(); + + CSRWExclusiveLock lock(m_lifecycleLock); + ClipboardMapping * view = static_cast(MapViewOfFile( + mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(ClipboardMapping))); + if (!view) + { + DEBUG_ERROR_HR(GetLastError(), "Failed to map clipboard channel"); + CloseHandle(mapping); + return false; + } + if (!CClipboardRing::Valid(*view, epoch)) + { + DEBUG_ERROR("Invalid clipboard mapping"); + UnmapViewOfFile(view); + CloseHandle(mapping); + return false; + } + + m_stop = CreateEventW(nullptr, TRUE, FALSE, nullptr); + m_kick = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!m_stop || !m_kick) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to create clipboard channel events"); + if (m_kick) + CloseHandle(m_kick); + if (m_stop) + CloseHandle(m_stop); + m_kick = nullptr; + m_stop = nullptr; + UnmapViewOfFile(view); + CloseHandle(mapping); + return false; + } + + m_mapping = mapping; + m_view = view; + m_in = helper ? &view->iddToHelper : &view->helperToIdd; + m_out = helper ? &view->helperToIdd : &view->iddToHelper; + m_epoch = epoch; + ++m_instance; + if (!m_instance) + ++m_instance; + m_doorbell = &doorbell; + Atomic::Store(m_available, true, std::memory_order_release); + + m_thread = CreateThread( + nullptr, 0, ThreadProc, this, 0, &m_threadId); + if (!m_thread) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to create clipboard channel worker"); + Atomic::Store(m_available, false, std::memory_order_release); + m_doorbell = nullptr; + m_epoch = 0; + m_out = nullptr; + m_in = nullptr; + m_threadId = 0; + m_view = nullptr; + m_mapping = nullptr; + CloseHandle(m_kick); + CloseHandle(m_stop); + UnmapViewOfFile(view); + CloseHandle(mapping); + m_kick = nullptr; + m_stop = nullptr; + return false; + } + + lock.Unlock(); + PublishState(true, epoch); + return true; +} + +void CClipboardChannel::Detach() +{ + HANDLE thread; + uint64_t epoch; + bool available; + { + CSRWExclusiveLock lock(m_lifecycleLock); + available = Atomic::Swap( + m_available, false, std::memory_order_acq_rel); + epoch = m_epoch; + thread = m_thread; + if (m_stop) + SetEvent(m_stop); + if (thread && InClipboardCallback(this)) + m_deferredDetach = true; + } + + // A callback may run on the worker or while the worker waits for the + // callback-quiescence lock. Let the worker release channel resources after + // this callback returns instead of waiting on it here. + if (thread && InClipboardCallback(this)) + { + if (available) + PublishState(false, epoch); + return; + } + + if (thread) + WaitForSingleObject(thread, INFINITE); + + CSRWExclusiveLock lock(m_lifecycleLock); + if (m_thread) + CloseHandle(m_thread); + if (m_kick) + CloseHandle(m_kick); + if (m_stop) + CloseHandle(m_stop); + if (m_view) + UnmapViewOfFile(m_view); + if (m_mapping) + CloseHandle(m_mapping); + + m_thread = nullptr; + m_threadId = 0; + m_kick = nullptr; + m_stop = nullptr; + m_in = nullptr; + m_out = nullptr; + m_view = nullptr; + m_mapping = nullptr; + m_doorbell = nullptr; + m_epoch = 0; + m_deferredDetach = false; + lock.Unlock(); + if (available) + PublishState(false, epoch); +} + +void CClipboardChannel::Kick(uint64_t epoch) +{ + CSRWSharedLock lock(m_lifecycleLock); + if (Available() && epoch == m_epoch && m_kick) + SetEvent(m_kick); +} + +void CClipboardChannel::Reset(uint64_t epoch, uint32_t reason) +{ + uint64_t instance; + { + CSRWSharedLock lock(m_lifecycleLock); + if (!Available() || epoch != m_epoch) + return; + instance = m_instance; + } + // A peer reset is terminal for this mapping too. Keep the control pipe + // connected, but stop both clipboard workers so neither endpoint can + // continue publishing into a ring the other side has abandoned. + Fail(instance, reason, false); +} + +ClipboardChannelResult CClipboardChannel::Send( + const KVMFRClipboardMessage& record, const void * data) +{ + if (!ValidRecord(record, data != nullptr)) + return ClipboardChannelResult::FAILED; + + CSRWSharedLock lifecycleLock(m_lifecycleLock); + if (!Available() || !m_out || !m_doorbell) + return ClipboardChannelResult::FAILED; + + { + CSRWExclusiveLock writeLock(m_writeLock); + uint32_t ticket; + ClipboardRingSlot * slot = CClipboardRing::BeginWrite(*m_out, ticket); + if (!slot) + return ClipboardChannelResult::BUSY; + + slot->header = record; + if (record.length) + memcpy(slot->data, data, record.length); + if (!CClipboardRing::EndWrite(*m_out, ticket)) + return ClipboardChannelResult::FAILED; + } + + // Once the producer index advances the record belongs to the channel. + // Doorbells are only a latency optimization; the peer also polls. + m_doorbell->ClipboardKick(m_epoch); + return ClipboardChannelResult::ACCEPTED; +} + +void CClipboardChannel::SetHandler(IClipboardChannelHandler * handler) +{ + if (InClipboardCallback(this)) + { + m_handler = handler; + } + else + { + CSRWExclusiveLock lock(m_handlerLock); + m_handler = handler; + } + + if (!handler) + return; + + uint64_t epoch; + bool available; + { + CSRWSharedLock lock(m_lifecycleLock); + available = Available(); + epoch = m_epoch; + } + PublishState(available, epoch); + if (available) + Kick(epoch); +} + +void CClipboardChannel::ClearHandler(IClipboardChannelHandler * handler) +{ + if (InClipboardCallback(this)) + { + if (m_handler == handler) + m_handler = nullptr; + return; + } + + // The exclusive callback lock makes return from ClearHandler a + // synchronous callback-quiescence point. + CSRWExclusiveLock lock(m_handlerLock); + if (m_handler == handler) + m_handler = nullptr; +} + +CClipboardChannel::DrainResult CClipboardChannel::Drain() +{ + for (;;) + { + KVMFRClipboardMessage record = {}; + std::vector data; + uint32_t ticket; + { + CSRWSharedLock lifecycleLock(m_lifecycleLock); + if (!Available() || !m_in || !m_doorbell) + return DrainResult::STOPPED; + + const ClipboardRingSlot * slot = nullptr; + const ClipboardRingReadResult read = + CClipboardRing::BeginRead(*m_in, ticket, slot); + if (read == ClipboardRingReadResult::EMPTY) + return DrainResult::IDLE; + if (read == ClipboardRingReadResult::CORRUPT) + return DrainResult::CORRUPT; + + record = slot->header; + if (CClipboardRing::Valid(*slot) && + ValidRecord(record, record.length != 0) && record.length) + { + try + { + data.resize(record.length); + memcpy(data.data(), slot->data, record.length); + } + catch (const std::bad_alloc&) + { + return DrainResult::BUSY; + } + } + } + + ClipboardChannelResult result = + ValidRecord(record, record.length != 0) ? + PublishRecord(record, data.empty() ? nullptr : data.data()) : + ClipboardChannelResult::FAILED; + if (result == ClipboardChannelResult::BUSY) + return DrainResult::BUSY; + + IClipboardChannelDoorbell * doorbell; + uint64_t epoch; + { + CSRWSharedLock lifecycleLock(m_lifecycleLock); + if (!Available() || !m_in || !m_doorbell) + return DrainResult::STOPPED; + if (!CClipboardRing::EndRead(*m_in, ticket)) + return DrainResult::CORRUPT; + doorbell = m_doorbell; + epoch = m_epoch; + } + + if (result == ClipboardChannelResult::FAILED) + { + // The invalid record has been consumed so it cannot be retried. Make + // this endpoint terminal as well; Thread::Fail performs the single + // local callback and peer reset outside the ring/lifecycle locks. + return DrainResult::CORRUPT; + } + else + doorbell->ClipboardKick(epoch); + } +} + +void CClipboardChannel::Thread() +{ + HANDLE events[] = { m_stop, m_kick }; + const uint64_t instance = m_instance; + for (;;) + { + const DWORD result = WaitForMultipleObjects( + ARRAYSIZE(events), events, FALSE, POLL_MS); + if (result == WAIT_FIRST_OBJECT_VALUE) + break; + if (result != WAIT_FIRST_OBJECT_VALUE + 1 && result != WAIT_TIMEOUT) + { + const DWORD error = GetLastError(); + DEBUG_ERROR_HR(error, + "Failed to wait for clipboard channel work"); + Fail(instance, error ? error : ERROR_GEN_FAILURE); + break; + } + + const DrainResult drain = Drain(); + if (drain == DrainResult::STOPPED) + break; + if (drain == DrainResult::CORRUPT) + { + Fail(instance, ERROR_INVALID_DATA); + break; + } + } + CleanupDeferredDetach(); +} + +void CClipboardChannel::Fail( + uint64_t instance, uint32_t reason, bool resetPeer) +{ + IClipboardChannelDoorbell * doorbell; + uint64_t epoch; + { + CSRWExclusiveLock lock(m_lifecycleLock); + if (instance != m_instance) + return; + if (!Atomic::Swap(m_available, false, std::memory_order_acq_rel)) + return; + epoch = m_epoch; + doorbell = m_doorbell; + if (m_stop) + SetEvent(m_stop); + } + + PublishReset(epoch, reason); + if (resetPeer && doorbell) + doorbell->ClipboardResetPeer(epoch, reason); + PublishState(false, epoch); +} + +void CClipboardChannel::CleanupDeferredDetach() +{ + CSRWExclusiveLock lock(m_lifecycleLock); + if (!m_deferredDetach || m_threadId != GetCurrentThreadId()) + return; + + if (m_kick) + CloseHandle(m_kick); + if (m_stop) + CloseHandle(m_stop); + if (m_view) + UnmapViewOfFile(m_view); + if (m_mapping) + CloseHandle(m_mapping); + + // A later external Detach closes the now-signaled thread handle. + m_threadId = 0; + m_kick = nullptr; + m_stop = nullptr; + m_in = nullptr; + m_out = nullptr; + m_view = nullptr; + m_mapping = nullptr; + m_doorbell = nullptr; + m_epoch = 0; + m_deferredDetach = false; +} + +void CClipboardChannel::PublishState(bool available, uint64_t epoch) +{ + if (InClipboardCallback(this)) + { + if (m_handler) + m_handler->ClipboardState(available, epoch); + return; + } + + CSRWExclusiveLock lock(m_handlerLock); + if (m_handler) + { + CClipboardCallbackScope scope(this); + m_handler->ClipboardState(available, epoch); + } +} + +ClipboardChannelResult CClipboardChannel::PublishRecord( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + if (InClipboardCallback(this)) + return m_handler ? m_handler->ClipboardRecord(record, data) : + ClipboardChannelResult::BUSY; + + CSRWExclusiveLock lock(m_handlerLock); + if (!m_handler) + return ClipboardChannelResult::BUSY; + + CClipboardCallbackScope scope(this); + return m_handler->ClipboardRecord(record, data); +} + +void CClipboardChannel::PublishReset(uint64_t epoch, uint32_t reason) +{ + if (InClipboardCallback(this)) + { + if (m_handler) + m_handler->ClipboardReset(epoch, reason); + return; + } + + CSRWExclusiveLock lock(m_handlerLock); + if (m_handler) + { + CClipboardCallbackScope scope(this); + m_handler->ClipboardReset(epoch, reason); + } +} + +uint64_t CClipboardChannel::Epoch() +{ + CSRWSharedLock lock(m_lifecycleLock); + return m_epoch; +} + +DWORD WINAPI CClipboardChannel::ThreadProc(void * context) +{ + static_cast(context)->Thread(); + return 0; +} diff --git a/idd/LGCommon/CClipboardChannel.h b/idd/LGCommon/CClipboardChannel.h new file mode 100644 index 00000000..f3f009b1 --- /dev/null +++ b/idd/LGCommon/CClipboardChannel.h @@ -0,0 +1,129 @@ +/** + * 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 + */ + +#pragma once + +#include "Atomic.h" +#include "ClipboardRing.h" +#include "CSRWLock.h" + +#include + +#include +#include + +enum class ClipboardChannelResult +{ + ACCEPTED, + BUSY, + FAILED, +}; + +class IClipboardChannelHandler +{ +public: + virtual ~IClipboardChannelHandler() = default; + + virtual void ClipboardState(bool available, uint64_t epoch) = 0; + // data is borrowed and remains valid only for the duration of this call. + // BUSY leaves the shared ring slot occupied so it can be retried later. + virtual ClipboardChannelResult ClipboardRecord( + const KVMFRClipboardMessage& record, const uint8_t * data) = 0; + virtual void ClipboardReset(uint64_t epoch, uint32_t reason) = 0; +}; + +class IClipboardChannelDoorbell +{ +public: + virtual ~IClipboardChannelDoorbell() = default; + + virtual bool ClipboardKick(uint64_t epoch) = 0; + virtual void ClipboardResetPeer(uint64_t epoch, uint32_t reason) = 0; +}; + +class CClipboardChannel +{ +private: + static constexpr DWORD POLL_MS = 50; + + enum class DrainResult + { + IDLE, + BUSY, + STOPPED, + CORRUPT, + }; + + CSRWLock m_lifecycleLock; + CSRWLock m_handlerLock; + CSRWLock m_writeLock; + + HANDLE m_mapping = nullptr; + ClipboardMapping * m_view = nullptr; + ClipboardRing * m_in = nullptr; + ClipboardRing * m_out = nullptr; + HANDLE m_stop = nullptr; + HANDLE m_kick = nullptr; + HANDLE m_thread = nullptr; + DWORD m_threadId = 0; + uint64_t m_epoch = 0; + uint64_t m_instance = 0; + bool m_deferredDetach = false; + + IClipboardChannelHandler * m_handler = nullptr; + IClipboardChannelDoorbell * m_doorbell = nullptr; + std::atomic m_available { false }; + + DrainResult Drain(); + void Thread(); + void Fail(uint64_t instance, uint32_t reason, bool resetPeer = true); + void CleanupDeferredDetach(); + void PublishState(bool available, uint64_t epoch); + ClipboardChannelResult PublishRecord( + const KVMFRClipboardMessage& record, const uint8_t * data); + void PublishReset(uint64_t epoch, uint32_t reason); + + static DWORD WINAPI ThreadProc(void * context); + +public: + CClipboardChannel() = default; + ~CClipboardChannel(); + + CClipboardChannel(const CClipboardChannel&) = delete; + CClipboardChannel& operator=(const CClipboardChannel&) = delete; + + bool Attach(HANDLE mapping, uint64_t epoch, bool helper, + IClipboardChannelDoorbell& doorbell); + void Detach(); + void Kick(uint64_t epoch); + void Reset(uint64_t epoch, uint32_t reason); + + ClipboardChannelResult Send( + const KVMFRClipboardMessage& record, const void * data = nullptr); + + void SetHandler(IClipboardChannelHandler * handler); + void ClearHandler(IClipboardChannelHandler * handler); + + bool Available() const + { + return Atomic::Load(m_available, std::memory_order_acquire); + } + uint64_t Epoch(); +}; diff --git a/idd/LGCommon/CClipboardRing.cpp b/idd/LGCommon/CClipboardRing.cpp index 943dd285..a096ef13 100644 --- a/idd/LGCommon/CClipboardRing.cpp +++ b/idd/LGCommon/CClipboardRing.cpp @@ -82,17 +82,21 @@ bool CClipboardRing::EndWrite(ClipboardRing& ring, uint32_t ticket) return true; } -const ClipboardRingSlot * CClipboardRing::BeginRead( - ClipboardRing& ring, uint32_t& ticket) +ClipboardRingReadResult CClipboardRing::BeginRead(ClipboardRing& ring, + uint32_t& ticket, const ClipboardRingSlot *& slot) { + slot = nullptr; const uint32_t consumed = Atomic::Load(ring.consumed); const uint32_t pending = Atomic::Load(ring.produced) - consumed; - if (!pending || pending > KVMFR_CLIPBOARD_SLOT_COUNT) - return nullptr; + if (!pending) + return ClipboardRingReadResult::EMPTY; + if (pending > KVMFR_CLIPBOARD_SLOT_COUNT) + return ClipboardRingReadResult::CORRUPT; Atomic::Fence(); ticket = consumed; - return &ring.slots[consumed % KVMFR_CLIPBOARD_SLOT_COUNT]; + slot = &ring.slots[consumed % KVMFR_CLIPBOARD_SLOT_COUNT]; + return ClipboardRingReadResult::READY; } bool CClipboardRing::EndRead(ClipboardRing& ring, uint32_t ticket) diff --git a/idd/LGCommon/CClipboardRing.h b/idd/LGCommon/CClipboardRing.h index 44a910d8..975e651a 100644 --- a/idd/LGCommon/CClipboardRing.h +++ b/idd/LGCommon/CClipboardRing.h @@ -22,6 +22,13 @@ #include "ClipboardRing.h" +enum class ClipboardRingReadResult +{ + EMPTY, + READY, + CORRUPT, +}; + class CClipboardRing { public: @@ -32,8 +39,8 @@ public: ClipboardRing& ring, uint32_t& ticket); static bool EndWrite(ClipboardRing& ring, uint32_t ticket); - static const ClipboardRingSlot * BeginRead( - ClipboardRing& ring, uint32_t& ticket); + static ClipboardRingReadResult BeginRead(ClipboardRing& ring, + uint32_t& ticket, const ClipboardRingSlot *& slot); static bool EndRead(ClipboardRing& ring, uint32_t ticket); static bool Valid(const ClipboardRingSlot& slot); diff --git a/idd/LGCommon/CPipeEndpoint.cpp b/idd/LGCommon/CPipeEndpoint.cpp index 1b2003c4..440271af 100644 --- a/idd/LGCommon/CPipeEndpoint.cpp +++ b/idd/LGCommon/CPipeEndpoint.cpp @@ -310,6 +310,12 @@ bool CPipeEndpoint::Send(const void * message, size_t size) return success; } +HANDLE CPipeEndpoint::NativeHandle() +{ + CSRWSharedLock lock(m_pipeLock); + return m_pipe; +} + DWORD WINAPI CPipeEndpoint::ThreadProc(void * context) { static_cast(context)->Thread(); diff --git a/idd/LGCommon/CPipeEndpoint.h b/idd/LGCommon/CPipeEndpoint.h index 2c0c07e2..29a642a0 100644 --- a/idd/LGCommon/CPipeEndpoint.h +++ b/idd/LGCommon/CPipeEndpoint.h @@ -69,6 +69,8 @@ public: bool IsRunning() const { return Atomic::Load(m_running); } bool IsConnected() const { return Atomic::Load(m_connected); } + HANDLE NativeHandle(); + void SetHandler(_In_opt_ IPipeEndpointHandler * handler) { m_handler = handler; diff --git a/idd/LGCommon/LGCommon.vcxproj b/idd/LGCommon/LGCommon.vcxproj index ea941284..ddf6a839 100644 --- a/idd/LGCommon/LGCommon.vcxproj +++ b/idd/LGCommon/LGCommon.vcxproj @@ -65,12 +65,14 @@ + + diff --git a/idd/LGCommon/LGCommon.vcxproj.filters b/idd/LGCommon/LGCommon.vcxproj.filters index 37aa7b57..6e1faf59 100644 --- a/idd/LGCommon/LGCommon.vcxproj.filters +++ b/idd/LGCommon/LGCommon.vcxproj.filters @@ -12,6 +12,9 @@ + + Source Files + Source Files @@ -29,6 +32,9 @@ Header Files + + Header Files + Header Files diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj index 31c90a7e..6963fd1e 100644 --- a/idd/LGIdd/LGIdd.vcxproj +++ b/idd/LGIdd/LGIdd.vcxproj @@ -66,6 +66,7 @@ + @@ -76,6 +77,7 @@ + @@ -125,6 +127,7 @@ + @@ -135,6 +138,7 @@ + @@ -149,6 +153,7 @@ + diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters index de7ab098..1e6cf0bc 100644 --- a/idd/LGIdd/LGIdd.vcxproj.filters +++ b/idd/LGIdd/LGIdd.vcxproj.filters @@ -169,6 +169,9 @@ Post-processing\Effects + + Transport + Transport @@ -199,6 +202,9 @@ Transport + + Transport + Transport @@ -241,6 +247,9 @@ Transport\LGMP + + Transport\LGMP + Transport\LGMP @@ -369,6 +378,9 @@ Post-processing\Effects + + Transport + Transport @@ -399,6 +411,9 @@ Transport\LGMP + + Transport\LGMP + Transport\LGMP diff --git a/idd/LGIdd/ipc/CPipeServer.cpp b/idd/LGIdd/ipc/CPipeServer.cpp index 7b494cec..e05f50e2 100644 --- a/idd/LGIdd/ipc/CPipeServer.cpp +++ b/idd/LGIdd/ipc/CPipeServer.cpp @@ -37,6 +37,7 @@ bool CPipeServer::Init() void CPipeServer::DeInit() { m_endpoint.Stop(); + m_clipboard.Detach(); } void CPipeServer::OnPipeConnected() @@ -60,6 +61,11 @@ void CPipeServer::OnPipeConnected() m_endpoint.Send(&m_recoveryRequest, sizeof(m_recoveryRequest)); } +void CPipeServer::OnPipeDisconnected() +{ + m_clipboard.Detach(); +} + bool CPipeServer::OnPipeMessage(const void * message, size_t size) { if (size != sizeof(LGPipeMsg)) @@ -82,12 +88,90 @@ bool CPipeServer::OnPipeMessage(const void * message, size_t size) HandleRecovery(msg); return true; + case LGPipeMsg::CLIPBOARD_SETUP: + { + HANDLE transferred = reinterpret_cast( + static_cast(msg.clipboardSetup.handle)); + HANDLE mapping = transferred; + uint64_t epoch = 0; + if (transferred && + msg.clipboardSetup.bytes == sizeof(ClipboardMapping)) + { + ClipboardMapping * view = static_cast( + MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, + sizeof(ClipboardMapping))); + if (view) + { + epoch = view->epoch; + UnmapViewOfFile(view); + } + } + else if (transferred) + { + CloseHandle(transferred); + mapping = nullptr; + } + + // The transferred handle has exactly one owner from this point: + // Attach consumes it on both success and failure paths. + const bool ready = m_clipboard.Attach( + mapping, epoch, false, *this); + LGPipeMsg reply = {}; + reply.size = sizeof(reply); + reply.type = LGPipeMsg::CLIPBOARD_READY; + reply.clipboardReady.epoch = epoch; + reply.clipboardReady.status = ready ? ERROR_SUCCESS : ERROR_INVALID_DATA; + m_endpoint.Send(&reply, sizeof(reply)); + return true; + } + + case LGPipeMsg::CLIPBOARD_READY: + // READY normally travels IDD to Helper. A failure in the reverse + // direction reports that Helper activation failed after IDD attach. + if (msg.clipboardReady.status != ERROR_SUCCESS && + msg.clipboardReady.epoch == m_clipboard.Epoch()) + { + m_clipboard.Reset( + msg.clipboardReady.epoch, msg.clipboardReady.status); + m_clipboard.Detach(); + } + return true; + + case LGPipeMsg::CLIPBOARD_KICK: + m_clipboard.Kick(msg.clipboardKick.epoch); + return true; + + case LGPipeMsg::CLIPBOARD_RESET: + m_clipboard.Reset( + msg.clipboardReset.epoch, msg.clipboardReset.reason); + return true; + default: DEBUG_ERROR("Unknown message type %d", msg.type); return true; } } +bool CPipeServer::ClipboardKick(uint64_t epoch) +{ + LGPipeMsg msg = {}; + msg.size = sizeof(msg); + msg.type = LGPipeMsg::CLIPBOARD_KICK; + msg.clipboardKick.epoch = epoch; + msg.clipboardKick.rings = 0; + return m_endpoint.Send(&msg, sizeof(msg)); +} + +void CPipeServer::ClipboardResetPeer(uint64_t epoch, uint32_t reason) +{ + LGPipeMsg msg = {}; + msg.size = sizeof(msg); + msg.type = LGPipeMsg::CLIPBOARD_RESET; + msg.clipboardReset.epoch = epoch; + msg.clipboardReset.reason = reason; + m_endpoint.Send(&msg, sizeof(msg)); +} + void CPipeServer::QueueMsgLocked(const LGPipeMsg & msg) { for (LGPipeMsg & queued : m_queue) diff --git a/idd/LGIdd/ipc/CPipeServer.h b/idd/LGIdd/ipc/CPipeServer.h index 9585041f..3a2761f5 100644 --- a/idd/LGIdd/ipc/CPipeServer.h +++ b/idd/LGIdd/ipc/CPipeServer.h @@ -26,12 +26,14 @@ #include #include "CPipeEndpoint.h" +#include "CClipboardChannel.h" #include "CSRWLock.h" #include "PipeMsg.h" class CDeviceContext; -class CPipeServer : private IPipeEndpointHandler +class CPipeServer : private IPipeEndpointHandler, + public IClipboardChannelDoorbell { public: using RecoveryHandler = void (*)(void * opaque, @@ -40,6 +42,7 @@ class CPipeServer : private IPipeEndpointHandler private: CPipeEndpoint m_endpoint; + CClipboardChannel m_clipboard; CSRWLock m_queueLock; std::vector m_queue; bool m_recoveryValid = false; @@ -60,6 +63,7 @@ class CPipeServer : private IPipeEndpointHandler void HandleRecovery(const LGPipeMsg & msg); void OnPipeConnected() override; + void OnPipeDisconnected() override; bool OnPipeMessage(const void * message, size_t size) override; public: @@ -80,6 +84,10 @@ class CPipeServer : private IPipeEndpointHandler uint32_t requiredSizeMiB); bool SetRecovery(void * owner, uint64_t route, uint64_t session, uint32_t serial, bool active); + + CClipboardChannel& Clipboard() { return m_clipboard; } + bool ClipboardKick(uint64_t epoch) override; + void ClipboardResetPeer(uint64_t epoch, uint32_t reason) override; }; extern CPipeServer g_pipe; diff --git a/idd/LGIdd/transport/CClipboardHub.cpp b/idd/LGIdd/transport/CClipboardHub.cpp new file mode 100644 index 00000000..6649a7fc --- /dev/null +++ b/idd/LGIdd/transport/CClipboardHub.cpp @@ -0,0 +1,380 @@ +/** + * 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 "transport/CClipboardHub.h" + +#include "Seq.h" + +namespace +{ + bool ValidHelperDirection(const KVMFRClipboardMessage& record) + { + if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST) + return kvmfrClipboardTransferFromHelper(record.transfer); + if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA) + return kvmfrClipboardTransferFromClient(record.transfer); + return true; + } +} + +CClipboardHub::CClipboardHub(CClipboardChannel& channel) : + m_channel(channel) +{ + m_channel.SetHandler(this); +} + +CClipboardHub::~CClipboardHub() +{ + Stop(); +} + +void CClipboardHub::AdvanceGenerationNL() +{ + Seq::Inc(m_generation); +} + +bool CClipboardHub::MarkFailed(IClipboardSource& source) +{ + CSRWExclusiveLock lock(m_lock); + if (m_source != &source || (!m_active && !m_reserved)) + return false; + + const bool changed = !m_failed; + m_failed = true; + m_failurePending = true; + return changed; +} + +ClipboardChannelResult CClipboardHub::HoldOrDiscard( + const KVMFRClipboardMessage& record) +{ + // Preserve the pending clipboard publication until a source is rebound. + // Transfer-scoped records belong to the source which admitted them and + // must never cross a transport generation. + if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || + record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR) + return ClipboardChannelResult::BUSY; + + if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST) + { + KVMFRClipboardMessage cancel = {}; + cancel.version = KVMFR_CLIPBOARD_VERSION; + cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + cancel.generation = record.generation; + cancel.clipboardGeneration = record.clipboardGeneration; + cancel.transfer = record.transfer; + cancel.format = record.format; + cancel.token = ERROR_DEVICE_NOT_CONNECTED; + const ClipboardChannelResult result = m_channel.Send(cancel); + if (result != ClipboardChannelResult::ACCEPTED) + return result; + } + + return ClipboardChannelResult::ACCEPTED; +} + +bool CClipboardHub::Bind( + BackendId backend, uint32_t epoch, IClipboardSource& source) +{ + CSRWExclusiveLock lifecycleLock(m_lifecycleLock); + if (!backend || !epoch) + return false; + + bool available; + uint32_t generation; + uint64_t channelEpoch; + { + CSRWExclusiveLock lock(m_lock); + if (m_stopped || m_source || m_active || m_reserved) + return false; + + AdvanceGenerationNL(); + m_source = &source; + m_backend = backend; + m_epoch = epoch; + m_active = true; + m_reserved = true; + m_failed = false; + m_failurePending = false; + available = m_available; + generation = m_generation; + channelEpoch = m_channelEpoch; + } + + CSRWExclusiveLock callbackLock(m_callbackLock); + const bool started = source.Start(*this); + bool attached = false; + { + CSRWExclusiveLock lock(m_lock); + if (started && m_source == &source && m_backend == backend && + m_epoch == epoch && m_active && m_reserved && !m_failed && + !m_stopped) + { + m_running = true; + m_reserved = false; + available = m_available; + generation = m_generation; + channelEpoch = m_channelEpoch; + attached = true; + } + else + { + m_active = false; + m_running = false; + } + } + + if (!attached) + { + source.Stop(); + CSRWExclusiveLock lock(m_lock); + if (m_source == &source && m_backend == backend && m_epoch == epoch) + { + m_source = nullptr; + m_backend = 0; + m_epoch = 0; + m_active = false; + m_running = false; + m_reserved = false; + m_failed = false; + m_failurePending = false; + } + return false; + } + + source.ClipboardState(available, generation); + callbackLock.Unlock(); + if (available && channelEpoch) + m_channel.Kick(channelEpoch); + return true; +} + +void CClipboardHub::Unbind(BackendId backend, uint32_t epoch) +{ + CSRWExclusiveLock lifecycleLock(m_lifecycleLock); + IClipboardSource * source = nullptr; + bool stop = false; + { + CSRWExclusiveLock lock(m_lock); + if ((!m_active && !m_failed && !m_reserved) || + m_backend != backend || m_epoch != epoch || !m_source) + return; + + source = m_source; + stop = m_running || m_reserved; + m_active = false; + m_running = false; + m_reserved = true; + m_failed = false; + m_failurePending = false; + AdvanceGenerationNL(); + } + + CSRWExclusiveLock callbackLock(m_callbackLock); + if (stop) + source->Stop(); + + CSRWExclusiveLock lock(m_lock); + if (m_source == source && m_backend == backend && m_epoch == epoch) + { + m_source = nullptr; + m_backend = 0; + m_epoch = 0; + m_active = false; + m_running = false; + m_reserved = false; + m_failed = false; + m_failurePending = false; + } +} + +bool CClipboardHub::TakeFailure(SourceKey& source) +{ + CSRWSharedLock lifecycleLock(m_lifecycleLock); + CSRWExclusiveLock lock(m_lock); + if (!m_failurePending) + return false; + + source = {}; + source.backend = m_backend; + source.epoch = m_epoch; + m_failurePending = false; + return true; +} + +void CClipboardHub::Stop() +{ + CSRWExclusiveLock lifecycleLock(m_lifecycleLock); + IClipboardSource * source = nullptr; + bool stop = false; + { + CSRWExclusiveLock lock(m_lock); + if (m_stopped) + return; + + m_stopped = true; + source = m_source; + stop = source && (m_running || m_reserved); + m_active = false; + m_running = false; + m_reserved = source != nullptr; + m_failed = false; + m_failurePending = false; + AdvanceGenerationNL(); + } + + // Clearing the channel handler is a callback-quiescence barrier. It must + // precede taking m_callbackLock because an in-flight channel callback owns + // the channel's handler lock while waiting for m_callbackLock. + m_channel.ClearHandler(this); + + CSRWExclusiveLock callbackLock(m_callbackLock); + if (stop) + source->Stop(); + + CSRWExclusiveLock lock(m_lock); + m_source = nullptr; + m_backend = 0; + m_epoch = 0; + m_active = false; + m_running = false; + m_reserved = false; + m_failed = false; + m_failurePending = false; +} + +void CClipboardHub::ClipboardState(bool available, uint64_t epoch) +{ + CSRWExclusiveLock callbackLock(m_callbackLock); + IClipboardSource * source = nullptr; + uint32_t generation; + { + CSRWExclusiveLock lock(m_lock); + if (m_stopped) + return; + + if (m_available != available || m_channelEpoch != epoch) + AdvanceGenerationNL(); + m_available = available; + m_channelEpoch = epoch; + generation = m_generation; + if (m_active && m_running && !m_failed) + source = m_source; + } + + if (source) + source->ClipboardState(available, generation); +} + +ClipboardChannelResult CClipboardHub::ClipboardRecord( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + if (!ValidHelperDirection(record)) + return ClipboardChannelResult::FAILED; + + CSRWExclusiveLock callbackLock(m_callbackLock); + IClipboardSource * source = nullptr; + uint32_t generation; + { + CSRWSharedLock lock(m_lock); + if (m_stopped || !m_available) + return ClipboardChannelResult::ACCEPTED; + if (m_failed || !m_active || !m_running || !m_source) + return HoldOrDiscard(record); + source = m_source; + generation = m_generation; + } + + KVMFRClipboardMessage stamped = record; + stamped.generation = generation; + const ClipboardChannelResult result = + source->SendClipboard(stamped, data); + if (result != ClipboardChannelResult::FAILED) + return result; + + if (MarkFailed(*source)) + { + const uint64_t epoch = m_channel.Epoch(); + if (epoch) + m_channel.Kick(epoch); + } + return HoldOrDiscard(record); +} + +void CClipboardHub::ClipboardReset(uint64_t epoch, uint32_t reason) +{ + CSRWExclusiveLock callbackLock(m_callbackLock); + IClipboardSource * source = nullptr; + uint32_t generation; + { + CSRWExclusiveLock lock(m_lock); + if (m_stopped || m_channelEpoch != epoch) + return; + + AdvanceGenerationNL(); + generation = m_generation; + if (m_active && m_running && !m_failed) + source = m_source; + } + + if (source) + source->ClipboardReset(generation, reason); +} + +ClipboardChannelResult CClipboardHub::SendClipboard( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + // Keep the shared lock through Send so Unbind cannot advance the binding + // generation after validation but before the record enters the channel. + CSRWSharedLock lock(m_lock); + if (m_stopped || !m_available || !m_active || !m_running || m_failed || + !m_source || record.generation != m_generation) + return ClipboardChannelResult::FAILED; + return m_channel.Send(record, data); +} + +void CClipboardHub::ClipboardReceiveReady() +{ + { + CSRWSharedLock lock(m_lock); + if (m_stopped || !m_available || !m_active || m_failed || + !m_running) + return; + } + + const uint64_t epoch = m_channel.Epoch(); + if (epoch) + m_channel.Kick(epoch); +} + +void CClipboardHub::ClipboardFailed() +{ + IClipboardSource * source; + { + CSRWSharedLock lock(m_lock); + source = m_source; + } + if (!source || !MarkFailed(*source)) + return; + + const uint64_t epoch = m_channel.Epoch(); + if (epoch) + m_channel.Kick(epoch); +} diff --git a/idd/LGIdd/transport/CClipboardHub.h b/idd/LGIdd/transport/CClipboardHub.h new file mode 100644 index 00000000..35f74be8 --- /dev/null +++ b/idd/LGIdd/transport/CClipboardHub.h @@ -0,0 +1,78 @@ +/** + * 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 + */ + +#pragma once + +#include "CClipboardChannel.h" +#include "CSRWLock.h" +#include "transport/IClipboardSource.h" +#include "transport/ITransport.h" + +class CClipboardHub final : public IClipboardTarget, + private IClipboardChannelHandler +{ +private: + CClipboardChannel& m_channel; + CSRWLock m_lifecycleLock; + CSRWLock m_callbackLock; + CSRWLock m_lock; + + IClipboardSource * m_source = nullptr; + BackendId m_backend = 0; + uint32_t m_epoch = 0; + uint64_t m_channelEpoch = 0; + uint32_t m_generation = 1; + bool m_available = false; + bool m_active = false; + bool m_running = false; + bool m_reserved = false; + bool m_failed = false; + bool m_failurePending = false; + bool m_stopped = false; + + void AdvanceGenerationNL(); + bool MarkFailed(IClipboardSource& source); + ClipboardChannelResult HoldOrDiscard( + const KVMFRClipboardMessage& record); + + void ClipboardState(bool available, uint64_t epoch) override; + ClipboardChannelResult ClipboardRecord( + const KVMFRClipboardMessage& record, + const uint8_t * data) override; + void ClipboardReset(uint64_t epoch, uint32_t reason) override; + +public: + explicit CClipboardHub(CClipboardChannel& channel); + ~CClipboardHub() override; + + CClipboardHub(const CClipboardHub&) = delete; + CClipboardHub& operator=(const CClipboardHub&) = delete; + + bool Bind(BackendId backend, uint32_t epoch, IClipboardSource& source); + void Unbind(BackendId backend, uint32_t epoch); + bool TakeFailure(SourceKey& source); + void Stop(); + + ClipboardChannelResult SendClipboard( + const KVMFRClipboardMessage& record, + const uint8_t * data) override; + void ClipboardReceiveReady() override; + void ClipboardFailed() override; +}; diff --git a/idd/LGIdd/transport/CTransportManager.cpp b/idd/LGIdd/transport/CTransportManager.cpp index de140967..523c20b7 100644 --- a/idd/LGIdd/transport/CTransportManager.cpp +++ b/idd/LGIdd/transport/CTransportManager.cpp @@ -23,7 +23,9 @@ #include "Atomic.h" #include "capture/CFrameGraph.h" #include "CDebug.h" +#include "ipc/CPipeServer.h" #include "Seq.h" +#include "transport/IClipboardSource.h" #include "transport/ITexStage.h" #include @@ -121,7 +123,8 @@ CTransportManager::Entry::~Entry() CloseHandle(idleEvent); } -CTransportManager::CTransportManager() : m_tex(m_frameRev) +CTransportManager::CTransportManager() : + m_tex(m_frameRev), m_clipboard(g_pipe.Clipboard()) { m_phaseIdle = CreateEvent(nullptr, TRUE, TRUE, nullptr); m_stoppedEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); @@ -492,48 +495,55 @@ bool CTransportManager::InitializeEntry(Entry& entry) bool CTransportManager::AddServices(Entry& entry) { std::shared_ptr transport; - BackendId id = 0; - uint32_t epoch = 0; - bool primary = false; - bool required = false; - uint32_t services = 0; - bool controlAdded = false; - bool controlFailed = false; - bool controlAbsent = false; - bool inputAdded = false; - bool inputFailed = false; - bool inputAbsent = false; - bool frameAdded = false; - bool frameBound = false; - bool frameAbsent = false; - uint64_t retryAt = 0; + BackendId id = 0; + uint32_t epoch = 0; + bool primary = false; + bool required = false; + uint32_t services = 0; + bool controlAdded = false; + bool controlFailed = false; + bool controlAbsent = false; + bool inputAdded = false; + bool inputFailed = false; + bool inputAbsent = false; + bool clipboardAdded = false; + bool clipboardFailed = false; + bool clipboardAbsent = false; + bool frameAdded = false; + bool frameBound = false; + bool frameAbsent = false; + uint64_t retryAt = 0; { CSRWSharedLock entryLock(entry.lock); - transport = entry.transport; - id = entry.id; - epoch = entry.epoch; - primary = entry.primary; - required = entry.required; - services = entry.config.services; - controlAdded = entry.controlAdded; - controlFailed = entry.controlFailed; - controlAbsent = entry.controlAbsent; - inputAdded = entry.inputAdded; - inputFailed = entry.inputFailed; - inputAbsent = entry.inputAbsent; - frameAdded = entry.frameAdded; - frameAbsent = entry.frameAbsent; - retryAt = entry.serviceRetryAt; + transport = entry.transport; + id = entry.id; + epoch = entry.epoch; + primary = entry.primary; + required = entry.required; + services = entry.config.services; + controlAdded = entry.controlAdded; + controlFailed = entry.controlFailed; + controlAbsent = entry.controlAbsent; + inputAdded = entry.inputAdded; + inputFailed = entry.inputFailed; + inputAbsent = entry.inputAbsent; + clipboardAdded = entry.clipboardAdded; + clipboardFailed = entry.clipboardFailed; + clipboardAbsent = entry.clipboardAbsent; + frameAdded = entry.frameAdded; + frameAbsent = entry.frameAbsent; + retryAt = entry.serviceRetryAt; } if (!transport) return !required; - const uint64_t now = GetTickCount64(); - const bool attach = now >= retryAt; - bool controlRetry = false; - bool frameRetry = false; - bool inputRetry = false; + const uint64_t now = GetTickCount64(); + const bool attach = now >= retryAt; + bool controlRetry = false; + bool frameRetry = false; + bool inputRetry = false; + bool clipboardRetry = false; if (!(services & TRANSPORT_SERVICE_CONTROL)) controlAbsent = true; else if (attach && !controlAdded && !controlFailed && !controlAbsent) @@ -610,7 +620,30 @@ bool CTransportManager::AddServices(Entry& entry) } } - if (attach && (controlRetry || inputRetry || frameRetry)) + if (!(services & TRANSPORT_SERVICE_CLIPBOARD)) + clipboardAbsent = true; + else if (attach && !clipboardAdded && !clipboardFailed && + !clipboardAbsent) + { + IClipboardSource * clipboard = transport->Clipboard(); + if (clipboard && m_clipboard.Bind(id, epoch, *clipboard)) + { + CSRWExclusiveLock entryLock(entry.lock); + entry.clipboardAdded = true; + clipboardAdded = true; + } + else if (clipboard) + clipboardRetry = true; + else + { + clipboardAbsent = true; + CSRWExclusiveLock entryLock(entry.lock); + entry.clipboardAbsent = true; + } + } + + if (attach && (controlRetry || inputRetry || frameRetry || + clipboardRetry)) { CSRWExclusiveLock entryLock(entry.lock); entry.serviceRetryAt = now + SERVICE_RETRY_DELAY_MS; @@ -623,9 +656,10 @@ bool CTransportManager::AddServices(Entry& entry) } const bool servicesReady = - (!(services & TRANSPORT_SERVICE_FRAME) || frameAdded) && - (!(services & TRANSPORT_SERVICE_CONTROL) || controlAdded) && - (!(services & TRANSPORT_SERVICE_INPUT) || inputAdded); + (!(services & TRANSPORT_SERVICE_FRAME) || frameAdded) && + (!(services & TRANSPORT_SERVICE_CONTROL) || controlAdded) && + (!(services & TRANSPORT_SERVICE_INPUT) || inputAdded) && + (!(services & TRANSPORT_SERVICE_CLIPBOARD) || clipboardAdded); return !required || servicesReady; } @@ -720,6 +754,35 @@ void CTransportManager::HandleServiceFailures() break; } } + + source = {}; + while (m_clipboard.TakeFailure(source)) + { + Entry * entries[FRAME_MAX_SINKS] = {}; + const unsigned count = Entries(entries); + for (unsigned i = 0; i < count; ++i) + { + Entry& entry = *entries[i]; + bool restart = false; + { + CSRWExclusiveLock entryLock(entry.lock); + if (entry.id != source.backend || entry.epoch != source.epoch || + !entry.clipboardAdded) + continue; + entry.clipboardAdded = false; + entry.clipboardFailed = true; + restart = !entry.exposed; + } + + m_clipboard.Unbind(source.backend, source.epoch); + if (restart) + { + RemoveServices(entry); + ScheduleRetry(entry); + } + break; + } + } } bool CTransportManager::SetupEntry(Entry& entry, size_t alignment) @@ -770,12 +833,13 @@ void CTransportManager::ScheduleRetry(Entry& entry) void CTransportManager::RemoveServices(Entry& entry) { - BackendId id = 0; - uint32_t epoch = 0; - bool frameAdded = false; - bool frameLegacy = false; - bool controlAdded = false; - bool inputAdded = false; + BackendId id = 0; + uint32_t epoch = 0; + bool frameAdded = false; + bool frameLegacy = false; + bool controlAdded = false; + bool inputAdded = false; + bool clipboardAdded = false; { CSRWExclusiveLock entryLock(entry.lock); id = entry.id; @@ -784,11 +848,13 @@ void CTransportManager::RemoveServices(Entry& entry) frameLegacy = entry.frameLegacy; controlAdded = entry.controlAdded; inputAdded = entry.inputAdded; + clipboardAdded = entry.clipboardAdded; entry.frameAdded = false; entry.frameLegacy = false; entry.texSink = nullptr; entry.controlAdded = false; entry.inputAdded = false; + entry.clipboardAdded = false; entry.frameRetryAt = 0; } @@ -796,6 +862,8 @@ void CTransportManager::RemoveServices(Entry& entry) m_tex.Drop(id, epoch); if (inputAdded) m_input.Unbind(id, epoch); + if (clipboardAdded) + m_clipboard.Unbind(id, epoch); if (controlAdded) m_control.Remove(id, epoch); if (frameLegacy) @@ -835,6 +903,8 @@ void CTransportManager::RetryEntry(Entry& entry, uint64_t now, entry.controlAbsent = false; entry.inputFailed = false; entry.inputAbsent = false; + entry.clipboardFailed = false; + entry.clipboardAbsent = false; entry.frameAbsent = false; entry.serviceRetryAt = 0; Seq::Inc(entry.epoch); @@ -1601,6 +1671,7 @@ void CTransportManager::Stop() } m_input.Stop(); + m_clipboard.Stop(); for (unsigned i = count; i > 0; --i) { diff --git a/idd/LGIdd/transport/CTransportManager.h b/idd/LGIdd/transport/CTransportManager.h index cead9fd4..142eafe4 100644 --- a/idd/LGIdd/transport/CTransportManager.h +++ b/idd/LGIdd/transport/CTransportManager.h @@ -22,6 +22,7 @@ #include "Atomic.h" #include "CSRWLock.h" +#include "transport/CClipboardHub.h" #include "transport/CControlHub.h" #include "transport/CFrameHub.h" #include "transport/CInputHub.h" @@ -109,6 +110,9 @@ private: bool inputAdded = false; bool inputFailed = false; bool inputAbsent = false; + bool clipboardAdded = false; + bool clipboardFailed = false; + bool clipboardAbsent = false; bool frameAdded = false; bool frameLegacy = false; ITexSink * texSink = nullptr; @@ -132,6 +136,7 @@ private: CFrameHub m_frames; CTexHub m_tex; CInputHub m_input; + CClipboardHub m_clipboard; CRecoveryHub m_recovery; Entry * m_primary = nullptr; bool m_initialized = false; diff --git a/idd/LGIdd/transport/IClipboardSource.h b/idd/LGIdd/transport/IClipboardSource.h new file mode 100644 index 00000000..0f1fe20c --- /dev/null +++ b/idd/LGIdd/transport/IClipboardSource.h @@ -0,0 +1,77 @@ +/** + * 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 + */ + +#pragma once + +#include "CClipboardChannel.h" +#include "common/KVMFRClipboard.h" + +#include + +class IClipboardTarget +{ +public: + virtual ~IClipboardTarget() = default; + + // Sends one client-to-Helper record. record.generation must be the binding + // generation most recently supplied by ClipboardState; transports which + // expose their own endpoint generation validate and replace it at this + // adapter boundary. clipboardGeneration remains the clipboard content + // generation and is preserved end-to-end. + // + // BUSY consumes neither the record nor its borrowed data; the source + // retains and retries the exact record from its bounded worker/Process + // drain. + virtual ClipboardChannelResult SendClipboard( + const KVMFRClipboardMessage& record, const uint8_t * data) = 0; + + // The source calls ClipboardReceiveReady after a Helper-to-client record + // returned BUSY and it can accept an exact retry of that record. + virtual void ClipboardReceiveReady() = 0; + virtual void ClipboardFailed() = 0; +}; + +class IClipboardSource +{ +public: + virtual ~IClipboardSource() = default; + + // Start attaches the target, but the source must wait for the initial + // ClipboardState callback before calling target.SendClipboard. Stop is a + // callback-quiescence barrier and leaves no work which can access target; + // it also cleans up a partially completed or failed Start. + virtual bool Start(IClipboardTarget& target) = 0; + virtual void Stop() = 0; + + // All callbacks into the source, including Start and Stop, are serialized. + // record.generation is stamped with the current binding generation. A + // generation change invalidates records from an earlier generation and + // cancels their transfers; clipboardGeneration is unaffected. data is + // borrowed only for the duration of SendClipboard. BUSY consumes neither + // record nor data, and the channel retries the same record after + // ClipboardReceiveReady (or its bounded polling fallback). + virtual void ClipboardState(bool available, uint32_t generation) = 0; + virtual ClipboardChannelResult SendClipboard( + const KVMFRClipboardMessage& record, const uint8_t * data) = 0; + + // Reset advances generation before the callback. The source cancels all + // transfers from the preceding generation. + virtual void ClipboardReset(uint32_t generation, uint32_t reason) = 0; +}; diff --git a/idd/LGIdd/transport/ITransport.h b/idd/LGIdd/transport/ITransport.h index e46492d3..b486db4a 100644 --- a/idd/LGIdd/transport/ITransport.h +++ b/idd/LGIdd/transport/ITransport.h @@ -29,6 +29,7 @@ #include #include +class IClipboardSource; class IControlSink; class IFrameSink; class IInputSource; @@ -178,4 +179,5 @@ public: virtual ITexSink * TexSink() { return nullptr; } virtual IControlSink * Control() { return nullptr; } virtual IInputSource * Input() { return nullptr; } + virtual IClipboardSource * Clipboard() { return nullptr; } }; diff --git a/idd/LGIdd/transport/TransportConfig.cpp b/idd/LGIdd/transport/TransportConfig.cpp index a34025ac..bf86d3de 100644 --- a/idd/LGIdd/transport/TransportConfig.cpp +++ b/idd/LGIdd/transport/TransportConfig.cpp @@ -125,6 +125,8 @@ namespace service = TRANSPORT_SERVICE_CONTROL; else if (Equal(name, L"input")) service = TRANSPORT_SERVICE_INPUT; + else if (Equal(name, L"clipboard")) + service = TRANSPORT_SERVICE_CLIPBOARD; else return false; diff --git a/idd/LGIdd/transport/TransportConfig.h b/idd/LGIdd/transport/TransportConfig.h index 6223d324..b0d68847 100644 --- a/idd/LGIdd/transport/TransportConfig.h +++ b/idd/LGIdd/transport/TransportConfig.h @@ -28,11 +28,13 @@ using BackendId = uint32_t; enum TransportService : uint32_t { - TRANSPORT_SERVICE_FRAME = 1U << 0, - TRANSPORT_SERVICE_CONTROL = 1U << 1, - TRANSPORT_SERVICE_INPUT = 1U << 2, - TRANSPORT_SERVICE_ALL = TRANSPORT_SERVICE_FRAME | - TRANSPORT_SERVICE_CONTROL | TRANSPORT_SERVICE_INPUT, + TRANSPORT_SERVICE_FRAME = 1U << 0, + TRANSPORT_SERVICE_CONTROL = 1U << 1, + TRANSPORT_SERVICE_INPUT = 1U << 2, + TRANSPORT_SERVICE_CLIPBOARD = 1U << 3, + TRANSPORT_SERVICE_ALL = TRANSPORT_SERVICE_FRAME | + TRANSPORT_SERVICE_CONTROL | TRANSPORT_SERVICE_INPUT | + TRANSPORT_SERVICE_CLIPBOARD, }; struct TransportInstance diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp new file mode 100644 index 00000000..50462e4a --- /dev/null +++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp @@ -0,0 +1,1434 @@ +/** + * 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 "transport/lgmp/CLGMPClipboardTransport.h" + +#include "CDebug.h" +#include "Seq.h" +#include "transport/lgmp/CLGMPHost.h" + +#include +#include + +namespace +{ + static constexpr DWORD WAIT_FIRST_OBJECT_VALUE = 0; + + static const LGMPQueueConfig CLIPBOARD_QUEUE_CONFIG = + { + LGMP_Q_CLIPBOARD, + LGMP_Q_CLIPBOARD_LEN, + 1000, + }; + + bool EmptyControl(const KVMFRClipboardMessage& message, + bool keepToken = false) + { + return !message.clipboardGeneration && !message.transfer && + !message.offset && !message.size && !message.format && + !message.flags && (keepToken || !message.token) && !message.length && + !message.sequence; + } + + bool AddValid(uint64_t offset, uint32_t length) + { + return offset <= (std::numeric_limits::max)() - length; + } + + bool ValidOffer(const KVMFRClipboardMessage& message) + { + return message.clipboardGeneration && message.token && + !(message.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) && + !message.transfer && !message.offset && !message.size && + !message.format && !message.flags && !message.length && + !message.sequence; + } + + bool ValidClear(const KVMFRClipboardMessage& message) + { + return message.clipboardGeneration && !message.token && + !message.transfer && !message.offset && !message.size && + !message.format && !message.flags && !message.length && + !message.sequence; + } + + bool ValidRequest(const KVMFRClipboardMessage& message, bool helper) + { + const bool validTransfer = helper ? + kvmfrClipboardTransferFromHelper(message.transfer) : + kvmfrClipboardTransferFromClient(message.transfer); + return message.clipboardGeneration && validTransfer && + kvmfrClipboardFormatValid(message.format) && + !message.offset && !message.size && !message.flags && + !message.token && !message.length && !message.sequence; + } + + bool ValidCancel(const KVMFRClipboardMessage& message) + { + return message.transfer && !message.offset && !message.size && + (!message.format || kvmfrClipboardFormatValid(message.format)) && + !message.flags && !message.length && !message.sequence; + } + + bool OwnerScopedLifecycle(const KVMFRClipboardMessage& message) + { + return message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST || + message.type == KVMFR_CLIPBOARD_MESSAGE_DATA || + message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL; + } +} + +CLGMPClipboardTransport::~CLGMPClipboardTransport() +{ + DeInit(); +} + +bool CLGMPClipboardTransport::Initialize() +{ + if (m_queue) + return true; + + LGMP_STATUS status = m_host.CreateQueue( + CLIPBOARD_QUEUE_CONFIG, &m_queue); + if (status != LGMP_OK) + { + DEBUG_ERROR("lgmpHostQueueCreate Failed (Clipboard): %s", + lgmpStatusString(status)); + return false; + } + + for (PLGMPMemory& memory : m_statusMemory) + { + status = m_host.Allocate(sizeof(KVMFRClipboardStatus), &memory); + if (status != LGMP_OK) + goto fail; + memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardStatus)); + } + + for (PLGMPMemory& memory : m_messageMemory) + { + status = m_host.Allocate(sizeof(KVMFRClipboardMessage), &memory); + if (status != LGMP_OK) + goto fail; + memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardMessage)); + } + + for (PLGMPMemory& memory : m_grantMemory) + { + status = m_host.Allocate(SLOT_BYTES, &memory); + if (status != LGMP_OK) + goto fail; + memset(lgmpHostMemPtr(memory), 0, SLOT_BYTES); + } + + for (PLGMPMemory& memory : m_dataMemory) + { + status = m_host.Allocate(SLOT_BYTES, &memory); + if (status != LGMP_OK) + goto fail; + memset(lgmpHostMemPtr(memory), 0, SLOT_BYTES); + } + + return true; + +fail: + DEBUG_ERROR("lgmpHostMemAlloc Failed (Clipboard): %s", + lgmpStatusString(status)); + DeInit(); + return false; +} + +void CLGMPClipboardTransport::DeInit() +{ + Stop(); + for (PLGMPMemory& memory : m_dataMemory) + lgmpHostMemFree(&memory); + for (PLGMPMemory& memory : m_grantMemory) + lgmpHostMemFree(&memory); + for (PLGMPMemory& memory : m_messageMemory) + lgmpHostMemFree(&memory); + for (PLGMPMemory& memory : m_statusMemory) + lgmpHostMemFree(&memory); + m_queue = nullptr; +} + +void CLGMPClipboardTransport::Wake() +{ + if (m_wakeEvent) + SetEvent(m_wakeEvent); +} + +bool CLGMPClipboardTransport::Start(IClipboardTarget& target) +{ + CSRWExclusiveLock lifecycleLock(m_lifecycleLock); + if (m_thread) + { + if (WaitForSingleObject(m_thread, 0) == WAIT_TIMEOUT) + return true; + + CloseHandle(m_thread); + CloseHandle(m_wakeEvent); + CloseHandle(m_stopEvent); + m_thread = nullptr; + m_wakeEvent = nullptr; + m_stopEvent = nullptr; + } + if (!m_queue) + return false; + + m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + m_wakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!m_stopEvent || !m_wakeEvent) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to create LGMP clipboard worker events"); + if (m_wakeEvent) + CloseHandle(m_wakeEvent); + if (m_stopEvent) + CloseHandle(m_stopEvent); + m_wakeEvent = nullptr; + m_stopEvent = nullptr; + return false; + } + + { + CSRWExclusiveLock lock(m_lock); + m_target = ⌖ + m_failed = false; + m_statusDirty = true; + } + m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr); + if (!m_thread) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to create LGMP clipboard worker"); + { + CSRWExclusiveLock lock(m_lock); + m_target = nullptr; + } + CloseHandle(m_wakeEvent); + CloseHandle(m_stopEvent); + m_wakeEvent = nullptr; + m_stopEvent = nullptr; + return false; + } + return true; +} + +void CLGMPClipboardTransport::Stop() +{ + CSRWExclusiveLock lifecycleLock(m_lifecycleLock); + if (m_stopEvent) + SetEvent(m_stopEvent); + if (m_thread) + WaitForSingleObject(m_thread, INFINITE); + + if (m_thread) + CloseHandle(m_thread); + if (m_wakeEvent) + CloseHandle(m_wakeEvent); + if (m_stopEvent) + CloseHandle(m_stopEvent); + m_thread = nullptr; + m_wakeEvent = nullptr; + m_stopEvent = nullptr; + + CSRWExclusiveLock lock(m_lock); + DropPendingTarget(); + ResetProtocol(false); + m_available = false; + m_endpointGeneration = 0; + m_target = nullptr; + m_failed = false; + m_statusDirty = true; +} + +bool CLGMPClipboardTransport::IsOwner( + uint32_t clientID, uint32_t generation) const +{ + return m_ownerClientID == clientID && + m_ownerGeneration == generation; +} + +bool CLGMPClipboardTransport::OwnerSubscribed() const +{ + if (!m_ownerClientID) + return false; + + uint32_t clients[LGMP_MAX_CLIENTS] = {}; + unsigned count = 0; + if (lgmpHostGetClientIDs(m_queue, clients, &count) != LGMP_OK) + return false; + for (unsigned i = 0; i < count; ++i) + if (clients[i] == m_ownerClientID) + return true; + return false; +} + +void CLGMPClipboardTransport::RenewLease() +{ + m_ownerDeadline = GetTickCount64() + OWNER_LEASE_MS; +} + +void CLGMPClipboardTransport::BlockOutbound( + const KVMFRClipboardMessage& record, uint32_t ownerClientID, + uint32_t ownerGeneration) +{ + m_outboundBlocked = true; + m_blockedOutbound = record; + m_blockedOwnerClientID = ownerClientID; + m_blockedOwnerGeneration = ownerGeneration; +} + +void CLGMPClipboardTransport::ClearOutboundBlock() +{ + m_outboundBlocked = false; + m_blockedOutbound = {}; + m_blockedOwnerClientID = 0; + m_blockedOwnerGeneration = 0; +} + +bool CLGMPClipboardTransport::BlockedOwnerLost( + const KVMFRClipboardMessage& record) const +{ + return m_outboundBlocked && OwnerScopedLifecycle(record) && + memcmp(&m_blockedOutbound, &record, sizeof(record)) == 0 && + (!m_available || record.generation != m_endpointGeneration || + m_blockedOwnerClientID != m_ownerClientID || + m_blockedOwnerGeneration != m_ownerGeneration); +} + +void CLGMPClipboardTransport::QueueHelperClear() +{ + if (!m_clientClipboardGeneration || !m_target || + !m_available || !m_endpointGeneration) + return; + + KVMFRClipboardMessage clear = {}; + clear.version = KVMFR_CLIPBOARD_VERSION; + clear.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; + clear.generation = m_endpointGeneration; + clear.clipboardGeneration = m_clientClipboardGeneration; + m_clientClipboardGeneration = 0; + m_clientFormats = 0; + QueueInternalTarget(clear); +} + +void CLGMPClipboardTransport::QueueTransferCancel( + const Transfer& transfer) +{ + if (!transfer.Active() || !m_target || !m_available || + !m_endpointGeneration) + return; + + KVMFRClipboardMessage cancel = {}; + cancel.version = KVMFR_CLIPBOARD_VERSION; + cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + cancel.generation = m_endpointGeneration; + cancel.clipboardGeneration = transfer.clipboardGeneration; + cancel.transfer = transfer.transfer; + cancel.format = transfer.format; + cancel.token = ERROR_DEVICE_NOT_CONNECTED; + QueueInternalTarget(cancel); +} + +void CLGMPClipboardTransport::QueueStaleRequestCancel( + const KVMFRClipboardMessage& request) +{ + if (request.type != KVMFR_CLIPBOARD_MESSAGE_REQUEST || + !m_target || !m_available || !m_endpointGeneration) + return; + + KVMFRClipboardMessage cancel = {}; + cancel.version = KVMFR_CLIPBOARD_VERSION; + cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + cancel.generation = m_endpointGeneration; + cancel.clipboardGeneration = request.clipboardGeneration; + cancel.transfer = request.transfer; + cancel.format = request.format; + cancel.token = ERROR_DEVICE_NOT_CONNECTED; + QueueInternalTarget(cancel); +} + +bool CLGMPClipboardTransport::QueueInternalTarget( + const KVMFRClipboardMessage& record) +{ + if (!m_pendingTarget.valid && !m_internalTargetCount) + return BeginTarget(record, nullptr, -2); + if (m_internalTargetCount == INTERNAL_TARGET_COUNT) + { + m_failed = true; + return false; + } + m_internalTarget[m_internalTargetCount++] = record; + return true; +} + +bool CLGMPClipboardTransport::PumpInternalTarget() +{ + if (m_pendingTarget.valid || !m_internalTargetCount) + return true; + + const KVMFRClipboardMessage record = m_internalTarget[0]; + for (unsigned i = 1; i < m_internalTargetCount; ++i) + m_internalTarget[i - 1] = m_internalTarget[i]; + m_internalTarget[--m_internalTargetCount] = {}; + return BeginTarget(record, nullptr, -2); +} + +void CLGMPClipboardTransport::ReleaseOwner( + const char * reason, bool clearHelper) +{ + if (!m_ownerClientID) + return; + + const uint32_t clientID = m_ownerClientID; + const uint32_t generation = m_ownerGeneration; + const Transfer clientToHelper = m_clientToHelper; + const Transfer helperToClient = m_helperToClient; + m_ownerClientID = 0; + m_ownerGeneration = 0; + m_ownerDeadline = 0; + m_replayPending = false; + m_clientToHelper.Clear(); + m_helperToClient.Clear(); + if (helperToClient.Active()) + m_discardHelperToClient = helperToClient.transfer; + for (Grant& grant : m_grants) + { + grant.generation = 0; + grant.offered = false; + grant.committed = false; + } + m_statusDirty = true; + + DEBUG_INFO("Clipboard owner %u generation %u released (%s)", + clientID, generation, reason); + if (clearHelper) + { + QueueTransferCancel(clientToHelper); + QueueTransferCancel(helperToClient); + QueueHelperClear(); + } + else + { + m_clientClipboardGeneration = 0; + m_clientFormats = 0; + } +} + +void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard) +{ + DropPendingTarget(); + if (m_ownerClientID) + ReleaseOwner("endpoint reset", false); + m_clientToHelper.Clear(); + m_helperToClient.Clear(); + m_clientClipboardGeneration = 0; + m_clientFormats = 0; + m_discardHelperToClient = 0; + m_replayPending = false; + ClearOutboundBlock(); + m_internalTargetCount = 0; + for (KVMFRClipboardMessage& record : m_internalTarget) + record = {}; + if (!keepClipboard) + { + m_cachedValid = false; + m_cachedClipboard = {}; + m_helperFormats = 0; + } + m_statusDirty = true; +} + +PLGMPMemory CLGMPClipboardTransport::FindAvailable( + PLGMPMemory (&memory)[MEMORY_COUNT]) const +{ + for (PLGMPMemory candidate : memory) + if (candidate && !lgmpHostQueuePayloadPending(m_queue, candidate)) + return candidate; + return nullptr; +} + +CLGMPClipboardTransport::PostResult +CLGMPClipboardTransport::PostForOwner( + uint64_t udata, PLGMPMemory memory) +{ + if (!m_ownerClientID) + return PostResult::GONE; + + unsigned recipients = 0; + const uint32_t clientID = m_ownerClientID; + const LGMP_STATUS status = lgmpHostQueuePostForClients( + m_queue, udata, memory, &clientID, 1, &recipients); + if (status == LGMP_ERR_QUEUE_FULL) + return PostResult::BUSY; + if (status != LGMP_OK) + { + Fail("lgmpHostQueuePostForClients", status); + return PostResult::FAILED; + } + return recipients ? PostResult::POSTED : PostResult::GONE; +} + +void CLGMPClipboardTransport::Fail( + const char * operation, LGMP_STATUS status) +{ + if (!m_failed) + DEBUG_ERROR("%s Failed (Clipboard): %s", + operation, lgmpStatusString(status)); + m_failed = true; + if (m_stopEvent) + SetEvent(m_stopEvent); +} + +bool CLGMPClipboardTransport::PublishStatus() +{ + if (lgmpHostQueueNewSubs(m_queue)) + m_statusDirty = true; + if (!m_statusDirty) + return true; + + uint32_t clients[LGMP_MAX_CLIENTS] = {}; + unsigned clientCount = 0; + LGMP_STATUS status = + lgmpHostGetClientIDs(m_queue, clients, &clientCount); + if (status != LGMP_OK) + { + Fail("lgmpHostGetClientIDs", status); + return false; + } + if (!clientCount) + return true; + + PLGMPMemory memory = FindAvailable(m_statusMemory); + if (!memory) + return true; + + KVMFRClipboardStatus clipboardStatus = {}; + clipboardStatus.version = KVMFR_CLIPBOARD_VERSION; + clipboardStatus.generation = m_endpointGeneration; + clipboardStatus.lease = static_cast(OWNER_LEASE_MS); + clipboardStatus.slotBytes = KVMFR_CLIPBOARD_DATA_BYTES; + if (m_available) + { + clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_AVAILABLE; + clipboardStatus.formats = m_helperFormats; + } + if (m_available && m_ownerClientID) + { + clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_HAS_OWNER; + clipboardStatus.ownerClientID = m_ownerClientID; + clipboardStatus.ownerGeneration = m_ownerGeneration; + } + memcpy(lgmpHostMemPtr(memory), &clipboardStatus, + sizeof(clipboardStatus)); + + const uint32_t serial = Seq::Next(m_statusSerial); + unsigned recipients = 0; + status = lgmpHostQueuePostForClients(m_queue, + KVMFR_CLIPBOARD_QUEUE_UDATA(KVMFR_CLIPBOARD_QUEUE_STATUS, serial), + memory, clients, clientCount, &recipients); + if (status == LGMP_ERR_QUEUE_FULL) + return true; + if (status != LGMP_OK) + { + Fail("lgmpHostQueuePostForClients", status); + return false; + } + if (recipients) + { + m_statusSerial = serial; + m_statusDirty = false; + } + return true; +} + +bool CLGMPClipboardTransport::PostGrants() +{ + if (!m_available || !m_ownerClientID) + return true; + + for (unsigned i = 0; i < MEMORY_COUNT; ++i) + { + Grant& grant = m_grants[i]; + if (grant.offered || grant.committed || + lgmpHostQueuePayloadPending(m_queue, m_grantMemory[i])) + continue; + + KVMFRClipboardSlotHeader header = {}; + header.version = KVMFR_CLIPBOARD_VERSION; + header.type = KVMFR_CLIPBOARD_MESSAGE_GRANT; + header.generation = m_ownerGeneration; + header.size = KVMFR_CLIPBOARD_DATA_BYTES; + header.token = i + 1; + memcpy(lgmpHostMemPtr(m_grantMemory[i]), &header, sizeof(header)); + + const PostResult result = PostForOwner( + KVMFR_CLIPBOARD_QUEUE_UDATA( + KVMFR_CLIPBOARD_QUEUE_GRANT, i + 1), m_grantMemory[i]); + if (result == PostResult::POSTED) + { + grant.generation = m_ownerGeneration; + grant.offered = true; + } + else if (result == PostResult::BUSY) + return true; + else if (result == PostResult::GONE) + { + ReleaseOwner("subscriber disappeared", true); + return true; + } + else + return false; + } + return true; +} + +bool CLGMPClipboardTransport::ReplayClipboard() +{ + if (!m_replayPending || !m_ownerClientID) + return true; + if (!m_cachedValid) + { + m_replayPending = false; + return true; + } + + PLGMPMemory memory = FindAvailable(m_messageMemory); + if (!memory) + return true; + KVMFRClipboardMessage message = m_cachedClipboard; + message.generation = m_ownerGeneration; + memcpy(lgmpHostMemPtr(memory), &message, sizeof(message)); + + const uint32_t serial = Seq::Next(m_messageSerial); + const PostResult result = PostForOwner( + KVMFR_CLIPBOARD_QUEUE_UDATA( + KVMFR_CLIPBOARD_QUEUE_MESSAGE, serial), memory); + if (result == PostResult::POSTED) + { + m_messageSerial = serial; + m_replayPending = false; + } + else if (result == PostResult::GONE) + ReleaseOwner("subscriber disappeared", true); + return result != PostResult::FAILED; +} + +bool CLGMPClipboardTransport::ValidateClaim( + const KVMFRClipboardMessage& message) const +{ + return message.version == KVMFR_CLIPBOARD_VERSION && + message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM && + message.generation && message.token == m_endpointGeneration && + EmptyControl(message, true); +} + +bool CLGMPClipboardTransport::ValidateOwnedControl( + const KVMFRClipboardMessage& message) const +{ + if (message.version != KVMFR_CLIPBOARD_VERSION || + message.generation != m_ownerGeneration) + return false; + + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_RELEASE: + case KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE: + return EmptyControl(message); + + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + return ValidateInboundRecord(message); + + case KVMFR_CLIPBOARD_MESSAGE_COMMIT: + return message.token >= 1 && message.token <= MEMORY_COUNT && + message.clipboardGeneration && message.transfer && + kvmfrClipboardFormatValid(message.format) && + message.length <= KVMFR_CLIPBOARD_DATA_BYTES && + !(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END)) && + AddValid(message.offset, message.length); + + default: + return false; + } +} + +bool CLGMPClipboardTransport::ValidateInboundRecord( + const KVMFRClipboardMessage& message) const +{ + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + return ValidOffer(message); + + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + return ValidClear(message); + + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + return ValidRequest(message, false); + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + return ValidCancel(message); + + default: + return false; + } +} + +bool CLGMPClipboardTransport::ValidateOutboundRecord( + const KVMFRClipboardMessage& message) const +{ + if (message.version != KVMFR_CLIPBOARD_VERSION || + message.generation != m_endpointGeneration) + return false; + + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + return ValidOffer(message); + + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + return ValidClear(message); + + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + return ValidRequest(message, true); + + case KVMFR_CLIPBOARD_MESSAGE_DATA: + return message.clipboardGeneration && + kvmfrClipboardTransferFromClient(message.transfer) && + kvmfrClipboardFormatValid(message.format) && + message.length <= KVMFR_CLIPBOARD_DATA_BYTES && + !(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END)) && !message.token && + AddValid(message.offset, message.length); + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + return ValidCancel(message); + + default: + return false; + } +} + +bool CLGMPClipboardTransport::ValidateChunk( + const Transfer& transfer, const KVMFRClipboardMessage& message) const +{ + if (!transfer.Active() || message.transfer != transfer.transfer || + message.clipboardGeneration != transfer.clipboardGeneration || + message.format != transfer.format || + message.offset != transfer.nextOffset || + message.sequence != transfer.nextSequence || + !AddValid(message.offset, message.length)) + return false; + + const uint64_t end = message.offset + message.length; + if (!message.length && + !(message.flags & (KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END))) + return false; + if (!transfer.began) + { + if (message.offset || message.sequence || + !(message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) + return false; + } + else if (message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN) + return false; + else if (!(message.flags & KVMFR_CLIPBOARD_FLAG_END) && + message.size != KVMFR_CLIPBOARD_SIZE_UNKNOWN) + return false; + + const uint64_t hint = transfer.began ? transfer.sizeHint : message.size; + if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint) + return false; + if (message.flags & KVMFR_CLIPBOARD_FLAG_END) + return message.size == end && + (hint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || hint == end); + return true; +} + +void CLGMPClipboardTransport::AdvanceChunk( + Transfer& transfer, const KVMFRClipboardMessage& message) +{ + if (!transfer.began) + { + transfer.began = true; + transfer.sizeHint = message.size; + } + transfer.nextOffset += message.length; + ++transfer.nextSequence; + if (message.flags & KVMFR_CLIPBOARD_FLAG_END) + transfer.Clear(); +} + +void CLGMPClipboardTransport::ApplyInbound( + const KVMFRClipboardMessage& message) +{ + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + m_clientClipboardGeneration = message.clipboardGeneration; + m_clientFormats = message.token; + m_clientToHelper.Clear(); + break; + + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + m_clientClipboardGeneration = 0; + m_clientFormats = 0; + m_clientToHelper.Clear(); + break; + + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + m_helperToClient.Clear(); + m_helperToClient.transfer = message.transfer; + m_helperToClient.clipboardGeneration = + message.clipboardGeneration; + m_helperToClient.format = message.format; + break; + + case KVMFR_CLIPBOARD_MESSAGE_DATA: + AdvanceChunk(m_clientToHelper, message); + break; + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + if (m_clientToHelper.transfer == message.transfer) + m_clientToHelper.Clear(); + if (m_helperToClient.transfer == message.transfer) + m_helperToClient.Clear(); + break; + } +} + +void CLGMPClipboardTransport::ApplyOutbound( + const KVMFRClipboardMessage& message) +{ + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + m_cachedClipboard = message; + m_cachedValid = true; + m_helperFormats = message.token; + m_helperToClient.Clear(); + m_replayPending = false; + m_statusDirty = true; + break; + + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + m_cachedClipboard = message; + m_cachedValid = true; + m_helperFormats = 0; + m_helperToClient.Clear(); + m_replayPending = false; + m_statusDirty = true; + break; + + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + m_clientToHelper.Clear(); + m_clientToHelper.transfer = message.transfer; + m_clientToHelper.clipboardGeneration = + message.clipboardGeneration; + m_clientToHelper.format = message.format; + break; + + case KVMFR_CLIPBOARD_MESSAGE_DATA: + AdvanceChunk(m_helperToClient, message); + break; + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + if (m_clientToHelper.transfer == message.transfer) + m_clientToHelper.Clear(); + if (m_helperToClient.transfer == message.transfer) + m_helperToClient.Clear(); + break; + } +} + +bool CLGMPClipboardTransport::BeginTarget( + const KVMFRClipboardMessage& message, const uint8_t * data, int grant) +{ + if (m_pendingTarget.valid || + (message.length && !data) || + message.length > KVMFR_CLIPBOARD_DATA_BYTES) + return false; + + m_pendingTarget.valid = true; + m_pendingTarget.acknowledge = grant >= -1; + m_pendingTarget.grant = grant; + m_pendingTarget.record = message; + if (message.length) + memcpy(m_pendingTarget.data, data, message.length); + return RetryTarget(); +} + +void CLGMPClipboardTransport::FinishTarget(bool accepted) +{ + const KVMFRClipboardMessage message = m_pendingTarget.record; + const int grant = m_pendingTarget.grant; + if (accepted) + { + ApplyInbound(message); + RenewLease(); + } + if (grant >= 0 && static_cast(grant) < MEMORY_COUNT) + m_grants[grant] = {}; + m_pendingTarget.Clear(); + + if (!accepted) + { + ReleaseOwner("Helper delivery failed", false); + m_failed = true; + } +} + +bool CLGMPClipboardTransport::RetryTarget() +{ + if (!m_pendingTarget.valid) + return true; + + const bool acknowledge = m_pendingTarget.acknowledge; + const uint8_t * data = m_pendingTarget.record.length ? + m_pendingTarget.data : nullptr; + const ClipboardChannelResult result = m_target ? + m_target->SendClipboard(m_pendingTarget.record, data) : + ClipboardChannelResult::FAILED; + if (result == ClipboardChannelResult::BUSY) + { + if (m_ownerClientID) + RenewLease(); + return true; + } + + FinishTarget(result == ClipboardChannelResult::ACCEPTED); + if (acknowledge) + { + const LGMP_STATUS status = lgmpHostAckData(m_queue); + if (status != LGMP_OK) + { + Fail("lgmpHostAckData", status); + return false; + } + } + return true; +} + +void CLGMPClipboardTransport::DropPendingTarget() +{ + if (!m_pendingTarget.valid) + return; + + const bool acknowledge = m_pendingTarget.acknowledge; + const int grant = m_pendingTarget.grant; + if (grant >= 0 && static_cast(grant) < MEMORY_COUNT) + m_grants[grant] = {}; + m_pendingTarget.Clear(); + if (acknowledge && m_queue) + { + const LGMP_STATUS status = lgmpHostAckData(m_queue); + if (status != LGMP_OK) + Fail("lgmpHostAckData", status); + } +} + +bool CLGMPClipboardTransport::ProcessMessage( + uint32_t clientID, const KVMFRClipboardMessage& message) +{ + if (message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM) + { + if (!m_available || !ValidateClaim(message)) + return true; + if (m_ownerClientID) + { + if (IsOwner(clientID, message.generation)) + RenewLease(); + return true; + } + + m_ownerClientID = clientID; + m_ownerGeneration = message.generation; + RenewLease(); + m_statusDirty = true; + m_replayPending = m_cachedValid; + DEBUG_INFO("Clipboard owner %u generation %u acquired", + m_ownerClientID, m_ownerGeneration); + return true; + } + + if (!IsOwner(clientID, message.generation)) + return true; + if (!ValidateOwnedControl(message)) + { + ReleaseOwner("invalid clipboard message", true); + return true; + } + + if (message.type == KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE) + { + RenewLease(); + return true; + } + if (message.type == KVMFR_CLIPBOARD_MESSAGE_RELEASE) + { + ReleaseOwner("client release", true); + return true; + } + + if (message.type == KVMFR_CLIPBOARD_MESSAGE_COMMIT) + { + const unsigned grantIndex = message.token - 1; + Grant& grant = m_grants[grantIndex]; + if (!grant.offered || grant.committed || + grant.generation != m_ownerGeneration) + { + ReleaseOwner("invalid clipboard grant", true); + return true; + } + + const uint8_t * slot = static_cast( + lgmpHostMemPtr(m_grantMemory[grantIndex])); + KVMFRClipboardMessage dataMessage = {}; + memcpy(&dataMessage, slot, sizeof(dataMessage)); + const bool matchesCommit = + dataMessage.version == KVMFR_CLIPBOARD_VERSION && + dataMessage.type == KVMFR_CLIPBOARD_MESSAGE_DATA && + dataMessage.generation == m_ownerGeneration && + dataMessage.clipboardGeneration == message.clipboardGeneration && + dataMessage.transfer == message.transfer && + dataMessage.offset == message.offset && + dataMessage.size == message.size && + dataMessage.format == message.format && + dataMessage.flags == message.flags && + dataMessage.length == message.length && + dataMessage.sequence == message.sequence && + !dataMessage.token; + if (!matchesCommit || + !kvmfrClipboardTransferFromHelper(dataMessage.transfer) || + !ValidateChunk(m_clientToHelper, dataMessage)) + { + ReleaseOwner("invalid clipboard commit", true); + return true; + } + + grant.committed = true; + dataMessage.generation = m_endpointGeneration; + RenewLease(); + BeginTarget(dataMessage, + slot + sizeof(KVMFRClipboardSlotHeader), + static_cast(grantIndex)); + return false; + } + + if (message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST && + m_helperToClient.Active()) + { + ReleaseOwner("overlapping clipboard request", true); + return true; + } + if (message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST && + (!m_cachedValid || + m_cachedClipboard.type != KVMFR_CLIPBOARD_MESSAGE_OFFER || + message.clipboardGeneration != + m_cachedClipboard.clipboardGeneration || + !(m_helperFormats & kvmfrClipboardFormatFlag(message.format)))) + { + ReleaseOwner("invalid clipboard request", true); + return true; + } + if (message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL && + message.transfer != m_clientToHelper.transfer && + message.transfer != m_helperToClient.transfer) + return true; + KVMFRClipboardMessage forwarded = message; + forwarded.generation = m_endpointGeneration; + RenewLease(); + BeginTarget(forwarded, nullptr, -1); + return false; +} + +bool CLGMPClipboardTransport::DrainMessage() +{ + for (unsigned count = 0; count < 64 && !m_pendingTarget.valid; ++count) + { + uint8_t data[LGMP_MSGS_SIZE] = {}; + size_t size = 0; + uint32_t clientID = 0; + const LGMP_STATUS status = lgmpHostReadDataWithSource( + m_queue, data, &size, &clientID); + if (status == LGMP_ERR_QUEUE_EMPTY) + return true; + if (status != LGMP_OK) + { + Fail("lgmpHostReadDataWithSource", status); + return false; + } + + bool acknowledge = true; + if (size != sizeof(KVMFRClipboardMessage)) + { + if (clientID == m_ownerClientID) + ReleaseOwner("invalid clipboard message size", true); + } + else + { + KVMFRClipboardMessage message = {}; + memcpy(&message, data, sizeof(message)); + acknowledge = ProcessMessage(clientID, message); + } + + if (acknowledge) + { + const LGMP_STATUS ackStatus = lgmpHostAckData(m_queue); + if (ackStatus != LGMP_OK) + { + Fail("lgmpHostAckData", ackStatus); + return false; + } + } + } + return true; +} + +ClipboardChannelResult CLGMPClipboardTransport::SendControl( + const KVMFRClipboardMessage& record) +{ + PLGMPMemory memory = FindAvailable(m_messageMemory); + if (!memory) + return ClipboardChannelResult::BUSY; + + KVMFRClipboardMessage message = record; + message.generation = m_ownerGeneration; + memcpy(lgmpHostMemPtr(memory), &message, sizeof(message)); + const uint32_t serial = Seq::Next(m_messageSerial); + const PostResult result = PostForOwner( + KVMFR_CLIPBOARD_QUEUE_UDATA( + KVMFR_CLIPBOARD_QUEUE_MESSAGE, serial), memory); + if (result == PostResult::POSTED) + { + m_messageSerial = serial; + ApplyOutbound(record); + return ClipboardChannelResult::ACCEPTED; + } + if (result == PostResult::BUSY) + return ClipboardChannelResult::BUSY; + if (result == PostResult::GONE) + { + ReleaseOwner("subscriber disappeared", true); + return record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || + record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR ? + ClipboardChannelResult::ACCEPTED : ClipboardChannelResult::BUSY; + } + return ClipboardChannelResult::FAILED; +} + +ClipboardChannelResult CLGMPClipboardTransport::SendData( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + if (!ValidateChunk(m_helperToClient, record)) + return ClipboardChannelResult::FAILED; + + PLGMPMemory memory = FindAvailable(m_dataMemory); + if (!memory) + return ClipboardChannelResult::BUSY; + + KVMFRClipboardMessage message = record; + message.generation = m_ownerGeneration; + uint8_t * slot = static_cast(lgmpHostMemPtr(memory)); + memcpy(slot, &message, sizeof(message)); + if (message.length) + memcpy(slot + sizeof(KVMFRClipboardSlotHeader), data, message.length); + + const uint32_t serial = Seq::Next(m_dataSerial); + const PostResult result = PostForOwner( + KVMFR_CLIPBOARD_QUEUE_UDATA( + KVMFR_CLIPBOARD_QUEUE_DATA, serial), memory); + if (result == PostResult::POSTED) + { + m_dataSerial = serial; + ApplyOutbound(record); + return ClipboardChannelResult::ACCEPTED; + } + if (result == PostResult::BUSY) + return ClipboardChannelResult::BUSY; + if (result == PostResult::GONE) + { + ReleaseOwner("subscriber disappeared", true); + return ClipboardChannelResult::ACCEPTED; + } + return ClipboardChannelResult::FAILED; +} + +ClipboardChannelResult CLGMPClipboardTransport::SendClipboard( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + CSRWExclusiveLock lock(m_lock); + if (m_failed || !m_target) + return ClipboardChannelResult::FAILED; + + // The channel retains a BUSY record for an exact retry. Once the owner + // epoch that caused the backpressure is gone, that record cannot be + // delivered to a replacement owner. Consume it and promptly cancel a + // stale request back toward Helper instead. + if (BlockedOwnerLost(record)) + { + QueueStaleRequestCancel(record); + ClearOutboundBlock(); + Wake(); + return ClipboardChannelResult::ACCEPTED; + } + + if (!m_available || + record.generation != m_endpointGeneration) + return ClipboardChannelResult::FAILED; + + if (!ValidateOutboundRecord(record) || + (record.length && !data)) + return ClipboardChannelResult::FAILED; + + if (record.transfer && + record.transfer == m_discardHelperToClient && + (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA || + record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL)) + { + if (record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL || + (record.flags & KVMFR_CLIPBOARD_FLAG_END)) + m_discardHelperToClient = 0; + ClearOutboundBlock(); + return ClipboardChannelResult::ACCEPTED; + } + + if (record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL && + record.transfer != m_clientToHelper.transfer && + record.transfer != m_helperToClient.transfer) + { + ClearOutboundBlock(); + return ClipboardChannelResult::ACCEPTED; + } + + // Preserve the latest Helper clipboard even when no client owns the + // endpoint. It is replayed to the next successful claimant. + if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || + record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR) + { + m_cachedClipboard = record; + m_cachedValid = true; + m_helperFormats = record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ? + record.token : 0; + m_statusDirty = true; + if (!m_ownerClientID) + { + ClearOutboundBlock(); + Wake(); + return ClipboardChannelResult::ACCEPTED; + } + } + + if (!m_ownerClientID) + { + if (OwnerScopedLifecycle(record)) + { + QueueStaleRequestCancel(record); + ClearOutboundBlock(); + Wake(); + return ClipboardChannelResult::ACCEPTED; + } + return ClipboardChannelResult::FAILED; + } + + const uint32_t ownerClientID = m_ownerClientID; + const uint32_t ownerGeneration = m_ownerGeneration; + if (!OwnerSubscribed()) + { + BlockOutbound(record, ownerClientID, ownerGeneration); + Wake(); + return ClipboardChannelResult::BUSY; + } + + ClipboardChannelResult result; + if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA) + { + if (!m_helperToClient.Active() || + record.transfer != m_helperToClient.transfer) + { + ClearOutboundBlock(); + return ClipboardChannelResult::ACCEPTED; + } + result = SendData(record, data); + } + else + { + if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST && + m_clientToHelper.Active()) + return ClipboardChannelResult::FAILED; + if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST && + (record.clipboardGeneration != m_clientClipboardGeneration || + !(m_clientFormats & kvmfrClipboardFormatFlag(record.format)))) + { + QueueStaleRequestCancel(record); + ClearOutboundBlock(); + return ClipboardChannelResult::ACCEPTED; + } + result = SendControl(record); + } + + if (result == ClipboardChannelResult::BUSY) + BlockOutbound(record, ownerClientID, ownerGeneration); + else + ClearOutboundBlock(); + Wake(); + return result; +} + +void CLGMPClipboardTransport::ClipboardState( + bool available, uint32_t generation) +{ + CSRWExclusiveLock lock(m_lock); + if (!generation) + return; + + if (generation != m_endpointGeneration || available != m_available) + { + DropPendingTarget(); + m_internalTargetCount = 0; + for (KVMFRClipboardMessage& record : m_internalTarget) + record = {}; + if (m_ownerClientID) + ReleaseOwner("Helper endpoint changed", false); + m_clientToHelper.Clear(); + m_helperToClient.Clear(); + m_clientClipboardGeneration = 0; + m_clientFormats = 0; + m_discardHelperToClient = 0; + m_cachedClipboard = {}; + m_cachedValid = false; + m_helperFormats = 0; + } + m_available = available; + m_endpointGeneration = generation; + m_statusDirty = true; + Wake(); +} + +void CLGMPClipboardTransport::ClipboardReset( + uint32_t generation, uint32_t reason) +{ + UNREFERENCED_PARAMETER(reason); + CSRWExclusiveLock lock(m_lock); + if (!generation) + return; + DropPendingTarget(); + m_internalTargetCount = 0; + for (KVMFRClipboardMessage& record : m_internalTarget) + record = {}; + if (m_ownerClientID) + ReleaseOwner("Helper reset", false); + m_clientToHelper.Clear(); + m_helperToClient.Clear(); + m_clientClipboardGeneration = 0; + m_clientFormats = 0; + m_discardHelperToClient = 0; + m_cachedClipboard = {}; + m_cachedValid = false; + m_helperFormats = 0; + m_endpointGeneration = generation; + m_statusDirty = true; + Wake(); +} + +DWORD CALLBACK CLGMPClipboardTransport::ThreadProc(void * context) +{ + static_cast(context)->Thread(); + return 0; +} + +void CLGMPClipboardTransport::Thread() +{ + bool notifyFailed = false; + for (;;) + { + DWORD timeout = IDLE_POLL_MS; + bool notifyReady = false; + IClipboardTarget * readyTarget = nullptr; + { + CSRWExclusiveLock lock(m_lock); + if (m_failed) + { + notifyFailed = true; + break; + } + + if (m_ownerClientID && !OwnerSubscribed()) + { + DropPendingTarget(); + ReleaseOwner("subscriber disappeared", true); + } + else if (m_ownerClientID && GetTickCount64() >= m_ownerDeadline) + ReleaseOwner("lease expired", true); + + if (!RetryTarget() || !PumpInternalTarget() || + !DrainMessage() || !PublishStatus() || + !ReplayClipboard() || !PostGrants()) + { + notifyFailed = true; + break; + } + + if (m_outboundBlocked) + { + notifyReady = m_target != nullptr; + readyTarget = m_target; + } + if (m_pendingTarget.valid || m_ownerClientID) + timeout = ACTIVE_POLL_MS; + } + + // ClipboardReceiveReady may synchronously retry SendClipboard, which + // acquires m_lock. Never invoke it while holding the transport lock. + if (notifyReady) + readyTarget->ClipboardReceiveReady(); + + const HANDLE handles[] = { m_stopEvent, m_wakeEvent }; + const DWORD wait = WaitForMultipleObjects( + _countof(handles), handles, FALSE, timeout); + if (wait == WAIT_FIRST_OBJECT_VALUE) + break; + if (wait != WAIT_FIRST_OBJECT_VALUE + 1 && wait != WAIT_TIMEOUT) + { + DEBUG_ERROR_HR(GetLastError(), + "LGMP clipboard worker wait failed"); + notifyFailed = true; + break; + } + } + + IClipboardTarget * target = nullptr; + { + CSRWExclusiveLock lock(m_lock); + DropPendingTarget(); + if (m_ownerClientID) + ReleaseOwner("transport stopped", false); + target = m_target; + } + if (notifyFailed && target) + target->ClipboardFailed(); +} diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h new file mode 100644 index 00000000..c7ea59f5 --- /dev/null +++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h @@ -0,0 +1,211 @@ +/** + * 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 + */ + +#pragma once + +#include "CSRWLock.h" +#include "transport/IClipboardSource.h" + +#include "common/KVMFRClipboard.h" +#include "common/LGMPConfig.h" + +#include + +#include + +extern "C" { + #include "lgmp/host.h" +} + +class CLGMPHost; + +class CLGMPClipboardTransport final : public IClipboardSource +{ +private: + static constexpr unsigned MEMORY_COUNT = 2; + static constexpr unsigned INTERNAL_TARGET_COUNT = 3; + static constexpr ULONGLONG OWNER_LEASE_MS = 1000; + static constexpr DWORD ACTIVE_POLL_MS = 5; + static constexpr DWORD IDLE_POLL_MS = 50; + static constexpr uint32_t SLOT_BYTES = + sizeof(KVMFRClipboardSlotHeader) + KVMFR_CLIPBOARD_DATA_BYTES; + + enum class PostResult + { + POSTED, + BUSY, + GONE, + FAILED, + }; + + struct Transfer + { + uint64_t transfer = 0; + uint64_t clipboardGeneration = 0; + uint64_t nextOffset = 0; + uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_NONE; + uint32_t nextSequence = 0; + bool began = false; + + void Clear() { *this = {}; } + bool Active() const { return transfer != 0; } + }; + + struct Grant + { + uint32_t generation = 0; + bool offered = false; + bool committed = false; + }; + + struct PendingTarget + { + bool valid = false; + bool acknowledge = false; + int grant = -1; + KVMFRClipboardMessage record = {}; + uint8_t data[KVMFR_CLIPBOARD_DATA_BYTES] = {}; + + void Clear() + { + valid = false; + acknowledge = false; + grant = -1; + record = {}; + } + }; + + CLGMPHost& m_host; + + PLGMPHostQueue m_queue = nullptr; + PLGMPMemory m_statusMemory [MEMORY_COUNT] = {}; + PLGMPMemory m_messageMemory[MEMORY_COUNT] = {}; + PLGMPMemory m_grantMemory [MEMORY_COUNT] = {}; + PLGMPMemory m_dataMemory [MEMORY_COUNT] = {}; + + CSRWLock m_lifecycleLock; + CSRWLock m_lock; + IClipboardTarget * m_target = nullptr; + HANDLE m_stopEvent = nullptr; + HANDLE m_wakeEvent = nullptr; + HANDLE m_thread = nullptr; + + bool m_available = false; + bool m_statusDirty = true; + bool m_cachedValid = false; + bool m_replayPending = false; + bool m_outboundBlocked = false; + bool m_failed = false; + uint32_t m_endpointGeneration = 0; + uint32_t m_ownerClientID = 0; + uint32_t m_ownerGeneration = 0; + uint32_t m_statusSerial = 0; + uint32_t m_messageSerial = 0; + uint32_t m_dataSerial = 0; + uint32_t m_helperFormats = 0; + uint32_t m_clientFormats = 0; + uint32_t m_blockedOwnerClientID = 0; + uint32_t m_blockedOwnerGeneration = 0; + uint64_t m_ownerDeadline = 0; + uint64_t m_clientClipboardGeneration = 0; + uint64_t m_discardHelperToClient = 0; + KVMFRClipboardMessage m_cachedClipboard = {}; + KVMFRClipboardMessage m_blockedOutbound = {}; + Transfer m_clientToHelper; + Transfer m_helperToClient; + Grant m_grants[MEMORY_COUNT]; + PendingTarget m_pendingTarget; + KVMFRClipboardMessage m_internalTarget[INTERNAL_TARGET_COUNT] = {}; + unsigned m_internalTargetCount = 0; + + bool Initialize(); + void DeInit(); + + static DWORD CALLBACK ThreadProc(void * context); + void Thread(); + void Wake(); + + bool IsOwner(uint32_t clientID, uint32_t generation) const; + bool OwnerSubscribed() const; + void RenewLease(); + void BlockOutbound(const KVMFRClipboardMessage& record, + uint32_t ownerClientID, uint32_t ownerGeneration); + void ClearOutboundBlock(); + bool BlockedOwnerLost(const KVMFRClipboardMessage& record) const; + void ReleaseOwner(const char * reason, bool clearHelper); + void ResetProtocol(bool keepClipboard); + void QueueHelperClear(); + void QueueTransferCancel(const Transfer& transfer); + void QueueStaleRequestCancel( + const KVMFRClipboardMessage& request); + bool QueueInternalTarget(const KVMFRClipboardMessage& record); + bool PumpInternalTarget(); + + PLGMPMemory FindAvailable(PLGMPMemory (&memory)[MEMORY_COUNT]) const; + PostResult PostForOwner(uint64_t udata, PLGMPMemory memory); + bool PublishStatus(); + bool PostGrants(); + bool ReplayClipboard(); + bool DrainMessage(); + bool ProcessMessage(uint32_t clientID, + const KVMFRClipboardMessage& message); + bool RetryTarget(); + void FinishTarget(bool accepted); + void DropPendingTarget(); + void Fail(const char * operation, LGMP_STATUS status); + + bool ValidateClaim(const KVMFRClipboardMessage& message) const; + bool ValidateOwnedControl(const KVMFRClipboardMessage& message) const; + bool ValidateInboundRecord(const KVMFRClipboardMessage& message) const; + bool ValidateOutboundRecord(const KVMFRClipboardMessage& message) const; + bool ValidateChunk(const Transfer& transfer, + const KVMFRClipboardMessage& message) const; + void AdvanceChunk(Transfer& transfer, + const KVMFRClipboardMessage& message); + void ApplyInbound(const KVMFRClipboardMessage& message); + void ApplyOutbound(const KVMFRClipboardMessage& message); + bool BeginTarget(const KVMFRClipboardMessage& message, + const uint8_t * data, int grant); + + ClipboardChannelResult SendControl( + const KVMFRClipboardMessage& record); + ClipboardChannelResult SendData( + const KVMFRClipboardMessage& record, const uint8_t * data); + + friend class CLGMPTransport; + +public: + explicit CLGMPClipboardTransport(CLGMPHost& host) : + m_host(host) {} + ~CLGMPClipboardTransport() override; + + CLGMPClipboardTransport(const CLGMPClipboardTransport&) = delete; + CLGMPClipboardTransport& operator=( + const CLGMPClipboardTransport&) = delete; + + bool Start(IClipboardTarget& target) override; + void Stop() override; + void ClipboardState(bool available, uint32_t generation) override; + ClipboardChannelResult SendClipboard( + const KVMFRClipboardMessage& record, + const uint8_t * data) override; + void ClipboardReset(uint32_t generation, uint32_t reason) override; +}; diff --git a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp index 011184b6..006eb3d0 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPHost.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPHost.cpp @@ -52,7 +52,8 @@ bool CLGMPHost::Initialize(CIVSHMEM& ivshmem) KVMFR_FEATURE_SETCURSORPOS | KVMFR_FEATURE_WINDOWSIZE | KVMFR_FEATURE_FRAME_SCHEDULE | - KVMFR_FEATURE_INPUT; + KVMFR_FEATURE_INPUT | + KVMFR_FEATURE_CLIPBOARD; strncpy_s(kvmfr.hostver, LG_VERSION_STR, sizeof(kvmfr.hostver) - 1); ss.write(reinterpret_cast(&kvmfr), sizeof(kvmfr)); } diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp index fe352b6a..cef0e070 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp @@ -67,7 +67,8 @@ CLGMPTransport::CLGMPTransport(const TransportInstance& config) : m_config(config), m_control(m_host), m_frames(m_host, m_ivshmem), - m_input(m_host) + m_input(m_host), + m_clipboard(m_host) { } @@ -90,10 +91,10 @@ bool CLGMPTransport::Initialize() if (!m_host.Initialize(m_ivshmem)) return false; - // Preserve the existing shared-memory layout by appending input after the - // frame and pointer queues and retained pointer state allocations. + // Preserve the existing shared-memory layout by appending input and then + // clipboard after the frame/pointer queues and retained pointer state. if (!m_frames.Initialize() || !m_control.Initialize() || - !m_input.Initialize()) + !m_input.Initialize() || !m_clipboard.Initialize()) return false; m_frames.SealMemoryLayout(); @@ -242,6 +243,7 @@ void CLGMPTransport::Stop() Atomic::Store(m_ready, false, std::memory_order_release); Abort(); m_hasActive = false; + m_clipboard.Stop(); m_input.Stop(); } diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.h b/idd/LGIdd/transport/lgmp/CLGMPTransport.h index 23b9f867..3b392446 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPTransport.h +++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.h @@ -23,6 +23,7 @@ #include "Atomic.h" #include "transport/ITransport.h" #include "transport/lgmp/CIVSHMEM.h" +#include "transport/lgmp/CLGMPClipboardTransport.h" #include "transport/lgmp/CLGMPControl.h" #include "transport/lgmp/CLGMPFrameTransport.h" #include "transport/lgmp/CLGMPHost.h" @@ -32,20 +33,21 @@ class CLGMPTransport final : public ITransport { private: - TransportInstance m_config; + TransportInstance m_config; // Keep this declaration order. Destruction must release frame and control // allocations before the LGMP host and its IVSHMEM mapping are destroyed. - CIVSHMEM m_ivshmem; - CLGMPHost m_host; - CLGMPControl m_control; - CLGMPFrameTransport m_frames; - CLGMPInputTransport m_input; - CRecovery m_recovery; - std::atomic m_ready = false; - FrameCfg m_activeCfg; - FrameCfg m_pendingCfg; - bool m_hasActive = false; - bool m_hasPending = false; + CIVSHMEM m_ivshmem; + CLGMPHost m_host; + CLGMPControl m_control; + CLGMPFrameTransport m_frames; + CLGMPInputTransport m_input; + CLGMPClipboardTransport m_clipboard; + CRecovery m_recovery; + std::atomic m_ready = false; + FrameCfg m_activeCfg; + FrameCfg m_pendingCfg; + bool m_hasActive = false; + bool m_hasPending = false; public: explicit CLGMPTransport(const TransportInstance& config); @@ -76,4 +78,5 @@ public: IFrameSink * FrameSink() override { return &m_frames; } IControlSink * Control() override { return &m_control; } IInputSource * Input() override { return &m_input; } + IClipboardSource * Clipboard() override { return &m_clipboard; } }; diff --git a/idd/LGIddHelper/CClipboardManager.cpp b/idd/LGIddHelper/CClipboardManager.cpp new file mode 100644 index 00000000..4290c93c --- /dev/null +++ b/idd/LGIddHelper/CClipboardManager.cpp @@ -0,0 +1,2294 @@ +/** + * 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 + +#include +#include +#include +#include +#include +#include + +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 REMOTE_RETRY_MS = 250; + static constexpr DWORD REMOTE_RETRY_TIMEOUT_MS = 5000; + 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 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(data); + while (size) + { + const DWORD chunk = static_cast((std::min)( + size, (std::numeric_limits::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(data); + while (size) + { + const DWORD chunk = static_cast((std::min)( + size, (std::numeric_limits::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& output, uint32_t codepoint) + { + if (codepoint <= 0x7f) + output.push_back(static_cast(codepoint)); + else if (codepoint <= 0x7ff) + { + output.push_back(static_cast(0xc0 | (codepoint >> 6))); + output.push_back(static_cast(0x80 | (codepoint & 0x3f))); + } + else if (codepoint <= 0xffff) + { + output.push_back(static_cast(0xe0 | (codepoint >> 12))); + output.push_back(static_cast(0x80 | + ((codepoint >> 6) & 0x3f))); + output.push_back(static_cast(0x80 | (codepoint & 0x3f))); + } + else + { + output.push_back(static_cast(0xf0 | (codepoint >> 18))); + output.push_back(static_cast(0x80 | + ((codepoint >> 12) & 0x3f))); + output.push_back(static_cast(0x80 | + ((codepoint >> 6) & 0x3f))); + output.push_back(static_cast(0x80 | (codepoint & 0x3f))); + } + } + + uint32_t DIBPixelOffset(const uint8_t * data, size_t size) + { + if (size < sizeof(DWORD)) + return 0; + + const DWORD headerSize = *reinterpret_cast(data); + if (headerSize < sizeof(BITMAPCOREHEADER) || headerSize > size) + return 0; + + uint64_t offset = headerSize; + if (headerSize == sizeof(BITMAPCOREHEADER)) + { + const BITMAPCOREHEADER * header = + reinterpret_cast(data); + if (header->bcBitCount <= 8) + offset += (1ULL << header->bcBitCount) * sizeof(RGBTRIPLE); + } + else if (headerSize >= sizeof(BITMAPINFOHEADER)) + { + const BITMAPINFOHEADER * header = + reinterpret_cast(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(colors) * sizeof(RGBQUAD); + } + else + return 0; + + return offset <= size && + offset <= (std::numeric_limits::max)() ? + static_cast(offset) : 0; + } +} + +class CClipboardSpool +{ +private: + std::vector 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(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().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(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(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 + bool ForEachUTF8(CClipboardSpool& spool, Callback callback) + { + uint32_t codepoint = 0; + uint32_t minimum = 0; + unsigned int remaining = 0; + std::vector buffer; + try + { + buffer.resize(KVMFR_CLIPBOARD_DATA_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((std::min)( + 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::max)() / + sizeof(wchar_t); + })) + { + if (!GetLastError()) + SetLastError(ERROR_FILE_TOO_LARGE); + return nullptr; + } + + ++units; + if (units > (std::numeric_limits::max)() / sizeof(wchar_t)) + { + SetLastError(ERROR_FILE_TOO_LARGE); + return nullptr; + } + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, + static_cast(units * sizeof(wchar_t))); + if (!memory) + return nullptr; + + wchar_t * output = static_cast(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(0xd800 + + (codepoint >> 10)); + output[index++] = static_cast(0xdc00 + + (codepoint & 0x3ff)); + } + else + output[index++] = static_cast(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::max)()) + { + SetLastError(ERROR_FILE_TOO_LARGE); + return nullptr; + } + + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, + static_cast(length)); + if (!memory) + return nullptr; + void * output = GlobalLock(memory); + if (!output || !spool.Read(offset, output, static_cast(length))) + { + const DWORD error = GetLastError(); + if (output) + GlobalUnlock(memory); + GlobalFree(memory); + SetLastError(error ? error : ERROR_READ_FAULT); + return nullptr; + } + GlobalUnlock(memory); + return memory; + } +} + +CClipboardManager::IncomingTransfer::~IncomingTransfer() +{ + if (event) + CloseHandle(event); +} + +CClipboardManager::CClipboardManager(HWND hwnd, + CClipboardChannel& channel) : m_hwnd(hwnd), m_channel(channel) +{ +} + +CClipboardManager::~CClipboardManager() +{ + Shutdown(); +} + +bool CClipboardManager::Initialize() +{ + 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"); + return false; + } + + Atomic::Store(m_shutdown, 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, REMOTE_RETRY_TIMER); + + CancelIncoming(ERROR_OPERATION_ABORTED); + 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 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; + } + m_thread = nullptr; + m_wake = nullptr; + m_stop = nullptr; +} + +DWORD WINAPI CClipboardManager::ThreadProc(void * context) +{ + static_cast(context)->Thread(); + return 0; +} + +void CClipboardManager::Thread() +{ + HANDLE events[] = { m_stop, m_wake }; + for (;;) + { + const DWORD result = WaitForMultipleObjects( + ARRAYSIZE(events), events, FALSE, INFINITE); + if (result == WAIT_OBJECT_0) + return; + if (result != WAIT_OBJECT_0 + 1) + return; + + for (;;) + { + Work work; + bool haveWork = false; + { + std::lock_guard 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)); + 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 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, 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; + + if (Atomic::Load(m_shutdown) || !cancel.transfer) + return false; + { + std::lock_guard 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 && + (m_sendWork[i]->record.type == + KVMFR_CLIPBOARD_MESSAGE_REQUEST || + m_sendWork[i]->record.type == + KVMFR_CLIPBOARD_MESSAGE_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 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 dropped = {}; + size_t droppedCount = 0; + { + std::lock_guard 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) + { + for (size_t i = 0; i < m_uiWorkCount;) + { + if (m_uiWork[i].type == UIType::OFFER || + m_uiWork[i].type == UIType::CLEAR) + 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 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)) + same = queued.record.clipboardGeneration == + work.record.clipboardGeneration; + 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::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); + } + 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); + 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; + } +} + +void CClipboardManager::ProcessRecord( + const KVMFRClipboardMessage& record, const uint8_t * data) +{ + switch (record.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + { + CancelIncoming(ERROR_OPERATION_ABORTED); + InvalidateOutgoing(ERROR_OPERATION_ABORTED); + 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); + 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 lock(m_transferLock); + const std::shared_ptr 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; + + 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 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 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) + 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 && + WaitForSingleObject(m_stop, CHANNEL_RETRY_MS) != WAIT_OBJECT_0) + { + if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL) + QueueCancel(work.record, work.record.token, 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 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 if (!QueueWork(std::move(work))) + failLocalPublication(); + 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(); +} + +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 size_t length = static_cast((std::min)( + KVMFR_CLIPBOARD_DATA_BYTES, total - work.record.offset)); + std::vector data; + if (length) + { + try + { + data.resize(length); + } + catch (const std::bad_alloc&) + { + ReleaseOutgoing(work.record.transfer); + QueueCancel(work.record, ERROR_OUTOFMEMORY); + return; + } + if (!work.spool->Read(work.record.offset, data.data(), length)) + { + const DWORD error = GetLastError(); + ReleaseOutgoing(work.record.transfer); + QueueCancel(work.record, error ? error : ERROR_READ_FAULT); + return; + } + } + + KVMFRClipboardMessage message = work.record; + message.type = KVMFR_CLIPBOARD_MESSAGE_DATA; + message.token = 0; + 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); + + ClipboardChannelResult result = ClipboardChannelResult::FAILED; + bool stale = false; + bool timedOut = false; + { + std::lock_guard lock(m_outgoingLock); + stale = Atomic::Load(m_outgoingTransfer, std::memory_order_acquire) != + message.transfer || + message.clipboardGeneration != Atomic::Load( + m_liveLocalGeneration, std::memory_order_acquire); + timedOut = GetTickCount64() >= work.deadline; + if (!stale && !timedOut) + result = m_channel.Send(message, + data.empty() ? nullptr : data.data()); + } + if (stale) + { + ReleaseOutgoing(message.transfer); + QueueCancel(message, ERROR_OPERATION_ABORTED); + return; + } + if (timedOut) + { + ReleaseOutgoing(message.transfer); + QueueCancel(message, ERROR_TIMEOUT); + return; + } + if (result == ClipboardChannelResult::BUSY) + { + if (GetTickCount64() < work.deadline && + WaitForSingleObject(m_stop, CHANNEL_RETRY_MS) != WAIT_OBJECT_0) + { + 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(work.record.transfer); + QueueCancel(work.record, ERROR_DEVICE_NOT_CONNECTED); + return; + } + work.deadline = GetTickCount64() + SEND_TIMEOUT_MS; + if (message.flags & KVMFR_CLIPBOARD_FLAG_END) + { + ReleaseOutgoing(work.record.transfer); + return; + } + + work.record.offset += length; + ++work.record.sequence; + const KVMFRClipboardMessage record = work.record; + if (!QueueWork(std::move(work))) + { + ReleaseOutgoing(record.transfer); + QueueCancel(record, ERROR_BUSY); + } +} + +void CClipboardManager::CancelIncoming(uint32_t reason, uint64_t transferID) +{ + std::lock_guard lock(m_transferLock); + const std::shared_ptr 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 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, const uint8_t * data) +{ + 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) || + !kvmfrClipboardFormatValid(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) || + !kvmfrClipboardFormatValid(record.format) || + (record.length && !data)) + return ClipboardChannelResult::FAILED; + break; + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + if (!record.transfer || record.length || record.flags) + return ClipboardChannelResult::FAILED; + break; + default: + return ClipboardChannelResult::FAILED; + } + + Work work; + work.type = WorkType::RECORD; + work.record = record; + if (record.length) + { + try + { + work.data.assign(data, data + record.length); + } + catch (const std::bad_alloc&) + { + return ClipboardChannelResult::BUSY; + } + } + 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(wParam)); + result = 0; + return true; + + case WM_RENDERALLFORMATS: + RenderAllFormats(); + result = 0; + return true; + + case WM_TIMER: + 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 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; + } + } +} + +void CClipboardManager::HandleState(bool available, uint64_t epoch) +{ + m_available = available; + m_epoch = available ? epoch : 0; + if (!available) + { + ClearRemoteRetry(); + InvalidateLocalClipboard(ERROR_DEVICE_NOT_CONNECTED); + ClearOwnedClipboard(); + m_remoteControlGeneration = 0; + return; + } + + PublishLocalClipboard(); +} + +void CClipboardManager::HandleOffer( + const KVMFRClipboardMessage& record) +{ + if (!m_available || !record.clipboardGeneration || + !record.token || (record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL)) + return; + if (m_remoteControlGeneration && + record.clipboardGeneration < m_remoteControlGeneration) + return; + + const bool newOffer = + m_pendingRemoteOffer.clipboardGeneration != record.clipboardGeneration; + if (newOffer) + { + ClearRemoteRetry(); + m_remoteControlGeneration = record.clipboardGeneration; + m_pendingRemoteOffer = record; + m_remoteRetryDeadline = GetTickCount64() + REMOTE_RETRY_TIMEOUT_MS; + InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); + } + + if (ApplyRemoteOffer(record.token, record.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(); +} + +void CClipboardManager::HandleClear( + const KVMFRClipboardMessage& record) +{ + if (record.clipboardGeneration && m_remoteControlGeneration && + record.clipboardGeneration < m_remoteControlGeneration) + return; + ClearRemoteRetry(); + m_remoteControlGeneration = record.clipboardGeneration; + InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); + ClearOwnedClipboard(); +} + +void CClipboardManager::HandleRequest( + const KVMFRClipboardMessage& record) +{ + if (!m_available || !record.transfer || + !kvmfrClipboardFormatValid(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 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); + } +} + +bool CClipboardManager::OpenClipboardRetry() const +{ + for (unsigned int attempt = 0; attempt != 8; ++attempt) + { + if (OpenClipboard(m_hwnd)) + return true; + Sleep(5U << (std::min)(attempt, 5U)); + } + return false; +} + +bool CClipboardManager::IsOurClipboard() +{ + if (GetClipboardOwner() == m_hwnd) + { + m_ownedSequence = GetClipboardSequenceNumber(); + return true; + } + + if (!m_formatOrigin || !m_remoteGeneration || + !IsClipboardFormatAvailable(m_formatOrigin) || !OpenClipboardRetry()) + return false; + + bool ours = false; + HANDLE data = GetClipboardData(m_formatOrigin); + if (data && GlobalSize(data) >= sizeof(ClipboardOrigin)) + { + const ClipboardOrigin * origin = + static_cast(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; + return formats; +} + +void CClipboardManager::HandleClipboardUpdate() +{ + if (m_applyingRemote || IsOurClipboard()) + return; + + ClearRemoteRetry(); + InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); + m_remoteGeneration = 0; + m_remoteFormats = 0; + m_ownedSequence = 0; + CancelIncoming(ERROR_OPERATION_ABORTED); + PublishLocalClipboard(); +} + +void CClipboardManager::HandleDestroyClipboard() +{ + if (m_applyingRemote) + return; + ClearRemoteRetry(); + InvalidateLocalClipboard(ERROR_OPERATION_ABORTED); + m_remoteGeneration = 0; + m_remoteFormats = 0; + m_ownedSequence = 0; + CancelIncoming(ERROR_OPERATION_ABORTED); +} + +void CClipboardManager::PublishLocalClipboard() +{ + if (!m_available) + { + Atomic::Store(m_liveLocalGeneration, UINT64_C(0), + std::memory_order_release); + m_localSequence = GetClipboardSequenceNumber(); + return; + } + + const DWORD before = GetClipboardSequenceNumber(); + const uint32_t formats = EnumerateFormats(); + const DWORD after = GetClipboardSequenceNumber(); + if (before != after) + { + PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0); + return; + } + + uint64_t generation = ++m_localGeneration; + if (!generation) + generation = ++m_localGeneration; + m_localSequence = after; + 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 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 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 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; + InvalidateOutgoing(reason); +} + +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; + HandleOffer(offer); +} + +bool CClipboardManager::SetOriginMarker(uint64_t generation) const +{ + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(ClipboardOrigin)); + if (!memory) + return false; + ClipboardOrigin * origin = + static_cast(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 (!OpenClipboardRetry()) + { + DEBUG_WARN_HR(GetLastError(), "Failed to open the clipboard"); + 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; + if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG) + complete = setDelayed(m_formatPNG) && complete; + if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP) + complete = setDelayed(CF_DIB) && 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(error, "Failed to replace the clipboard"); + return false; + } + m_ownedSequence = GetClipboardSequenceNumber(); + return true; +} + +void CClipboardManager::ClearOwnedClipboard() +{ + CancelIncoming(ERROR_OPERATION_ABORTED); + if (m_hwnd && GetClipboardOwner() == m_hwnd && OpenClipboardRetry()) + { + 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 CClipboardManager::CaptureFormat( + KVMFRClipboardFormat format, DWORD sequence) +{ + if (GetClipboardSequenceNumber() != sequence) + { + SetLastError(ERROR_RETRY); + return nullptr; + } + if (!OpenClipboardRetry()) + 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(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 spool; + try + { + spool = std::make_shared(); + } + 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(source); + const size_t capacity = sourceSize / sizeof(wchar_t); + size_t length = 0; + while (length < capacity && text[length]) + ++length; + + std::vector output; + try + { + output.reserve(TEXT_CONVERSION_CHUNK); + for (size_t index = 0; success && index < length; ++index) + { + uint32_t codepoint = static_cast(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(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::max)() - sizeof(BITMAPFILEHEADER)) + { + SetLastError(sourceSize > + (std::numeric_limits::max)() - sizeof(BITMAPFILEHEADER) ? + ERROR_FILE_TOO_LARGE : ERROR_INVALID_DATA); + success = false; + } + else + { + BITMAPFILEHEADER header = {}; + header.bfType = 0x4d42; + header.bfSize = static_cast( + 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, + CClipboardSpool& spool) +{ + UINT windowsFormat = ToWindowsFormat(format); + 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_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) +{ + const KVMFRClipboardFormat format = ToWireFormat(windowsFormat); + if (!m_available || !m_remoteGeneration || + !kvmfrClipboardFormatValid(format) || + !(m_remoteFormats & kvmfrClipboardFormatFlag(format))) + return; + if (deadline && GetTickCount64() >= deadline) + return; + + std::shared_ptr transfer; + try + { + transfer = std::make_shared(); + transfer->spool = std::make_shared(); + } + 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 = 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); + } + + { + std::lock_guard 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( + (std::min)(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 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 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, *transfer->spool)) + DEBUG_WARN_HR(GetLastError(), "Failed to render clipboard format %u", + format); +} + +void CClipboardManager::RenderAllFormats() +{ + if (!OpenClipboardRetry()) + return; + if (GetClipboardOwner() != m_hwnd) + { + CloseClipboard(); + return; + } + + const uint32_t formats = m_remoteFormats; + const uint64_t deadline = GetTickCount64() + RENDER_TIMEOUT_MS; + if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TEXT) + RenderFormat(CF_UNICODETEXT, deadline); + if (GetTickCount64() < deadline && + (formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG)) + RenderFormat(m_formatPNG, deadline); + if (GetTickCount64() < deadline && + (formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP)) + RenderFormat(CF_DIB, 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(); +} diff --git a/idd/LGIddHelper/CClipboardManager.h b/idd/LGIddHelper/CClipboardManager.h new file mode 100644 index 00000000..a85a2d1c --- /dev/null +++ b/idd/LGIddHelper/CClipboardManager.h @@ -0,0 +1,231 @@ +/** + * 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 + */ + +#pragma once + +#include "CClipboardChannel.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class CClipboardSpool; + +class CClipboardManager final : private IClipboardChannelHandler +{ +private: + enum class WorkType + { + STATE, + RESET, + RECORD, + SEND, + SEND_DATA, + }; + + enum class UIType + { + STATE, + OFFER, + CLEAR, + REQUEST, + }; + + struct Work + { + WorkType type = WorkType::STATE; + bool available = false; + uint64_t epoch = 0; + uint32_t reason = 0; + KVMFRClipboardMessage record = {}; + std::vector data; + std::shared_ptr spool; + uint64_t deadline = 0; + }; + + struct UIWork + { + UIType type = UIType::STATE; + bool available = false; + uint64_t epoch = 0; + KVMFRClipboardMessage record = {}; + }; + + struct PendingCancel + { + bool valid = false; + KVMFRClipboardMessage record = {}; + uint64_t deadline = 0; + }; + + struct PendingControl + { + WorkType type = WorkType::STATE; + bool available = false; + uint64_t epoch = 0; + uint32_t reason = 0; + }; + + static_assert(std::is_nothrow_move_constructible_v); + static_assert(std::is_nothrow_move_assignable_v); + + struct IncomingTransfer + { + uint64_t transfer = 0; + uint64_t generation = 0; + KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_NONE; + uint64_t nextOffset = 0; + uint32_t nextSequence = 0; + uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + bool began = false; + bool complete = false; + uint32_t error = ERROR_SUCCESS; + HANDLE event = nullptr; + std::shared_ptr spool; + + ~IncomingTransfer(); + }; + + 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_CONTROL = 16; + static constexpr DWORD RENDER_TIMEOUT_MS = 15000; + static constexpr DWORD SEND_TIMEOUT_MS = RENDER_TIMEOUT_MS; + + HWND m_hwnd; + CClipboardChannel& m_channel; + HANDLE m_stop = nullptr; + HANDLE m_wake = nullptr; + HANDLE m_thread = nullptr; + + std::mutex m_workLock; + std::array, MAX_WORK> m_recordWork; + size_t m_recordWorkCount = 0; + std::array, MAX_WORK> m_sendWork; + size_t m_sendWorkCount = 0; + std::array m_pendingCancel; + size_t m_pendingCancelCursor = 0; + std::array m_pendingControl; + size_t m_pendingControlCount = 0; + + std::recursive_mutex m_uiLock; + std::array m_uiWork; + size_t m_uiWorkCount = 0; + + std::mutex m_transferLock; + std::shared_ptr m_incoming; + std::mutex m_outgoingLock; + + UINT m_formatPNG = 0; + UINT m_formatJPEG = 0; + UINT m_formatOrigin = 0; + bool m_listener = false; + std::atomic m_shutdown { false }; + bool m_applyingRemote = false; + bool m_available = false; + uint64_t m_epoch = 0; + uint64_t m_localGeneration = 0; + DWORD m_localSequence = 0; + uint64_t m_remoteControlGeneration = 0; + uint64_t m_remoteGeneration = 0; + uint32_t m_remoteFormats = 0; + DWORD m_ownedSequence = 0; + std::atomic m_liveLocalGeneration { 0 }; + std::atomic m_nextTransfer { + KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1) }; + std::atomic m_outgoingTransfer { 0 }; + KVMFRClipboardMessage m_pendingRemoteOffer = {}; + uint64_t m_remoteRetryDeadline = 0; + + static DWORD WINAPI ThreadProc(void * context); + void Thread(); + + bool QueueWork(Work&& work); + 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 DrainUI(); + void ProcessWork(Work&& work); + void ProcessRecord(const KVMFRClipboardMessage& record, + const uint8_t * data); + void ProcessData(const KVMFRClipboardMessage& record, + const uint8_t * data); + void ProcessSend(Work&& work); + void ProcessSendData(Work&& work); + void CancelIncoming(uint32_t reason, uint64_t transfer = 0); + void ReleaseOutgoing(uint64_t transfer); + + void HandleState(bool available, uint64_t epoch); + void HandleOffer(const KVMFRClipboardMessage& record); + void HandleClear(const KVMFRClipboardMessage& record); + void HandleRequest(const KVMFRClipboardMessage& record); + void HandleClipboardUpdate(); + void HandleDestroyClipboard(); + void RenderFormat(UINT format, uint64_t deadline = 0); + void RenderAllFormats(); + void RetryRemoteOffer(); + + bool OpenClipboardRetry() const; + bool IsOurClipboard(); + uint32_t EnumerateFormats() const; + 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 ClearRemoteRetry(); + bool SetOriginMarker(uint64_t generation) const; + + KVMFRClipboardFormat ToWireFormat(UINT format) const; + UINT ToWindowsFormat(KVMFRClipboardFormat format) const; + std::shared_ptr CaptureFormat( + KVMFRClipboardFormat format, DWORD sequence); + bool MaterializeFormat(KVMFRClipboardFormat format, + CClipboardSpool& spool); + + void ClipboardState(bool available, uint64_t epoch) override; + ClipboardChannelResult ClipboardRecord(const KVMFRClipboardMessage& record, + const uint8_t * data) override; + void ClipboardReset(uint64_t epoch, uint32_t reason) override; + +public: + CClipboardManager(HWND hwnd, CClipboardChannel& channel); + ~CClipboardManager(); + + CClipboardManager(const CClipboardManager&) = delete; + CClipboardManager& operator=(const CClipboardManager&) = delete; + + bool Initialize(); + void Shutdown(); + bool HandleMessage(UINT message, WPARAM wParam, LPARAM lParam, + LRESULT& result); +}; diff --git a/idd/LGIddHelper/CNotifyWindow.cpp b/idd/LGIddHelper/CNotifyWindow.cpp index 77e6509e..038b7563 100644 --- a/idd/LGIddHelper/CNotifyWindow.cpp +++ b/idd/LGIddHelper/CNotifyWindow.cpp @@ -19,6 +19,7 @@ */ #include "CNotifyWindow.h" +#include "CClipboardManager.h" #include "CConfigWindow.h" #include "Resources.h" #include @@ -86,6 +87,11 @@ CNotifyWindow::~CNotifyWindow() LRESULT CNotifyWindow::handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { + LRESULT clipboardResult; + if (m_clipboard && m_clipboard->HandleMessage( + uMsg, wParam, lParam, clipboardResult)) + return clipboardResult; + switch (uMsg) { case WM_NOTIFY_ICON: @@ -168,6 +174,8 @@ LRESULT CNotifyWindow::onClose() LRESULT CNotifyWindow::onDestroy() { + if (m_clipboard) + m_clipboard->Shutdown(); KillTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER); Shell_NotifyIcon(NIM_DELETE, &m_iconData); return 0; @@ -310,6 +318,20 @@ void CNotifyWindow::setRecoveryMode(bool active) DEBUG_ERROR_HR(GetLastError(), "Failed to update recovery state"); } +bool CNotifyWindow::initClipboard(CClipboardChannel& channel) +{ + if (m_clipboard) + return true; + std::unique_ptr clipboard( + new (std::nothrow) CClipboardManager(m_hwnd, channel)); + if (!clipboard) + return false; + if (!clipboard->Initialize()) + return false; + m_clipboard = std::move(clipboard); + return true; +} + void CNotifyWindow::handleResolutionRejected(uint32_t width, uint32_t height, uint32_t requiredSizeMiB) { diff --git a/idd/LGIddHelper/CNotifyWindow.h b/idd/LGIddHelper/CNotifyWindow.h index e3f94927..41425a28 100644 --- a/idd/LGIddHelper/CNotifyWindow.h +++ b/idd/LGIddHelper/CNotifyWindow.h @@ -27,6 +27,8 @@ #include class CConfigWindow; +class CClipboardChannel; +class CClipboardManager; class CNotifyWindow : public CWindow { @@ -40,6 +42,7 @@ class CNotifyWindow : public CWindow std::atomic_bool closeRequested; bool m_recoveryActive; std::unique_ptr m_config; + std::unique_ptr m_clipboard; std::function m_onSettingChange; std::function m_onEnsureOnlyDisplay; @@ -80,6 +83,8 @@ public: uint32_t requiredSizeMiB); void setRecoveryMode(bool active); + bool initClipboard(CClipboardChannel& channel); + HWND hwndDialog(); void close(); diff --git a/idd/LGIddHelper/CPipeClient.cpp b/idd/LGIddHelper/CPipeClient.cpp index b798160c..2072f8bb 100644 --- a/idd/LGIddHelper/CPipeClient.cpp +++ b/idd/LGIddHelper/CPipeClient.cpp @@ -19,6 +19,7 @@ */ #include "CPipeClient.h" +#include "CClipboardRing.h" #include "CDebug.h" #include "CSRWLock.h" #include "CNotifyWindow.h" @@ -399,7 +400,11 @@ bool CPipeClient::Init() void CPipeClient::DeInit() { + // Stop first so no endpoint callback can race mapping teardown. m_endpoint.Stop(); + + CSRWExclusiveLock lock(m_clipboardSetupLock); + ResetClipboardSetupLocked(); } bool CPipeClient::IsLGIddDeviceAttached() @@ -484,6 +489,88 @@ void CPipeClient::OnPipeConnected() if (hasStatus) WriteMsg(status); + + if (!m_clipboardEnabled) + return; + + CSRWExclusiveLock setupLock(m_clipboardSetupLock); + ResetClipboardSetupLocked(); + + LARGE_INTEGER size = {}; + size.QuadPart = sizeof(ClipboardMapping); + m_clipboardMapping = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, size.HighPart, size.LowPart, nullptr); + if (!m_clipboardMapping) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to create the clipboard mapping"); + return; + } + + ClipboardMapping * view = static_cast(MapViewOfFile( + m_clipboardMapping, FILE_MAP_ALL_ACCESS, 0, 0, + sizeof(ClipboardMapping))); + if (!view) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to initialize the clipboard mapping"); + ResetClipboardSetupLocked(); + return; + } + + ++m_clipboardEpochCounter; + if (!m_clipboardEpochCounter) + ++m_clipboardEpochCounter; + m_clipboardEpoch = m_clipboardEpochCounter; + CClipboardRing::Initialize(*view, m_clipboardEpoch); + UnmapViewOfFile(view); + + DWORD serverPid = 0; + if (!GetNamedPipeServerProcessId(m_endpoint.NativeHandle(), &serverPid)) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to identify the clipboard mapping target"); + ResetClipboardSetupLocked(); + return; + } + + HANDLE target = OpenProcess(PROCESS_DUP_HANDLE, FALSE, serverPid); + HANDLE remote = nullptr; + if (!target || !DuplicateHandle(GetCurrentProcess(), + m_clipboardMapping, target, &remote, 0, FALSE, + DUPLICATE_SAME_ACCESS)) + { + DEBUG_ERROR_HR(GetLastError(), + "Failed to share the clipboard mapping with the IDD"); + if (target) + CloseHandle(target); + ResetClipboardSetupLocked(); + return; + } + + LGPipeMsg setup = {}; + setup.size = sizeof(setup); + setup.type = LGPipeMsg::CLIPBOARD_SETUP; + setup.clipboardSetup.handle = + static_cast(reinterpret_cast(remote)); + setup.clipboardSetup.bytes = sizeof(ClipboardMapping); + const bool sent = m_endpoint.Send(&setup, sizeof(setup)); + if (!sent) + { + DEBUG_WARN("Failed to send clipboard mapping setup"); + HANDLE reclaimed = nullptr; + if (DuplicateHandle(target, remote, GetCurrentProcess(), &reclaimed, + 0, FALSE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) + CloseHandle(reclaimed); + ResetClipboardSetupLocked(); + } + CloseHandle(target); +} + +void CPipeClient::OnPipeDisconnected() +{ + CSRWExclusiveLock lock(m_clipboardSetupLock); + ResetClipboardSetupLocked(); } void CPipeClient::ReloadSettings() @@ -740,12 +827,85 @@ bool CPipeClient::OnPipeMessage(const void * message, size_t size) HandleSetRecovery(msg); return true; + case LGPipeMsg::CLIPBOARD_READY: + { + CSRWExclusiveLock setupLock(m_clipboardSetupLock); + + if (msg.clipboardReady.status != ERROR_SUCCESS || + msg.clipboardReady.epoch != m_clipboardEpoch) + { + DEBUG_WARN("IDD rejected the clipboard mapping (%u)", + msg.clipboardReady.status); + ResetClipboardSetupLocked(); + return true; + } + + HANDLE mapping = nullptr; + if (!m_clipboardMapping || + !DuplicateHandle(GetCurrentProcess(), m_clipboardMapping, + GetCurrentProcess(), &mapping, 0, FALSE, DUPLICATE_SAME_ACCESS) || + !m_clipboard.Attach(mapping, m_clipboardEpoch, true, *this)) + { + DEBUG_ERROR("Failed to activate the clipboard mapping"); + const uint64_t epoch = m_clipboardEpoch; + ResetClipboardSetupLocked(); + setupLock.Unlock(); + + // Tell the IDD that its successful mapping is unusable here, so it + // does not leave a one-sided channel advertised as available. + LGPipeMsg failure = {}; + failure.size = sizeof(failure); + failure.type = LGPipeMsg::CLIPBOARD_READY; + failure.clipboardReady.epoch = epoch; + failure.clipboardReady.status = ERROR_NOT_READY; + m_endpoint.Send(&failure, sizeof(failure)); + } + return true; + } + + case LGPipeMsg::CLIPBOARD_KICK: + m_clipboard.Kick(msg.clipboardKick.epoch); + return true; + + case LGPipeMsg::CLIPBOARD_RESET: + m_clipboard.Reset( + msg.clipboardReset.epoch, msg.clipboardReset.reason); + return true; + default: DEBUG_ERROR("Unknown message type %d", msg.type); return true; } } +bool CPipeClient::ClipboardKick(uint64_t epoch) +{ + LGPipeMsg msg = {}; + msg.size = sizeof(msg); + msg.type = LGPipeMsg::CLIPBOARD_KICK; + msg.clipboardKick.epoch = epoch; + return m_endpoint.Send(&msg, sizeof(msg)); +} + +void CPipeClient::ClipboardResetPeer(uint64_t epoch, uint32_t reason) +{ + LGPipeMsg msg = {}; + msg.size = sizeof(msg); + msg.type = LGPipeMsg::CLIPBOARD_RESET; + msg.clipboardReset.epoch = epoch; + msg.clipboardReset.reason = reason; + m_endpoint.Send(&msg, sizeof(msg)); +} + +void CPipeClient::ResetClipboardSetupLocked() +{ + m_clipboard.Detach(); + if (m_clipboardMapping) + CloseHandle(m_clipboardMapping); + m_clipboardMapping = nullptr; + m_clipboardEpoch = 0; +} + void CPipeClient::HandleSetCursorPos(const LGPipeMsg& msg) { SetActiveDesktop(); diff --git a/idd/LGIddHelper/CPipeClient.h b/idd/LGIddHelper/CPipeClient.h index 5f3236c4..bd89fffc 100644 --- a/idd/LGIddHelper/CPipeClient.h +++ b/idd/LGIddHelper/CPipeClient.h @@ -24,14 +24,22 @@ #include #include "CPipeEndpoint.h" +#include "CClipboardChannel.h" #include "CSRWLock.h" #include "PipeMsg.h" -class CPipeClient : private IPipeEndpointHandler +class CPipeClient : private IPipeEndpointHandler, + public IClipboardChannelDoorbell { private: - CPipeEndpoint m_endpoint; - CSRWLock m_displayLock; + CPipeEndpoint m_endpoint; + CClipboardChannel m_clipboard; + CSRWLock m_clipboardSetupLock; + HANDLE m_clipboardMapping = nullptr; + uint64_t m_clipboardEpoch = 0; + uint64_t m_clipboardEpochCounter = 0; + bool m_clipboardEnabled = false; + CSRWLock m_displayLock; bool m_recoveryActive = false; bool m_hasRecoveryStatus = false; @@ -50,8 +58,10 @@ private: void HandleGPUStatus(const LGPipeMsg& msg); void HandleResolutionRejected(const LGPipeMsg& msg); void HandleSetRecovery(const LGPipeMsg& msg); + void ResetClipboardSetupLocked(); void OnPipeConnected() override; + void OnPipeDisconnected() override; bool ShouldReconnect() override; bool OnPipeMessage(const void * message, size_t size) override; @@ -64,6 +74,11 @@ public: void DeInit(); bool IsRunning() { return m_endpoint.IsRunning(); } + CClipboardChannel& Clipboard() { return m_clipboard; } + void EnableClipboard() { m_clipboardEnabled = true; } + bool ClipboardKick(uint64_t epoch) override; + void ClipboardResetPeer(uint64_t epoch, uint32_t reason) override; + void ReloadSettings(); bool EnsureOnlyDisplay(); }; diff --git a/idd/LGIddHelper/LGIddHelper.vcxproj b/idd/LGIddHelper/LGIddHelper.vcxproj index 363f89e5..eb11a96d 100644 --- a/idd/LGIddHelper/LGIddHelper.vcxproj +++ b/idd/LGIddHelper/LGIddHelper.vcxproj @@ -191,6 +191,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd + @@ -207,6 +208,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd + diff --git a/idd/LGIddHelper/LGIddHelper.vcxproj.filters b/idd/LGIddHelper/LGIddHelper.vcxproj.filters index 0cd5b124..e4f10bd1 100644 --- a/idd/LGIddHelper/LGIddHelper.vcxproj.filters +++ b/idd/LGIddHelper/LGIddHelper.vcxproj.filters @@ -57,6 +57,9 @@ Source Files + + Source Files + @@ -98,6 +101,9 @@ Header Files + + Header Files + Header Files diff --git a/idd/LGIddHelper/main.cpp b/idd/LGIddHelper/main.cpp index 17423cde..416b3366 100644 --- a/idd/LGIddHelper/main.cpp +++ b/idd/LGIddHelper/main.cpp @@ -136,6 +136,11 @@ int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _ CNotifyWindow& window = CNotifyWindow::instance(); + if (!window.initClipboard(g_pipe.Clipboard())) + DEBUG_ERROR("Failed to initialize clipboard synchronization"); + else + g_pipe.EnableClipboard(); + // the pipe must be initialized after the CNotifyWindow // has been created to avoid a potential race if (!g_pipe.Init())