[idd] clipboard: add direct synchronization

This commit is contained in:
Geoffrey McRae
2026-08-14 06:26:35 +10:00
parent a8d531b797
commit b786167f72
35 changed files with 5956 additions and 79 deletions

View File

@@ -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 <new>
#include <string.h>
#include <vector>
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<ClipboardMapping *>(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<uint8_t> 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<CClipboardChannel *>(context)->Thread();
return 0;
}

View File

@@ -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 <Windows.h>
#include <atomic>
#include <stdint.h>
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<bool> 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();
};

View File

@@ -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)

View File

@@ -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);

View File

@@ -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<CPipeEndpoint *>(context)->Thread();

View File

@@ -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;

View File

@@ -65,12 +65,14 @@
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="CClipboardChannel.cpp" />
<ClCompile Include="CClipboardRing.cpp" />
<ClCompile Include="CDebug.cpp" />
<ClCompile Include="CPipeEndpoint.cpp" />
<ClCompile Include="RefreshRate.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="CClipboardChannel.h" />
<ClInclude Include="CClipboardRing.h" />
<ClInclude Include="Atomic.h" />
<ClInclude Include="CDebug.h" />

View File

@@ -12,6 +12,9 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="CClipboardChannel.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CClipboardRing.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -29,6 +32,9 @@
<ClInclude Include="Atomic.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CClipboardChannel.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CClipboardRing.h">
<Filter>Header Files</Filter>
</ClInclude>