[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

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

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>

View File

@@ -66,6 +66,7 @@
<ClCompile Include="postprocess\effect\CFormatEffect.cpp" />
<ClCompile Include="postprocess\effect\CHDR16to10Effect.cpp" />
<ClCompile Include="postprocess\effect\CRGB24Effect.cpp" />
<ClCompile Include="transport\CClipboardHub.cpp" />
<ClCompile Include="transport\CControlHub.cpp" />
<ClCompile Include="transport\CFrameHub.cpp" />
<ClCompile Include="transport\CInputHub.cpp" />
@@ -76,6 +77,7 @@
<ClCompile Include="transport\TransportFactory.cpp" />
<ClCompile Include="transport\lgmp\CIVSHMEM.cpp" />
<ClCompile Include="transport\lgmp\CRecovery.cpp" />
<ClCompile Include="transport\lgmp\CLGMPClipboardTransport.cpp" />
<ClCompile Include="transport\lgmp\CLGMPControl.cpp" />
<ClCompile Include="transport\lgmp\CLGMPFrameCaps.cpp" />
<ClCompile Include="transport\lgmp\CLGMPFrameTransport.cpp" />
@@ -125,6 +127,7 @@
<ClInclude Include="postprocess\effect\CFormatEffect.h" />
<ClInclude Include="postprocess\effect\CHDR16to10Effect.h" />
<ClInclude Include="postprocess\effect\CRGB24Effect.h" />
<ClInclude Include="transport\CClipboardHub.h" />
<ClInclude Include="transport\CControlHub.h" />
<ClInclude Include="transport\CFrameHub.h" />
<ClInclude Include="transport\CInputHub.h" />
@@ -135,6 +138,7 @@
<ClInclude Include="transport\FrameCaps.h" />
<ClInclude Include="transport\FrameProfile.h" />
<ClInclude Include="transport\FrameIn.h" />
<ClInclude Include="transport\IClipboardSource.h" />
<ClInclude Include="transport\IControlSink.h" />
<ClInclude Include="transport\IControlTransport.h" />
<ClInclude Include="transport\IFrameSink.h" />
@@ -149,6 +153,7 @@
<ClInclude Include="transport\TransportFactory.h" />
<ClInclude Include="transport\lgmp\CIVSHMEM.h" />
<ClInclude Include="transport\lgmp\CRecovery.h" />
<ClInclude Include="transport\lgmp\CLGMPClipboardTransport.h" />
<ClInclude Include="transport\lgmp\CLGMPControl.h" />
<ClInclude Include="transport\lgmp\CLGMPFrameCaps.h" />
<ClInclude Include="transport\lgmp\CLGMPFrameTransport.h" />

View File

@@ -169,6 +169,9 @@
<ClInclude Include="postprocess\effect\CRGB24Effect.h">
<Filter>Post-processing\Effects</Filter>
</ClInclude>
<ClInclude Include="transport\CClipboardHub.h">
<Filter>Transport</Filter>
</ClInclude>
<ClInclude Include="transport\CControlHub.h">
<Filter>Transport</Filter>
</ClInclude>
@@ -199,6 +202,9 @@
<ClInclude Include="transport\FrameIn.h">
<Filter>Transport</Filter>
</ClInclude>
<ClInclude Include="transport\IClipboardSource.h">
<Filter>Transport</Filter>
</ClInclude>
<ClInclude Include="transport\IControlSink.h">
<Filter>Transport</Filter>
</ClInclude>
@@ -241,6 +247,9 @@
<ClInclude Include="transport\lgmp\CRecovery.h">
<Filter>Transport\LGMP</Filter>
</ClInclude>
<ClInclude Include="transport\lgmp\CLGMPClipboardTransport.h">
<Filter>Transport\LGMP</Filter>
</ClInclude>
<ClInclude Include="transport\lgmp\CLGMPControl.h">
<Filter>Transport\LGMP</Filter>
</ClInclude>
@@ -369,6 +378,9 @@
<ClCompile Include="postprocess\effect\CRGB24Effect.cpp">
<Filter>Post-processing\Effects</Filter>
</ClCompile>
<ClCompile Include="transport\CClipboardHub.cpp">
<Filter>Transport</Filter>
</ClCompile>
<ClCompile Include="transport\CControlHub.cpp">
<Filter>Transport</Filter>
</ClCompile>
@@ -399,6 +411,9 @@
<ClCompile Include="transport\lgmp\CRecovery.cpp">
<Filter>Transport\LGMP</Filter>
</ClCompile>
<ClCompile Include="transport\lgmp\CLGMPClipboardTransport.cpp">
<Filter>Transport\LGMP</Filter>
</ClCompile>
<ClCompile Include="transport\lgmp\CLGMPControl.cpp">
<Filter>Transport\LGMP</Filter>
</ClCompile>

View File

@@ -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<HANDLE>(
static_cast<uintptr_t>(msg.clipboardSetup.handle));
HANDLE mapping = transferred;
uint64_t epoch = 0;
if (transferred &&
msg.clipboardSetup.bytes == sizeof(ClipboardMapping))
{
ClipboardMapping * view = static_cast<ClipboardMapping *>(
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)

View File

@@ -26,12 +26,14 @@
#include <vector>
#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<LGPipeMsg> 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;

View File

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

View File

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

View File

@@ -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 <Windows.h>
@@ -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<ITransport> 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)
{

View File

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

View File

@@ -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 <stdint.h>
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;
};

View File

@@ -29,6 +29,7 @@
#include <stddef.h>
#include <stdint.h>
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; }
};

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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<const char *>(&kvmfr), sizeof(kvmfr));
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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 <Windows.h>
#include <array>
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <type_traits>
#include <vector>
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<uint8_t> data;
std::shared_ptr<CClipboardSpool> 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<Work>);
static_assert(std::is_nothrow_move_assignable_v<Work>);
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<CClipboardSpool> 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<std::optional<Work>, MAX_WORK> m_recordWork;
size_t m_recordWorkCount = 0;
std::array<std::optional<Work>, MAX_WORK> m_sendWork;
size_t m_sendWorkCount = 0;
std::array<PendingCancel, MAX_PENDING_CANCEL> m_pendingCancel;
size_t m_pendingCancelCursor = 0;
std::array<PendingControl, MAX_PENDING_CONTROL> m_pendingControl;
size_t m_pendingControlCount = 0;
std::recursive_mutex m_uiLock;
std::array<UIWork, MAX_UI_WORK> m_uiWork;
size_t m_uiWorkCount = 0;
std::mutex m_transferLock;
std::shared_ptr<IncomingTransfer> m_incoming;
std::mutex m_outgoingLock;
UINT m_formatPNG = 0;
UINT m_formatJPEG = 0;
UINT m_formatOrigin = 0;
bool m_listener = false;
std::atomic<bool> 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<uint64_t> m_liveLocalGeneration { 0 };
std::atomic<uint64_t> m_nextTransfer {
KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1) };
std::atomic<uint64_t> 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<CClipboardSpool> 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);
};

View File

@@ -19,6 +19,7 @@
*/
#include "CNotifyWindow.h"
#include "CClipboardManager.h"
#include "CConfigWindow.h"
#include "Resources.h"
#include <CDebug.h>
@@ -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<CClipboardManager> 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)
{

View File

@@ -27,6 +27,8 @@
#include <optional>
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<CConfigWindow> m_config;
std::unique_ptr<CClipboardManager> m_clipboard;
std::function<void()> m_onSettingChange;
std::function<bool()> m_onEnsureOnlyDisplay;
@@ -80,6 +83,8 @@ public:
uint32_t requiredSizeMiB);
void setRecoveryMode(bool active);
bool initClipboard(CClipboardChannel& channel);
HWND hwndDialog();
void close();

View File

@@ -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<ClipboardMapping *>(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<uint64_t>(reinterpret_cast<uintptr_t>(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();

View File

@@ -24,14 +24,22 @@
#include <stdint.h>
#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();
};

View File

@@ -191,6 +191,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
<ItemGroup>
<ClCompile Include="CButton.cpp" />
<ClCompile Include="CCheckbox.cpp" />
<ClCompile Include="CClipboardManager.cpp" />
<ClCompile Include="CConfigWindow.cpp" />
<ClCompile Include="CEditWidget.cpp" />
<ClCompile Include="CGroupBox.cpp" />
@@ -207,6 +208,7 @@ copy /Y "$(ProjectDir)VERSION" "$(SolutionDir)$(Platform)\$(Configuration)\LGIdd
<ItemGroup>
<ClInclude Include="CButton.h" />
<ClInclude Include="CCheckbox.h" />
<ClInclude Include="CClipboardManager.h" />
<ClInclude Include="CConfigWindow.h" />
<ClInclude Include="CEditWidget.h" />
<ClInclude Include="CGroupBox.h" />

View File

@@ -57,6 +57,9 @@
<ClCompile Include="CCheckbox.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CClipboardManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="CPipeClient.h">
@@ -98,6 +101,9 @@
<ClInclude Include="CCheckbox.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CClipboardManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Resources.h">
<Filter>Header Files</Filter>
</ClInclude>

View File

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