Files
LookingGlass/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp
Geoffrey McRae 5bdfc0fe80 [idd] clipboard: remove stream payload restaging
Keep inbound stream payloads in the bounded stream-target window while
the Helper channel is busy and publish directly from those buffers.
Retain the pending target only for LGMP queue controls. Their credits
remain outstanding across retries.

This removes one full payload copy per client-to-Helper record without
changing delivery order, backpressure, or reset behavior.
2026-08-15 16:17:39 +10:00

2065 lines
58 KiB
C++

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <ntstatus.h>
#include "transport/lgmp/CLGMPClipboardTransport.h"
#include <wudfwdm.h>
#include "CDebug.h"
#include "Seq.h"
#include "WCCopy.h"
#include "transport/lgmp/CLGMPHost.h"
#include <limits>
#include <string.h>
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
#endif
namespace
{
static_assert(sizeof(LGMPStreamDescriptor) ==
sizeof(KVMFRStreamDescriptor),
"LGMP and KVMFR stream descriptor sizes differ");
static_assert(offsetof(LGMPStreamDescriptor, offset) ==
offsetof(KVMFRStreamDescriptor, offset),
"LGMP and KVMFR stream descriptor offsets differ");
static_assert(offsetof(LGMPStreamDescriptor, slotSize) ==
offsetof(KVMFRStreamDescriptor, slotSize),
"LGMP and KVMFR stream descriptor geometry differs");
static const LGMPQueueConfig CLIPBOARD_QUEUE_CONFIG =
{
LGMP_Q_CLIPBOARD,
LGMP_Q_CLIPBOARD_LEN,
1000,
};
bool ArmPollTimer(HANDLE timer, uint32_t waitUs)
{
LARGE_INTEGER due = {};
due.QuadPart = -static_cast<LONGLONG>(waitUs) * 10;
return SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE) != FALSE;
}
bool EmptyControl(const KVMFRClipboardMessage& message,
bool keepToken = false)
{
return !message.clipboardGeneration && !message.transfer &&
!message.offset && !message.size && !message.format &&
!message.flags && (keepToken || !message.token) &&
!message.length && !message.sequence;
}
bool AddValid(uint64_t offset, uint32_t length)
{
return offset <= (std::numeric_limits<uint64_t>::max)() - length;
}
bool ValidOffer(const KVMFRClipboardMessage& message)
{
return message.clipboardGeneration && message.token &&
!(message.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) &&
!message.transfer && !message.offset && !message.size &&
!message.format && !message.flags && !message.length &&
!message.sequence;
}
bool ValidClear(const KVMFRClipboardMessage& message)
{
return message.clipboardGeneration && !message.token &&
!message.transfer && !message.offset && !message.size &&
!message.format && !message.flags && !message.length &&
!message.sequence;
}
bool ValidRequest(const KVMFRClipboardMessage& message, bool helper)
{
const bool validTransfer = helper ?
kvmfrClipboardTransferFromHelper(message.transfer) :
kvmfrClipboardTransferFromClient(message.transfer);
return message.clipboardGeneration && validTransfer &&
kvmfrClipboardRepresentationFormatValid(message.format) &&
!message.offset && !message.size && !message.flags &&
!message.token && !message.length && !message.sequence;
}
bool ValidCancel(const KVMFRClipboardMessage& message)
{
return message.transfer && !message.offset && !message.size &&
(!message.format ||
kvmfrClipboardRepresentationFormatValid(message.format)) &&
!message.flags && !message.length && !message.sequence;
}
bool OwnerScopedLifecycle(const KVMFRClipboardMessage& message)
{
return message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST ||
message.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL ||
CLGMPClipboardFiles::IsRecord(message);
}
KVMFRStreamDescriptor StreamDescriptorToWire(
const LGMPStreamDescriptor& descriptor)
{
KVMFRStreamDescriptor wire = {};
wire.magic = descriptor.magic;
wire.version = descriptor.version;
wire.size = descriptor.size;
wire.offset = descriptor.offset;
wire.regionSize = descriptor.regionSize;
wire.direction = descriptor.direction;
wire.policy = descriptor.policy;
wire.slotCount = descriptor.slotCount;
wire.slotSize = descriptor.slotSize;
return wire;
}
}
CLGMPClipboardTransport::~CLGMPClipboardTransport()
{
DeInit();
}
bool CLGMPClipboardTransport::Initialize()
{
if (m_queue)
return true;
LGMP_STATUS status = m_host.CreateQueue(
CLIPBOARD_QUEUE_CONFIG, &m_queue);
if (status != LGMP_OK)
{
DEBUG_ERROR("lgmpHostQueueCreate Failed (Clipboard): %s",
lgmpStatusString(status));
return false;
}
for (PLGMPMemory& memory : m_statusMemory)
{
status = m_host.Allocate(sizeof(KVMFRClipboardStatus), &memory);
if (status != LGMP_OK)
goto fail;
memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardStatus));
}
for (PLGMPMemory& memory : m_messageMemory)
{
status = m_host.Allocate(sizeof(KVMFRClipboardMessage), &memory);
if (status != LGMP_OK)
goto fail;
memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardMessage));
}
if (!InitializeStreams())
{
DeInit();
return false;
}
return true;
fail:
DEBUG_ERROR("lgmpHostMemAlloc Failed (Clipboard): %s",
lgmpStatusString(status));
DeInit();
return false;
}
void CLGMPClipboardTransport::DeInit()
{
Stop();
DeInitStreams();
for (PLGMPMemory& memory : m_messageMemory)
lgmpHostMemFree(&memory);
for (PLGMPMemory& memory : m_statusMemory)
lgmpHostMemFree(&memory);
m_queue = nullptr;
}
bool CLGMPClipboardTransport::InitializeStreams()
{
if (m_hostToClientStream && m_clientToHostStream)
return true;
const LGMPStreamConfig hostToClient =
{
LGMP_STREAM_HOST_TO_CLIENT,
LGMP_STREAM_RELIABLE_FIFO,
KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
SLOT_BYTES,
};
LGMP_STATUS status = m_host.CreateStream(
hostToClient, &m_hostToClientStream);
if (status != LGMP_OK)
{
DEBUG_ERROR("Failed to create host-to-client clipboard stream: %s",
lgmpStatusString(status));
return false;
}
const LGMPStreamConfig clientToHost =
{
LGMP_STREAM_CLIENT_TO_HOST,
LGMP_STREAM_RELIABLE_FIFO,
KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
SLOT_BYTES,
};
status = m_host.CreateStream(clientToHost, &m_clientToHostStream);
if (status != LGMP_OK)
{
DEBUG_ERROR("Failed to create client-to-host clipboard stream: %s",
lgmpStatusString(status));
lgmpHostStreamFree(&m_hostToClientStream);
return false;
}
LGMPStreamDescriptor descriptor = {};
lgmpHostStreamGetDescriptor(m_hostToClientStream, &descriptor);
m_hostToClientDescriptor = StreamDescriptorToWire(descriptor);
lgmpHostStreamGetDescriptor(m_clientToHostStream, &descriptor);
m_clientToHostDescriptor = StreamDescriptorToWire(descriptor);
return true;
}
void CLGMPClipboardTransport::DeInitStreams()
{
ForceUnbindStreams();
lgmpHostStreamFree(&m_clientToHostStream);
lgmpHostStreamFree(&m_hostToClientStream);
m_clientToHostDescriptor = {};
m_hostToClientDescriptor = {};
}
bool CLGMPClipboardTransport::BindStreams(uint32_t clientID) const
{
if (!m_hostToClientStream || !m_clientToHostStream)
return false;
LGMP_STATUS status = lgmpHostStreamBind(
m_hostToClientStream, clientID, nullptr);
if (status != LGMP_OK)
{
DEBUG_ERROR("Failed to bind host-to-client clipboard stream for "
"client %u: %s", clientID, lgmpStatusString(status));
return false;
}
status = lgmpHostStreamBind(m_clientToHostStream, clientID, nullptr);
if (status == LGMP_OK)
return true;
DEBUG_ERROR("Failed to bind client-to-host clipboard stream for "
"client %u: %s", clientID, lgmpStatusString(status));
const LGMP_STATUS rollback =
lgmpHostStreamUnbind(m_hostToClientStream);
if (rollback != LGMP_OK && rollback != LGMP_ERR_STREAM_UNBOUND &&
rollback != LGMP_ERR_STREAM_BUSY)
DEBUG_ERROR("Failed to roll back host-to-client clipboard stream "
"binding: %s", lgmpStatusString(rollback));
return false;
}
bool CLGMPClipboardTransport::TryUnbindStreams()
{
bool complete = true;
if (m_clientToHostStream)
{
const LGMP_STATUS status =
lgmpHostStreamUnbind(m_clientToHostStream);
if (status == LGMP_ERR_STREAM_BUSY)
complete = false;
else if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
{
Fail("lgmpHostStreamUnbind(client-to-host)", status);
complete = false;
}
}
if (m_hostToClientStream)
{
const LGMP_STATUS status =
lgmpHostStreamUnbind(m_hostToClientStream);
if (status == LGMP_ERR_STREAM_BUSY)
complete = false;
else if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
{
Fail("lgmpHostStreamUnbind(host-to-client)", status);
complete = false;
}
}
return complete;
}
void CLGMPClipboardTransport::ForceUnbindStreams() const
{
if (m_clientToHostStream)
{
const LGMP_STATUS status =
lgmpHostStreamForceUnbind(m_clientToHostStream);
if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
DEBUG_ERROR("Failed to force-unbind client-to-host clipboard "
"stream: %s", lgmpStatusString(status));
}
if (m_hostToClientStream)
{
const LGMP_STATUS status =
lgmpHostStreamForceUnbind(m_hostToClientStream);
if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
DEBUG_ERROR("Failed to force-unbind host-to-client clipboard "
"stream: %s", lgmpStatusString(status));
}
}
void CLGMPClipboardTransport::Wake() const
{
if (m_wakeEvent)
SetEvent(m_wakeEvent);
}
bool CLGMPClipboardTransport::Start(IClipboardTarget& target)
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
if (m_thread)
{
if (WaitForSingleObject(m_thread, 0) == WAIT_TIMEOUT)
return true;
CloseHandle(m_thread);
CloseHandle(m_pollTimer);
CloseHandle(m_wakeEvent);
CloseHandle(m_stopEvent);
m_thread = nullptr;
m_pollTimer = nullptr;
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
}
if (!m_queue)
return false;
m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!m_stopEvent)
{
const DWORD error = GetLastError();
DEBUG_ERROR_HR(error,
"Failed to create LGMP clipboard stop event");
m_stopEvent = nullptr;
return false;
}
m_wakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!m_wakeEvent)
{
const DWORD error = GetLastError();
DEBUG_ERROR_HR(error,
"Failed to create LGMP clipboard wake event");
CloseHandle(m_stopEvent);
m_stopEvent = nullptr;
return false;
}
m_pollTimer = CreateWaitableTimerExW(nullptr, nullptr,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
if (!m_pollTimer)
m_pollTimer = CreateWaitableTimerExW(
nullptr, nullptr, 0, TIMER_ALL_ACCESS);
if (!m_pollTimer)
{
const DWORD error = GetLastError();
DEBUG_ERROR_HR(error,
"Failed to create LGMP clipboard poll timer");
CloseHandle(m_wakeEvent);
CloseHandle(m_stopEvent);
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
return false;
}
{
CSRWExclusiveLock lock(m_lock);
m_target = &target;
m_failed = false;
m_statusDirty = true;
}
m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
if (!m_thread)
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to create LGMP clipboard worker");
{
CSRWExclusiveLock lock(m_lock);
m_target = nullptr;
}
CloseHandle(m_pollTimer);
CloseHandle(m_wakeEvent);
CloseHandle(m_stopEvent);
m_pollTimer = nullptr;
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
return false;
}
return true;
}
void CLGMPClipboardTransport::Stop()
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
if (m_stopEvent)
SetEvent(m_stopEvent);
if (m_thread)
WaitForSingleObject(m_thread, INFINITE);
if (m_thread)
CloseHandle(m_thread);
if (m_pollTimer)
CloseHandle(m_pollTimer);
if (m_wakeEvent)
CloseHandle(m_wakeEvent);
if (m_stopEvent)
CloseHandle(m_stopEvent);
m_thread = nullptr;
m_pollTimer = nullptr;
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
CSRWExclusiveLock lock(m_lock);
DropPendingTarget();
ResetProtocol(false);
m_available = false;
m_endpointGeneration = 0;
m_target = nullptr;
m_failed = false;
m_statusDirty = true;
}
bool CLGMPClipboardTransport::IsOwner(
uint32_t clientID, uint32_t generation) const
{
return m_ownerClientID == clientID &&
m_ownerGeneration == generation;
}
bool CLGMPClipboardTransport::OwnerSubscribed() const
{
if (!m_ownerClientID)
return false;
uint32_t clients[LGMP_MAX_CLIENTS] = {};
unsigned count = 0;
if (lgmpHostGetClientIDs(m_queue, clients, &count) != LGMP_OK)
return false;
for (unsigned i = 0; i < count; ++i)
if (clients[i] == m_ownerClientID)
return true;
return false;
}
void CLGMPClipboardTransport::RenewLease()
{
m_ownerDeadline = GetTickCount64() + OWNER_LEASE_MS;
}
void CLGMPClipboardTransport::BlockOutbound(
const KVMFRClipboardMessage& record, uint32_t ownerClientID,
uint32_t ownerGeneration)
{
m_outboundBlocked = true;
m_blockedOutbound = record;
m_blockedOwnerClientID = ownerClientID;
m_blockedOwnerGeneration = ownerGeneration;
}
void CLGMPClipboardTransport::ClearOutboundBlock()
{
m_outboundBlocked = false;
m_blockedOutbound = {};
m_blockedOwnerClientID = 0;
m_blockedOwnerGeneration = 0;
}
bool CLGMPClipboardTransport::BlockedOwnerLost(
const KVMFRClipboardMessage& record) const
{
return m_outboundBlocked && OwnerScopedLifecycle(record) &&
memcmp(&m_blockedOutbound, &record, sizeof(record)) == 0 &&
(!m_available || m_ownerReleasing ||
record.generation != m_endpointGeneration ||
m_blockedOwnerClientID != m_ownerClientID ||
m_blockedOwnerGeneration != m_ownerGeneration);
}
void CLGMPClipboardTransport::QueueHelperClear()
{
if (!m_clientClipboardGeneration || !m_target ||
!m_available || !m_endpointGeneration)
return;
KVMFRClipboardMessage clear = {};
clear.version = KVMFR_CLIPBOARD_VERSION;
clear.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR;
clear.generation = m_endpointGeneration;
clear.clipboardGeneration = m_clientClipboardGeneration;
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
QueueInternalTarget(clear);
}
void CLGMPClipboardTransport::QueueTransferCancel(
const Transfer& transfer)
{
if (!transfer.Active() || !m_target || !m_available ||
!m_endpointGeneration)
return;
KVMFRClipboardMessage cancel = {};
cancel.version = KVMFR_CLIPBOARD_VERSION;
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
cancel.generation = m_endpointGeneration;
cancel.clipboardGeneration = transfer.clipboardGeneration;
cancel.transfer = transfer.transfer;
cancel.format = transfer.format;
cancel.token = ERROR_DEVICE_NOT_CONNECTED;
QueueInternalTarget(cancel);
}
void CLGMPClipboardTransport::QueueStaleRequestCancel(
const KVMFRClipboardMessage& request)
{
if (request.type != KVMFR_CLIPBOARD_MESSAGE_REQUEST ||
!m_target || !m_available || !m_endpointGeneration)
return;
KVMFRClipboardMessage cancel = {};
cancel.version = KVMFR_CLIPBOARD_VERSION;
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
cancel.generation = m_endpointGeneration;
cancel.clipboardGeneration = request.clipboardGeneration;
cancel.transfer = request.transfer;
cancel.format = request.format;
cancel.token = ERROR_DEVICE_NOT_CONNECTED;
QueueInternalTarget(cancel);
}
void CLGMPClipboardTransport::QueueStaleFileRecord(
const KVMFRClipboardMessage& record)
{
if (!m_target || !m_available || !m_endpointGeneration)
return;
KVMFRClipboardMessage response = {};
response.version = KVMFR_CLIPBOARD_VERSION;
response.generation = m_endpointGeneration;
response.clipboardGeneration = record.clipboardGeneration;
response.transfer = record.transfer;
response.format = KVMFR_CLIPBOARD_FORMAT_FILES;
response.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE)
response.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED;
else if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
response.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
else
return;
QueueInternalTarget(response);
}
void CLGMPClipboardTransport::QueueFileDisconnect()
{
KVMFRClipboardMessage records[
CLGMPClipboardFiles::MAX_ACQUISITIONS +
CLGMPClipboardFiles::MAX_REQUESTS] = {};
const unsigned count = m_files.Disconnect(records, _countof(records),
m_endpointGeneration);
for (unsigned i = 0; i < count; ++i)
QueueInternalTarget(records[i]);
}
bool CLGMPClipboardTransport::QueueInternalTarget(
const KVMFRClipboardMessage& record)
{
if (!m_pendingTarget.valid && !m_streamTargetBusy &&
!m_internalTargetCount)
return BeginTarget(record, false);
if (m_internalTargetCount == INTERNAL_TARGET_COUNT)
{
m_failed = true;
return false;
}
m_internalTarget[m_internalTargetCount++] = record;
return true;
}
bool CLGMPClipboardTransport::PumpInternalTarget()
{
if (m_pendingTarget.valid || m_streamTargetBusy ||
!m_internalTargetCount)
return true;
const KVMFRClipboardMessage record = m_internalTarget[0];
for (unsigned i = 1; i < m_internalTargetCount; ++i)
m_internalTarget[i - 1] = m_internalTarget[i];
m_internalTarget[--m_internalTargetCount] = {};
return BeginTarget(record, false);
}
void CLGMPClipboardTransport::ClearStreamTargets()
{
for (StreamTarget& target : m_streamTarget)
target.Clear();
m_streamTargetHead = 0;
m_streamTargetCount = 0;
m_streamTargetBusy = false;
}
void CLGMPClipboardTransport::ClearQueuedStreamTargets()
{
if (!m_streamTargetBusy)
{
ClearStreamTargets();
return;
}
// The BUSY head has already crossed the target boundary and must receive
// the same drain treatment as a BUSY PendingTarget. Only discard records
// which have not yet been presented to the target.
for (unsigned i = 1; i < m_streamTargetCount; ++i)
m_streamTarget[
(m_streamTargetHead + i) % STREAM_TARGET_COUNT].Clear();
m_streamTargetCount = 1;
}
bool CLGMPClipboardTransport::QueueStreamTarget(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
if (m_streamTargetCount == STREAM_TARGET_COUNT ||
(record.length && !data))
return false;
StreamTarget& target = m_streamTarget[
(m_streamTargetHead + m_streamTargetCount) % STREAM_TARGET_COUNT];
target.record = record;
if (record.length)
WCCopy::Copy(target.data, data, record.length);
++m_streamTargetCount;
return true;
}
void CLGMPClipboardTransport::PopStreamTarget()
{
StreamTarget& target = m_streamTarget[m_streamTargetHead];
target.Clear();
m_streamTargetHead =
(m_streamTargetHead + 1) % STREAM_TARGET_COUNT;
--m_streamTargetCount;
m_streamTargetBusy = false;
}
bool CLGMPClipboardTransport::PublishStreamTarget()
{
StreamTarget& target = m_streamTarget[m_streamTargetHead];
const ClipboardChannelResult result = m_target ?
m_target->SendClipboard(target.record,
target.record.length ? target.data : nullptr) :
ClipboardChannelResult::FAILED;
if (result == ClipboardChannelResult::BUSY)
{
m_streamTargetBusy = true;
if (m_ownerClientID)
RenewLease();
return true;
}
if (result == ClipboardChannelResult::ACCEPTED)
{
ApplyInbound(target.record);
if (m_ownerClientID && !m_ownerReleasing)
RenewLease();
}
PopStreamTarget();
if (result == ClipboardChannelResult::FAILED)
{
ReleaseOwner("Helper delivery failed", false);
m_failed = true;
}
return true;
}
bool CLGMPClipboardTransport::RetryStreamTarget()
{
if (!m_streamTargetBusy || m_pendingTarget.valid)
return true;
// The retained head is already validated and stamped. Retry it exactly;
// owner-release state may have changed while the target was BUSY.
m_streamTargetBusy = false;
return PublishStreamTarget();
}
bool CLGMPClipboardTransport::ProcessStreamTarget()
{
StreamTarget& target = m_streamTarget[m_streamTargetHead];
const KVMFRClipboardMessage record = target.record;
if (!m_ownerClientID)
{
PopStreamTarget();
return true;
}
if (m_ownerReleasing && !m_releaseClearHelper)
{
PopStreamTarget();
return true;
}
if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA &&
record.transfer == m_discardClientToHelper)
{
PopStreamTarget();
return true;
}
const bool fileData =
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
const bool staleFileData = fileData && m_files.IsStaleTerminal(
record, CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER);
const bool validData = fileData ?
(staleFileData || m_files.Validate(record,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER, 0, false)) :
(!record.token && kvmfrClipboardTransferFromHelper(record.transfer) &&
ValidateChunk(m_clientToHelper, record));
if (!validData)
{
PopStreamTarget();
ReleaseOwner("invalid clipboard stream data", true);
return true;
}
if (!m_ownerReleasing)
RenewLease();
if (staleFileData)
{
PopStreamTarget();
return true;
}
target.record.generation = m_endpointGeneration;
return PublishStreamTarget();
}
bool CLGMPClipboardTransport::PumpStreamTarget()
{
while (!m_pendingTarget.valid && !m_streamTargetBusy &&
m_streamTargetCount)
if (!ProcessStreamTarget())
return false;
return true;
}
bool CLGMPClipboardTransport::DrainStream(bool& received)
{
received = false;
if (!m_ownerClientID || !m_clientToHostStream)
return true;
for (unsigned drained = 0;
drained < STREAM_DRAIN_MAX &&
m_streamTargetCount < STREAM_TARGET_COUNT; ++drained)
{
LGMPStreamBuffer buffer = {};
LGMP_STATUS status =
lgmpHostStreamReadPeek(m_clientToHostStream, &buffer);
if (status == LGMP_ERR_STREAM_EMPTY)
break;
if (status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
{
if (m_ownerReleasing && status == LGMP_ERR_STREAM_UNBOUND)
break;
ReleaseOwner("clipboard stream binding lost", true);
return true;
}
if (status != LGMP_OK)
{
Fail("lgmpHostStreamReadPeek", status);
return false;
}
received = true;
KVMFRClipboardMessage record = {};
if (buffer.data && buffer.size >= sizeof(record))
memcpy(&record, buffer.data, sizeof(record));
const bool valid = buffer.data &&
buffer.size == sizeof(record) + record.length &&
record.version == KVMFR_CLIPBOARD_VERSION &&
record.generation == m_ownerGeneration &&
(record.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA) &&
record.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
!(record.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)) &&
AddValid(record.offset, record.length);
const bool queued = valid && QueueStreamTarget(record,
static_cast<const uint8_t *>(buffer.data) + sizeof(record));
status = lgmpHostStreamReadRelease(m_clientToHostStream, &buffer);
if (status != LGMP_OK)
{
Fail("lgmpHostStreamReadRelease", status);
return false;
}
if (!queued)
{
ReleaseOwner("invalid clipboard stream record", true);
return true;
}
if (!PumpStreamTarget())
return false;
}
return PumpStreamTarget();
}
void CLGMPClipboardTransport::ReleaseOwner(
const char * reason, bool clearHelper, bool force)
{
if (!m_ownerClientID)
return;
if (!m_ownerReleasing)
{
m_ownerReleasing = true;
m_releaseClearHelper = clearHelper;
m_ownerReleaseReason = reason;
m_ownerDeadline = 0;
m_ownerDrainDeadline = GetTickCount64() + STREAM_DRAIN_TIMEOUT_MS;
m_replayPending = false;
ClearOutboundBlock();
}
else if (force)
{
m_releaseClearHelper |= clearHelper;
m_ownerReleaseReason = reason;
}
if (force)
{
ForceUnbindStreams();
FinishOwnerRelease();
return;
}
if (TryUnbindStreams() && !m_pendingTarget.valid &&
!m_streamTargetCount)
FinishOwnerRelease();
}
void CLGMPClipboardTransport::FinishOwnerRelease()
{
if (!m_ownerClientID)
return;
const uint32_t clientID = m_ownerClientID;
const uint32_t generation = m_ownerGeneration;
const char * const reason = m_ownerReleaseReason ?
m_ownerReleaseReason : "unspecified";
const Transfer clientToHelper = m_clientToHelper;
const Transfer helperToClient = m_helperToClient;
const bool clearHelper = m_releaseClearHelper;
m_ownerClientID = 0;
m_ownerGeneration = 0;
m_ownerReleasing = false;
m_releaseClearHelper = false;
m_ownerReleaseReason = nullptr;
m_ownerDeadline = 0;
m_ownerDrainDeadline = 0;
m_replayPending = false;
ClearQueuedStreamTargets();
m_discardClientToHelper = 0;
m_clientToHelper.Clear();
m_helperToClient.Clear();
if (helperToClient.Active())
m_discardHelperToClient = helperToClient.transfer;
m_statusDirty = true;
if (clearHelper)
{
QueueFileDisconnect();
QueueTransferCancel(clientToHelper);
QueueTransferCancel(helperToClient);
QueueHelperClear();
}
else
{
m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
}
DEBUG_INFO("Clipboard owner %u generation %u released (%s)",
clientID, generation, reason);
}
bool CLGMPClipboardTransport::RetryOwnerRelease()
{
if (!m_ownerReleasing)
return true;
if (GetTickCount64() >= m_ownerDrainDeadline)
{
DEBUG_WARN("Timed out draining clipboard streams for owner %u "
"generation %u; discarding in-flight records",
m_ownerClientID, m_ownerGeneration);
DropPendingTarget();
ClearStreamTargets();
ForceUnbindStreams();
FinishOwnerRelease();
return true;
}
if (TryUnbindStreams() && !m_pendingTarget.valid &&
!m_streamTargetCount)
FinishOwnerRelease();
return !m_failed;
}
void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard)
{
DropPendingTarget();
if (m_ownerClientID)
ReleaseOwner("endpoint reset", false);
m_clientToHelper.Clear();
m_helperToClient.Clear();
m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
m_discardClientToHelper = 0;
m_discardHelperToClient = 0;
m_replayPending = false;
ClearOutboundBlock();
m_internalTargetCount = 0;
ClearStreamTargets();
for (KVMFRClipboardMessage& record : m_internalTarget)
record = {};
if (!keepClipboard)
{
m_cachedValid = false;
m_cachedClipboard = {};
m_helperFormats = 0;
}
m_statusDirty = true;
}
PLGMPMemory CLGMPClipboardTransport::FindAvailable(
PLGMPMemory (&memory)[MEMORY_COUNT]) const
{
for (PLGMPMemory candidate : memory)
if (candidate && !lgmpHostQueuePayloadPending(m_queue, candidate))
return candidate;
return nullptr;
}
CLGMPClipboardTransport::PostResult
CLGMPClipboardTransport::PostForOwner(
uint64_t udata, PLGMPMemory memory)
{
if (!m_ownerClientID)
return PostResult::GONE;
unsigned recipients = 0;
const uint32_t clientID = m_ownerClientID;
const LGMP_STATUS status = lgmpHostQueuePostForClients(
m_queue, udata, memory, &clientID, 1, &recipients);
if (status == LGMP_ERR_QUEUE_FULL)
return PostResult::BUSY;
if (status != LGMP_OK)
{
Fail("lgmpHostQueuePostForClients", status);
return PostResult::FAILED;
}
return recipients ? PostResult::POSTED : PostResult::GONE;
}
void CLGMPClipboardTransport::Fail(
const char * operation, LGMP_STATUS status)
{
if (!m_failed)
DEBUG_ERROR("%s Failed (Clipboard): %s",
operation, lgmpStatusString(status));
m_failed = true;
if (m_stopEvent)
SetEvent(m_stopEvent);
}
bool CLGMPClipboardTransport::PublishStatus()
{
if (lgmpHostQueueNewSubs(m_queue))
m_statusDirty = true;
if (!m_statusDirty)
return true;
uint32_t clients[LGMP_MAX_CLIENTS] = {};
unsigned clientCount = 0;
LGMP_STATUS status =
lgmpHostGetClientIDs(m_queue, clients, &clientCount);
if (status != LGMP_OK)
{
Fail("lgmpHostGetClientIDs", status);
return false;
}
if (!clientCount)
return true;
PLGMPMemory memory = FindAvailable(m_statusMemory);
if (!memory)
return true;
KVMFRClipboardStatus clipboardStatus = {};
clipboardStatus.version = KVMFR_CLIPBOARD_VERSION;
clipboardStatus.generation = m_endpointGeneration;
clipboardStatus.lease = static_cast<uint32_t>(OWNER_LEASE_MS);
clipboardStatus.slotBytes = KVMFR_CLIPBOARD_DATA_BYTES;
clipboardStatus.streamVersion = KVMFR_CLIPBOARD_STREAM_VERSION;
clipboardStatus.streamSlotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT;
clipboardStatus.hostToClient = m_hostToClientDescriptor;
clipboardStatus.clientToHost = m_clientToHostDescriptor;
if (m_available)
{
clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_AVAILABLE;
clipboardStatus.formats = m_helperFormats;
}
if (m_available && m_ownerClientID)
{
clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_HAS_OWNER;
clipboardStatus.ownerClientID = m_ownerClientID;
clipboardStatus.ownerGeneration = m_ownerGeneration;
}
memcpy(lgmpHostMemPtr(memory), &clipboardStatus,
sizeof(clipboardStatus));
const uint32_t serial = Seq::Next(m_statusSerial);
unsigned recipients = 0;
status = lgmpHostQueuePostForClients(m_queue,
KVMFR_CLIPBOARD_QUEUE_UDATA(KVMFR_CLIPBOARD_QUEUE_STATUS, serial),
memory, clients, clientCount, &recipients);
if (status == LGMP_ERR_QUEUE_FULL)
return true;
if (status != LGMP_OK)
{
Fail("lgmpHostQueuePostForClients", status);
return false;
}
if (recipients)
{
m_statusSerial = serial;
m_statusDirty = false;
}
return true;
}
bool CLGMPClipboardTransport::ReplayClipboard()
{
if (!m_replayPending || !m_ownerClientID || m_ownerReleasing)
return true;
if (!m_cachedValid)
{
m_replayPending = false;
return true;
}
PLGMPMemory memory = FindAvailable(m_messageMemory);
if (!memory)
return true;
KVMFRClipboardMessage message = m_cachedClipboard;
message.generation = m_ownerGeneration;
memcpy(lgmpHostMemPtr(memory), &message, sizeof(message));
const uint32_t serial = Seq::Next(m_messageSerial);
const PostResult result = PostForOwner(
KVMFR_CLIPBOARD_QUEUE_UDATA(
KVMFR_CLIPBOARD_QUEUE_MESSAGE, serial), memory);
if (result == PostResult::POSTED)
{
m_messageSerial = serial;
m_replayPending = false;
}
else if (result == PostResult::GONE)
ReleaseOwner("subscriber disappeared", true, true);
return result != PostResult::FAILED;
}
bool CLGMPClipboardTransport::ValidateClaim(
const KVMFRClipboardMessage& message) const
{
return message.version == KVMFR_CLIPBOARD_VERSION &&
message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM &&
message.generation && message.token == m_endpointGeneration &&
m_hostToClientStream && m_clientToHostStream &&
EmptyControl(message, true);
}
bool CLGMPClipboardTransport::ValidateOwnedControl(
const KVMFRClipboardMessage& message) const
{
if (message.version != KVMFR_CLIPBOARD_VERSION ||
message.generation != m_ownerGeneration)
return false;
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_RELEASE:
case KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE:
return EmptyControl(message);
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
return ValidateInboundRecord(message);
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
return ValidateInboundRecord(message);
default:
return false;
}
}
bool CLGMPClipboardTransport::ValidateInboundRecord(
const KVMFRClipboardMessage& message) const
{
if (CLGMPClipboardFiles::IsRecord(message))
{
if (m_files.IsStaleTerminal(message,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER))
return true;
const bool filesOffered = m_cachedValid &&
m_cachedClipboard.type == KVMFR_CLIPBOARD_MESSAGE_OFFER &&
(m_helperFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES);
const uint64_t offeredDataset = filesOffered ?
m_cachedClipboard.clipboardGeneration : 0;
return m_files.Validate(message,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER,
offeredDataset, filesOffered);
}
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
return ValidOffer(message);
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
return ValidClear(message);
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
return ValidRequest(message, false);
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
return ValidCancel(message);
default:
return false;
}
}
bool CLGMPClipboardTransport::ValidateOutboundRecord(
const KVMFRClipboardMessage& message) const
{
if (message.version != KVMFR_CLIPBOARD_VERSION ||
message.generation != m_endpointGeneration)
return false;
if (CLGMPClipboardFiles::IsRecord(message))
{
const bool filesOffered = m_clientClipboardGeneration &&
(m_clientFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES);
return m_files.Validate(message,
CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT,
m_clientClipboardGeneration, filesOffered);
}
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
return ValidOffer(message);
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
return ValidClear(message);
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
return ValidRequest(message, true);
case KVMFR_CLIPBOARD_MESSAGE_DATA:
return message.clipboardGeneration &&
kvmfrClipboardTransferFromClient(message.transfer) &&
kvmfrClipboardRepresentationFormatValid(message.format) &&
message.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
!(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)) && !message.token &&
AddValid(message.offset, message.length);
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
return ValidCancel(message);
default:
return false;
}
}
bool CLGMPClipboardTransport::ValidateChunk(
const Transfer& transfer, const KVMFRClipboardMessage& message) const
{
if (!transfer.Active() || message.transfer != transfer.transfer ||
message.clipboardGeneration != transfer.clipboardGeneration ||
message.format != transfer.format ||
message.offset != transfer.nextOffset ||
message.sequence != transfer.nextSequence ||
!AddValid(message.offset, message.length))
return false;
const uint64_t end = message.offset + message.length;
if (!message.length &&
!(message.flags & (KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)))
return false;
if (!transfer.began)
{
if (message.offset || message.sequence ||
!(message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN))
return false;
}
else if (message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)
return false;
else if (!(message.flags & KVMFR_CLIPBOARD_FLAG_END) &&
message.size != KVMFR_CLIPBOARD_SIZE_UNKNOWN)
return false;
const uint64_t hint = transfer.began ? transfer.sizeHint : message.size;
if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint)
return false;
if (message.flags & KVMFR_CLIPBOARD_FLAG_END)
return message.size == end &&
(hint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || hint == end);
return true;
}
void CLGMPClipboardTransport::AdvanceChunk(
Transfer& transfer, const KVMFRClipboardMessage& message)
{
if (!transfer.began)
{
transfer.began = true;
transfer.sizeHint = message.size;
}
transfer.nextOffset += message.length;
++transfer.nextSequence;
if (message.flags & KVMFR_CLIPBOARD_FLAG_END)
transfer.Clear();
}
void CLGMPClipboardTransport::ApplyInbound(
const KVMFRClipboardMessage& message)
{
if (CLGMPClipboardFiles::IsRecord(message))
{
m_files.Apply(message,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER);
return;
}
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
m_clientClipboardGeneration = message.clipboardGeneration;
m_clientFormats = message.token;
if (m_clientToHelper.Active())
m_discardClientToHelper = m_clientToHelper.transfer;
m_clientToHelper.Clear();
break;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
if (m_clientToHelper.Active())
m_discardClientToHelper = m_clientToHelper.transfer;
m_clientToHelper.Clear();
break;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
m_helperToClient.Clear();
m_helperToClient.transfer = message.transfer;
m_helperToClient.clipboardGeneration =
message.clipboardGeneration;
m_helperToClient.format = message.format;
break;
case KVMFR_CLIPBOARD_MESSAGE_DATA:
AdvanceChunk(m_clientToHelper, message);
break;
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
if (m_clientToHelper.transfer == message.transfer)
{
m_discardClientToHelper = message.transfer;
m_clientToHelper.Clear();
}
if (m_helperToClient.transfer == message.transfer)
m_helperToClient.Clear();
break;
}
}
void CLGMPClipboardTransport::ApplyOutbound(
const KVMFRClipboardMessage& message)
{
if (CLGMPClipboardFiles::IsRecord(message))
{
m_files.Apply(message,
CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT);
return;
}
switch (message.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
m_cachedClipboard = message;
m_cachedValid = true;
m_helperFormats = message.token;
m_helperToClient.Clear();
m_replayPending = false;
m_statusDirty = true;
break;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
m_cachedClipboard = message;
m_cachedValid = true;
m_helperFormats = 0;
m_helperToClient.Clear();
m_replayPending = false;
m_statusDirty = true;
break;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
m_clientToHelper.Clear();
m_discardClientToHelper = 0;
m_clientToHelper.transfer = message.transfer;
m_clientToHelper.clipboardGeneration =
message.clipboardGeneration;
m_clientToHelper.format = message.format;
break;
case KVMFR_CLIPBOARD_MESSAGE_DATA:
AdvanceChunk(m_helperToClient, message);
break;
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
if (m_clientToHelper.transfer == message.transfer)
m_clientToHelper.Clear();
if (m_helperToClient.transfer == message.transfer)
m_helperToClient.Clear();
break;
}
}
bool CLGMPClipboardTransport::BeginTarget(
const KVMFRClipboardMessage& message, bool acknowledge)
{
if (m_pendingTarget.valid || message.length)
return false;
m_pendingTarget.valid = true;
m_pendingTarget.acknowledge = acknowledge;
m_pendingTarget.record = message;
return RetryTarget();
}
void CLGMPClipboardTransport::FinishTarget(bool accepted)
{
const KVMFRClipboardMessage message = m_pendingTarget.record;
if (accepted)
{
ApplyInbound(message);
if (m_ownerClientID && !m_ownerReleasing)
RenewLease();
}
m_pendingTarget.Clear();
if (!accepted)
{
ReleaseOwner("Helper delivery failed", false);
m_failed = true;
}
}
bool CLGMPClipboardTransport::RetryTarget()
{
if (!m_pendingTarget.valid)
return true;
const bool acknowledge = m_pendingTarget.acknowledge;
const ClipboardChannelResult result = m_target ?
m_target->SendClipboard(m_pendingTarget.record, nullptr) :
ClipboardChannelResult::FAILED;
if (result == ClipboardChannelResult::BUSY)
{
if (m_ownerClientID)
RenewLease();
return true;
}
FinishTarget(result == ClipboardChannelResult::ACCEPTED);
if (acknowledge)
{
const LGMP_STATUS status = lgmpHostAckData(m_queue);
if (status != LGMP_OK)
{
Fail("lgmpHostAckData", status);
return false;
}
}
return true;
}
void CLGMPClipboardTransport::DropPendingTarget()
{
if (!m_pendingTarget.valid)
return;
const bool acknowledge = m_pendingTarget.acknowledge;
m_pendingTarget.Clear();
if (acknowledge && m_queue)
{
const LGMP_STATUS status = lgmpHostAckData(m_queue);
if (status != LGMP_OK)
Fail("lgmpHostAckData", status);
}
}
bool CLGMPClipboardTransport::ProcessMessage(
uint32_t clientID, const KVMFRClipboardMessage& message)
{
if (message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM)
{
if (!m_available || !ValidateClaim(message))
return true;
if (m_ownerClientID)
{
if (!m_ownerReleasing &&
IsOwner(clientID, message.generation))
RenewLease();
return true;
}
if (!BindStreams(clientID))
{
DeInitStreams();
m_failed = true;
if (m_stopEvent)
SetEvent(m_stopEvent);
return true;
}
m_ownerClientID = clientID;
m_ownerGeneration = message.generation;
RenewLease();
m_statusDirty = true;
m_replayPending = m_cachedValid;
DEBUG_INFO("Clipboard owner %u generation %u acquired",
m_ownerClientID, m_ownerGeneration);
return true;
}
if (!IsOwner(clientID, message.generation))
return true;
if (m_ownerReleasing)
return true;
if (CLGMPClipboardFiles::IsRecord(message) &&
CLGMPClipboardFiles::DirectionValid(message,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER) &&
!m_files.TransferActive(message.transfer) &&
(message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST) &&
!ValidateInboundRecord(message))
{
// A cancellation or replacement can cross a request which was generated
// from the preceding file offer. The already-posted lifecycle record will
// cancel the client-side work; consuming this stale request avoids
// treating normal cross-queue ordering as a protocol failure.
RenewLease();
return true;
}
if (!ValidateOwnedControl(message))
{
ReleaseOwner("invalid clipboard message", true);
return true;
}
if (message.type == KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE)
{
RenewLease();
return true;
}
if (message.type == KVMFR_CLIPBOARD_MESSAGE_RELEASE)
{
ReleaseOwner("client release", true);
return true;
}
if (m_files.IsStaleTerminal(message,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER))
{
RenewLease();
return true;
}
if (message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
m_helperToClient.Active())
{
ReleaseOwner("overlapping clipboard request", true);
return true;
}
if (message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
(!m_cachedValid ||
m_cachedClipboard.type != KVMFR_CLIPBOARD_MESSAGE_OFFER ||
message.clipboardGeneration !=
m_cachedClipboard.clipboardGeneration ||
!(m_helperFormats & kvmfrClipboardFormatFlag(message.format))))
{
ReleaseOwner("invalid clipboard request", true);
return true;
}
if (message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL &&
message.transfer != m_clientToHelper.transfer &&
message.transfer != m_helperToClient.transfer)
return true;
KVMFRClipboardMessage forwarded = message;
forwarded.generation = m_endpointGeneration;
RenewLease();
BeginTarget(forwarded, true);
return false;
}
bool CLGMPClipboardTransport::DrainMessage()
{
for (unsigned count = 0; count < 64 && !m_pendingTarget.valid; ++count)
{
uint8_t data[LGMP_MSGS_SIZE] = {};
size_t size = 0;
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
m_queue, data, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
return true;
if (status != LGMP_OK)
{
Fail("lgmpHostReadDataWithSource", status);
return false;
}
bool acknowledge = true;
if (size != sizeof(KVMFRClipboardMessage))
{
if (clientID == m_ownerClientID)
ReleaseOwner("invalid clipboard message size", true);
}
else
{
KVMFRClipboardMessage message = {};
memcpy(&message, data, sizeof(message));
acknowledge = ProcessMessage(clientID, message);
}
if (acknowledge)
{
const LGMP_STATUS ackStatus = lgmpHostAckData(m_queue);
if (ackStatus != LGMP_OK)
{
Fail("lgmpHostAckData", ackStatus);
return false;
}
}
}
return true;
}
ClipboardChannelResult CLGMPClipboardTransport::SendControl(
const KVMFRClipboardMessage& record)
{
PLGMPMemory memory = FindAvailable(m_messageMemory);
if (!memory)
return ClipboardChannelResult::BUSY;
KVMFRClipboardMessage message = record;
message.generation = m_ownerGeneration;
memcpy(lgmpHostMemPtr(memory), &message, sizeof(message));
const uint32_t serial = Seq::Next(m_messageSerial);
const PostResult result = PostForOwner(
KVMFR_CLIPBOARD_QUEUE_UDATA(
KVMFR_CLIPBOARD_QUEUE_MESSAGE, serial), memory);
if (result == PostResult::POSTED)
{
m_messageSerial = serial;
ApplyOutbound(record);
return ClipboardChannelResult::ACCEPTED;
}
if (result == PostResult::BUSY)
return ClipboardChannelResult::BUSY;
if (result == PostResult::GONE)
{
ReleaseOwner("subscriber disappeared", true, true);
return record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR ?
ClipboardChannelResult::ACCEPTED : ClipboardChannelResult::BUSY;
}
return ClipboardChannelResult::FAILED;
}
ClipboardChannelResult CLGMPClipboardTransport::SendData(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
const bool valid = CLGMPClipboardFiles::IsData(record) ?
m_files.Validate(record,
CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT, 0, false) :
ValidateChunk(m_helperToClient, record);
if (!valid)
return ClipboardChannelResult::FAILED;
KVMFRClipboardMessage message = record;
message.generation = m_ownerGeneration;
LGMPStreamBuffer buffer = {};
LGMP_STATUS status =
lgmpHostStreamWriteAcquire(m_hostToClientStream, &buffer);
if (status == LGMP_ERR_STREAM_FULL)
return ClipboardChannelResult::BUSY;
if (status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
{
ReleaseOwner("clipboard stream binding lost", true);
return ClipboardChannelResult::ACCEPTED;
}
if (status != LGMP_OK)
{
Fail("lgmpHostStreamWriteAcquire", status);
return ClipboardChannelResult::FAILED;
}
const uint32_t bytes = sizeof(message) + message.length;
if (!buffer.data || buffer.capacity < bytes)
{
const LGMP_STATUS cancel =
lgmpHostStreamWriteCancel(m_hostToClientStream, &buffer);
if (cancel != LGMP_OK)
DEBUG_ERROR("Failed to cancel an undersized host-to-client "
"clipboard stream reservation: %s", lgmpStatusString(cancel));
DEBUG_ERROR("Host-to-client clipboard stream returned an "
"undersized slot: capacity=%u required=%u",
buffer.capacity, bytes);
m_failed = true;
if (m_stopEvent)
SetEvent(m_stopEvent);
return ClipboardChannelResult::FAILED;
}
memcpy(buffer.data, &message, sizeof(message));
if (message.length)
memcpy(static_cast<uint8_t *>(buffer.data) + sizeof(message), data,
message.length);
status = lgmpHostStreamWriteCommit(
m_hostToClientStream, &buffer, bytes);
if (status == LGMP_OK)
{
ApplyOutbound(record);
return ClipboardChannelResult::ACCEPTED;
}
DEBUG_ERROR("Failed to commit host-to-client clipboard stream data: %s",
lgmpStatusString(status));
if (status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
{
ReleaseOwner("clipboard stream binding lost", true);
return ClipboardChannelResult::ACCEPTED;
}
Fail("lgmpHostStreamWriteCommit", status);
return ClipboardChannelResult::FAILED;
}
ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
CSRWExclusiveLock lock(m_lock);
if (m_failed || !m_target)
return ClipboardChannelResult::FAILED;
// The channel retains a BUSY record for an exact retry. Once the owner
// epoch that caused the backpressure is gone, that record cannot be
// delivered to a replacement owner. Consume it and promptly cancel a
// stale request back toward Helper instead.
if (BlockedOwnerLost(record))
{
QueueStaleRequestCancel(record);
QueueStaleFileRecord(record);
ClearOutboundBlock();
Wake();
return ClipboardChannelResult::ACCEPTED;
}
if (!m_available ||
record.generation != m_endpointGeneration)
return ClipboardChannelResult::FAILED;
if (m_files.IsStaleTerminal(record,
CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT))
{
ClearOutboundBlock();
return ClipboardChannelResult::ACCEPTED;
}
const bool validOutbound = ValidateOutboundRecord(record);
if (!validOutbound && CLGMPClipboardFiles::IsRecord(record) &&
CLGMPClipboardFiles::DirectionValid(record,
CLGMPClipboardFiles::Direction::HELPER_TO_CLIENT) &&
!m_files.TransferActive(record.transfer) &&
(record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST))
{
// The LGMP owner may disappear or replace its dataset before Helper
// observes the matching lifecycle record. Fail new work locally without
// tearing down the shared Helper clipboard channel.
QueueStaleFileRecord(record);
ClearOutboundBlock();
Wake();
return ClipboardChannelResult::ACCEPTED;
}
if (!validOutbound || (record.length && !data))
return ClipboardChannelResult::FAILED;
if (record.transfer &&
record.transfer == m_discardHelperToClient &&
(record.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL))
{
if (record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL ||
(record.flags & KVMFR_CLIPBOARD_FLAG_END))
m_discardHelperToClient = 0;
ClearOutboundBlock();
return ClipboardChannelResult::ACCEPTED;
}
if (record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL &&
record.transfer != m_clientToHelper.transfer &&
record.transfer != m_helperToClient.transfer)
{
ClearOutboundBlock();
return ClipboardChannelResult::ACCEPTED;
}
// Preserve the latest Helper clipboard even when no client owns the
// endpoint. It is replayed to the next successful claimant.
if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR)
{
m_cachedClipboard = record;
m_cachedValid = true;
m_helperFormats = record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ?
record.token : 0;
m_statusDirty = true;
if (!m_ownerClientID || m_ownerReleasing)
{
ClearOutboundBlock();
Wake();
return ClipboardChannelResult::ACCEPTED;
}
}
if (!m_ownerClientID || m_ownerReleasing)
{
if (OwnerScopedLifecycle(record))
{
QueueStaleRequestCancel(record);
QueueStaleFileRecord(record);
ClearOutboundBlock();
Wake();
return ClipboardChannelResult::ACCEPTED;
}
return ClipboardChannelResult::FAILED;
}
const uint32_t ownerClientID = m_ownerClientID;
const uint32_t ownerGeneration = m_ownerGeneration;
if (!OwnerSubscribed())
{
BlockOutbound(record, ownerClientID, ownerGeneration);
Wake();
return ClipboardChannelResult::BUSY;
}
ClipboardChannelResult result;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA)
result = SendData(record, data);
else if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
{
if (!m_helperToClient.Active() ||
record.transfer != m_helperToClient.transfer)
{
ClearOutboundBlock();
return ClipboardChannelResult::ACCEPTED;
}
result = SendData(record, data);
}
else
{
if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
m_clientToHelper.Active())
return ClipboardChannelResult::FAILED;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST &&
(record.clipboardGeneration != m_clientClipboardGeneration ||
!(m_clientFormats & kvmfrClipboardFormatFlag(record.format))))
{
QueueStaleRequestCancel(record);
ClearOutboundBlock();
return ClipboardChannelResult::ACCEPTED;
}
result = SendControl(record);
}
if (result == ClipboardChannelResult::BUSY)
BlockOutbound(record, ownerClientID, ownerGeneration);
else
ClearOutboundBlock();
Wake();
return result;
}
void CLGMPClipboardTransport::ClipboardState(
bool available, uint32_t generation)
{
CSRWExclusiveLock lock(m_lock);
if (!generation)
return;
if (generation != m_endpointGeneration || available != m_available)
{
DropPendingTarget();
ClearStreamTargets();
m_internalTargetCount = 0;
for (KVMFRClipboardMessage& record : m_internalTarget)
record = {};
if (m_ownerClientID)
ReleaseOwner("Helper endpoint changed", false);
m_clientToHelper.Clear();
m_helperToClient.Clear();
m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
m_discardClientToHelper = 0;
m_discardHelperToClient = 0;
m_cachedClipboard = {};
m_cachedValid = false;
m_helperFormats = 0;
}
m_available = available;
m_endpointGeneration = generation;
m_statusDirty = true;
Wake();
}
void CLGMPClipboardTransport::ClipboardReset(
uint32_t generation, uint32_t reason)
{
UNREFERENCED_PARAMETER(reason);
CSRWExclusiveLock lock(m_lock);
if (!generation)
return;
DropPendingTarget();
ClearStreamTargets();
m_internalTargetCount = 0;
for (KVMFRClipboardMessage& record : m_internalTarget)
record = {};
if (m_ownerClientID)
ReleaseOwner("Helper reset", false);
m_clientToHelper.Clear();
m_helperToClient.Clear();
m_files.Reset();
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
m_discardClientToHelper = 0;
m_discardHelperToClient = 0;
m_cachedClipboard = {};
m_cachedValid = false;
m_helperFormats = 0;
m_endpointGeneration = generation;
m_statusDirty = true;
Wake();
}
DWORD CALLBACK CLGMPClipboardTransport::ThreadProc(void * context)
{
static_cast<CLGMPClipboardTransport *>(context)->Thread();
return 0;
}
void CLGMPClipboardTransport::Thread()
{
LGMPStreamPollState streamPoll = {};
const LGMPStreamPollConfig pollConfig =
{
32U,
50U,
1000U,
};
const LGMP_STATUS pollStatus = lgmpStreamPollInit(&streamPoll,
pollConfig);
if (pollStatus != LGMP_OK)
{
DEBUG_ERROR("Failed to initialize LGMP clipboard polling: %s",
lgmpStatusString(pollStatus));
if (m_target)
m_target->ClipboardFailed();
return;
}
bool notifyFailed = false;
for (;;)
{
uint32_t waitUs = IDLE_POLL_MS * 1000U;
bool streamReceived = false;
bool notifyReady = false;
IClipboardTarget * readyTarget = nullptr;
{
CSRWExclusiveLock lock(m_lock);
if (m_failed)
{
notifyFailed = true;
break;
}
if (m_ownerClientID && !OwnerSubscribed())
{
DropPendingTarget();
ClearStreamTargets();
ReleaseOwner("subscriber disappeared", true, true);
}
else if (m_ownerClientID && !m_ownerReleasing &&
GetTickCount64() >= m_ownerDeadline)
ReleaseOwner("lease expired", true);
// A restarted client can reuse dataset and transfer IDs. Deliver every
// old-owner cleanup record before admitting messages from a new owner.
if (!RetryTarget() || !RetryStreamTarget() ||
!PumpInternalTarget() ||
!PumpStreamTarget() ||
(!m_streamTargetBusy && m_internalTargetCount == 0 &&
!DrainMessage()) ||
!DrainStream(streamReceived) ||
!RetryOwnerRelease() ||
!PublishStatus() ||
!ReplayClipboard())
{
notifyFailed = true;
break;
}
if (m_outboundBlocked)
{
notifyReady = m_target != nullptr;
readyTarget = m_target;
}
if (m_pendingTarget.valid || m_internalTargetCount ||
m_streamTargetCount || m_ownerClientID)
waitUs = ACTIVE_POLL_MS * 1000U;
if (m_ownerClientID)
{
if (streamReceived)
lgmpStreamPollActivity(&streamPoll);
waitUs = lgmpStreamPollIdle(&streamPoll);
}
else
lgmpStreamPollActivity(&streamPoll);
}
// ClipboardReceiveReady may synchronously retry SendClipboard, which
// acquires m_lock. Never invoke it while holding the transport lock.
if (notifyReady)
readyTarget->ClipboardReceiveReady();
if (!waitUs)
continue;
if (!ArmPollTimer(m_pollTimer, waitUs))
{
const DWORD error = GetLastError();
if (WaitForSingleObject(m_stopEvent, 0) == WAIT_OBJECT_0)
break;
DEBUG_ERROR_HR(error,
"Failed to arm LGMP clipboard poll timer");
notifyFailed = true;
break;
}
const HANDLE handles[] =
{
m_stopEvent,
m_wakeEvent,
m_pollTimer,
};
const DWORD wait = WaitForMultipleObjects(
_countof(handles), handles, FALSE, INFINITE);
if (wait == WAIT_OBJECT_0)
break;
if (wait == WAIT_OBJECT_0 + 1)
{
lgmpStreamPollActivity(&streamPoll);
continue;
}
if (wait == WAIT_OBJECT_0 + 2)
continue;
if (wait == WAIT_FAILED)
{
DEBUG_ERROR_HR(GetLastError(),
"LGMP clipboard worker wait failed");
notifyFailed = true;
break;
}
DEBUG_ERROR("LGMP clipboard worker returned an unexpected wait "
"result: %lu", wait);
notifyFailed = true;
break;
}
IClipboardTarget * target = nullptr;
{
CSRWExclusiveLock lock(m_lock);
DropPendingTarget();
ClearStreamTargets();
if (m_ownerClientID)
ReleaseOwner("transport stopped", false, true);
target = m_target;
}
if (notifyFailed && target)
target->ClipboardFailed();
}