Files
LookingGlass/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp
Geoffrey McRae 72522dadb8 [idd] clipboard: carry payloads over streams
Create one reliable four-slot lane in each direction and advertise the
stream data plane for clipboard protocol v4.

Bind both lanes to the accepted owner generation, stage incoming data
within a fixed window, and apply backpressure when Helper delivery is
full.

Drain reliable records before releasing an owner. Force stale or
unresponsive bindings after a bounded deadline while preserving the
existing cancellation and clipboard-clear cleanup.
2026-08-15 14:38:16 +10:00

2087 lines
60 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 "transport/lgmp/CLGMPClipboardTransport.h"
#include "CDebug.h"
#include "Seq.h"
#include "transport/lgmp/CLGMPHost.h"
#include <limits>
#include <string.h>
namespace
{
static constexpr DWORD WAIT_FIRST_OBJECT_VALUE = 0;
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 EmptyControl(const KVMFRClipboardMessage& message,
bool keepToken = false, bool keepFlags = false)
{
return !message.clipboardGeneration && !message.transfer &&
!message.offset && !message.size && !message.format &&
(keepFlags || !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_dataMemory)
lgmpHostMemFree(&memory);
for (PLGMPMemory& memory : m_grantMemory)
lgmpHostMemFree(&memory);
for (PLGMPMemory& memory : m_messageMemory)
lgmpHostMemFree(&memory);
for (PLGMPMemory& memory : m_statusMemory)
lgmpHostMemFree(&memory);
m_queue = nullptr;
}
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)
{
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()
{
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()
{
if (m_wakeEvent)
SetEvent(m_wakeEvent);
}
bool CLGMPClipboardTransport::Start(IClipboardTarget& target)
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
if (m_thread)
{
if (WaitForSingleObject(m_thread, 0) == WAIT_TIMEOUT)
return true;
CloseHandle(m_thread);
CloseHandle(m_wakeEvent);
CloseHandle(m_stopEvent);
m_thread = nullptr;
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
}
if (!m_queue)
return false;
m_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
m_wakeEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!m_stopEvent || !m_wakeEvent)
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to create LGMP clipboard worker events");
if (m_wakeEvent)
CloseHandle(m_wakeEvent);
if (m_stopEvent)
CloseHandle(m_stopEvent);
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
return false;
}
{
CSRWExclusiveLock lock(m_lock);
m_target = &target;
m_failed = false;
m_statusDirty = true;
}
m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
if (!m_thread)
{
DEBUG_ERROR_HR(GetLastError(),
"Failed to create LGMP clipboard worker");
{
CSRWExclusiveLock lock(m_lock);
m_target = nullptr;
}
CloseHandle(m_wakeEvent);
CloseHandle(m_stopEvent);
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
return false;
}
return true;
}
void CLGMPClipboardTransport::Stop()
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
if (m_stopEvent)
SetEvent(m_stopEvent);
if (m_thread)
WaitForSingleObject(m_thread, INFINITE);
if (m_thread)
CloseHandle(m_thread);
if (m_wakeEvent)
CloseHandle(m_wakeEvent);
if (m_stopEvent)
CloseHandle(m_stopEvent);
m_thread = nullptr;
m_wakeEvent = nullptr;
m_stopEvent = nullptr;
CSRWExclusiveLock lock(m_lock);
DropPendingTarget();
ResetProtocol(false);
m_available = false;
m_endpointGeneration = 0;
m_target = nullptr;
m_failed = false;
m_statusDirty = true;
}
bool CLGMPClipboardTransport::IsOwner(
uint32_t clientID, uint32_t generation) const
{
return m_ownerClientID == clientID &&
m_ownerGeneration == generation;
}
bool CLGMPClipboardTransport::OwnerSubscribed() const
{
if (!m_ownerClientID)
return false;
uint32_t clients[LGMP_MAX_CLIENTS] = {};
unsigned count = 0;
if (lgmpHostGetClientIDs(m_queue, clients, &count) != LGMP_OK)
return false;
for (unsigned i = 0; i < count; ++i)
if (clients[i] == m_ownerClientID)
return true;
return false;
}
void CLGMPClipboardTransport::RenewLease()
{
m_ownerDeadline = GetTickCount64() + OWNER_LEASE_MS;
}
void CLGMPClipboardTransport::BlockOutbound(
const KVMFRClipboardMessage& record, uint32_t ownerClientID,
uint32_t ownerGeneration)
{
m_outboundBlocked = true;
m_blockedOutbound = record;
m_blockedOwnerClientID = ownerClientID;
m_blockedOwnerGeneration = ownerGeneration;
}
void CLGMPClipboardTransport::ClearOutboundBlock()
{
m_outboundBlocked = false;
m_blockedOutbound = {};
m_blockedOwnerClientID = 0;
m_blockedOwnerGeneration = 0;
}
bool CLGMPClipboardTransport::BlockedOwnerLost(
const KVMFRClipboardMessage& record) const
{
return m_outboundBlocked && OwnerScopedLifecycle(record) &&
memcmp(&m_blockedOutbound, &record, sizeof(record)) == 0 &&
(!m_available || 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_internalTargetCount)
return BeginTarget(record, nullptr, -2);
if (m_internalTargetCount == INTERNAL_TARGET_COUNT)
{
m_failed = true;
return false;
}
m_internalTarget[m_internalTargetCount++] = record;
return true;
}
bool CLGMPClipboardTransport::PumpInternalTarget()
{
if (m_pendingTarget.valid || !m_internalTargetCount)
return true;
const KVMFRClipboardMessage record = m_internalTarget[0];
for (unsigned i = 1; i < m_internalTargetCount; ++i)
m_internalTarget[i - 1] = m_internalTarget[i];
m_internalTarget[--m_internalTargetCount] = {};
return BeginTarget(record, nullptr, -2);
}
void CLGMPClipboardTransport::ClearStreamTargets()
{
for (StreamTarget& target : m_streamTarget)
target.Clear();
m_streamTargetHead = 0;
m_streamTargetCount = 0;
}
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)
memcpy(target.data, data, record.length);
++m_streamTargetCount;
return true;
}
bool CLGMPClipboardTransport::ProcessStreamTarget(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
if (!m_ownerClientID ||
m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM)
return true;
if (m_ownerReleasing && !m_releaseClearHelper)
return true;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA &&
record.transfer == m_discardClientToHelper)
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)
{
ReleaseOwner("invalid clipboard stream data", true);
return true;
}
if (!m_ownerReleasing)
RenewLease();
if (staleFileData)
return true;
KVMFRClipboardMessage forwarded = record;
forwarded.generation = m_endpointGeneration;
if (!BeginTarget(forwarded, data, -2))
{
ReleaseOwner("failed to stage clipboard stream data", true);
return true;
}
return true;
}
bool CLGMPClipboardTransport::PumpStreamTarget()
{
while (!m_pendingTarget.valid && m_streamTargetCount)
{
StreamTarget& target = m_streamTarget[m_streamTargetHead];
const KVMFRClipboardMessage record = target.record;
const uint8_t * data = record.length ? target.data : nullptr;
target.Clear();
m_streamTargetHead =
(m_streamTargetHead + 1) % STREAM_TARGET_COUNT;
--m_streamTargetCount;
if (!ProcessStreamTarget(record, data))
return false;
}
return true;
}
bool CLGMPClipboardTransport::DrainStream(bool& received)
{
received = false;
if (!m_ownerClientID ||
m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM ||
!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();
for (Grant& grant : m_grants)
{
grant.generation = 0;
grant.offered = false;
grant.committed = false;
}
}
else if (force)
{
m_releaseClearHelper |= clearHelper;
m_ownerReleaseReason = reason;
}
if (m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM)
{
FinishOwnerRelease();
return;
}
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_ownerTransport = 0;
m_ownerReleasing = false;
m_releaseClearHelper = false;
m_ownerReleaseReason = nullptr;
m_ownerDeadline = 0;
m_ownerDrainDeadline = 0;
m_replayPending = false;
ClearStreamTargets();
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 (m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM)
{
FinishOwnerRelease();
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;
if (m_hostToClientStream && m_clientToHostStream)
{
clipboardStatus.transports = KVMFR_CLIPBOARD_TRANSPORT_STREAM;
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;
clipboardStatus.ownerTransport = m_ownerTransport;
}
memcpy(lgmpHostMemPtr(memory), &clipboardStatus,
sizeof(clipboardStatus));
const uint32_t serial = Seq::Next(m_statusSerial);
unsigned recipients = 0;
status = lgmpHostQueuePostForClients(m_queue,
KVMFR_CLIPBOARD_QUEUE_UDATA(KVMFR_CLIPBOARD_QUEUE_STATUS, serial),
memory, clients, clientCount, &recipients);
if (status == LGMP_ERR_QUEUE_FULL)
return true;
if (status != LGMP_OK)
{
Fail("lgmpHostQueuePostForClients", status);
return false;
}
if (recipients)
{
m_statusSerial = serial;
m_statusDirty = false;
}
return true;
}
bool CLGMPClipboardTransport::PostGrants()
{
if (!m_available || !m_ownerClientID || m_ownerReleasing ||
m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_LEGACY)
return true;
for (unsigned i = 0; i < MEMORY_COUNT; ++i)
{
Grant& grant = m_grants[i];
if (grant.offered || grant.committed ||
lgmpHostQueuePayloadPending(m_queue, m_grantMemory[i]))
continue;
KVMFRClipboardSlotHeader header = {};
header.version = KVMFR_CLIPBOARD_VERSION;
header.type = KVMFR_CLIPBOARD_MESSAGE_GRANT;
header.generation = m_ownerGeneration;
header.size = KVMFR_CLIPBOARD_DATA_BYTES;
header.token = i + 1;
memcpy(lgmpHostMemPtr(m_grantMemory[i]), &header, sizeof(header));
const PostResult result = PostForOwner(
KVMFR_CLIPBOARD_QUEUE_UDATA(
KVMFR_CLIPBOARD_QUEUE_GRANT, i + 1), m_grantMemory[i]);
if (result == PostResult::POSTED)
{
grant.generation = m_ownerGeneration;
grant.offered = true;
}
else if (result == PostResult::BUSY)
return true;
else if (result == PostResult::GONE)
{
ReleaseOwner("subscriber disappeared", true, true);
return true;
}
else
return 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
{
KVMFRClipboardTransportFlags transports = 0;
if (m_hostToClientStream && m_clientToHostStream)
transports = KVMFR_CLIPBOARD_TRANSPORT_STREAM;
return message.version == KVMFR_CLIPBOARD_VERSION &&
message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM &&
message.generation && message.token == m_endpointGeneration &&
kvmfrClipboardTransportValid(message.flags) &&
(message.flags & transports) &&
EmptyControl(message, true, 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);
case KVMFR_CLIPBOARD_MESSAGE_COMMIT:
return m_ownerTransport == KVMFR_CLIPBOARD_TRANSPORT_LEGACY &&
message.token >= 1 && message.token <= MEMORY_COUNT &&
message.clipboardGeneration && message.transfer &&
(kvmfrClipboardRepresentationFormatValid(message.format) ||
message.format == KVMFR_CLIPBOARD_FORMAT_FILES) &&
message.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
!(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)) &&
AddValid(message.offset, message.length);
default:
return false;
}
}
bool CLGMPClipboardTransport::ValidateInboundRecord(
const KVMFRClipboardMessage& message) const
{
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;
m_clientToHelper.Clear();
break;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
m_clientClipboardGeneration = 0;
m_clientFormats = 0;
m_clientToHelper.Clear();
break;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
m_helperToClient.Clear();
m_helperToClient.transfer = message.transfer;
m_helperToClient.clipboardGeneration =
message.clipboardGeneration;
m_helperToClient.format = message.format;
break;
case KVMFR_CLIPBOARD_MESSAGE_DATA:
AdvanceChunk(m_clientToHelper, message);
break;
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
if (m_clientToHelper.transfer == message.transfer)
{
m_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, const uint8_t * data, int grant)
{
if (m_pendingTarget.valid ||
(message.length && !data) ||
message.length > KVMFR_CLIPBOARD_DATA_BYTES)
return false;
m_pendingTarget.valid = true;
m_pendingTarget.acknowledge = grant >= -1;
m_pendingTarget.grant = grant;
m_pendingTarget.record = message;
if (message.length)
memcpy(m_pendingTarget.data, data, message.length);
return RetryTarget();
}
void CLGMPClipboardTransport::FinishTarget(bool accepted)
{
const KVMFRClipboardMessage message = m_pendingTarget.record;
const int grant = m_pendingTarget.grant;
if (accepted)
{
ApplyInbound(message);
if (m_ownerClientID && !m_ownerReleasing)
RenewLease();
}
if (grant >= 0 && static_cast<unsigned>(grant) < MEMORY_COUNT)
m_grants[grant] = {};
m_pendingTarget.Clear();
if (!accepted)
{
ReleaseOwner("Helper delivery failed", false);
m_failed = true;
}
}
bool CLGMPClipboardTransport::RetryTarget()
{
if (!m_pendingTarget.valid)
return true;
const bool acknowledge = m_pendingTarget.acknowledge;
const uint8_t * data = m_pendingTarget.record.length ?
m_pendingTarget.data : nullptr;
const ClipboardChannelResult result = m_target ?
m_target->SendClipboard(m_pendingTarget.record, data) :
ClipboardChannelResult::FAILED;
if (result == ClipboardChannelResult::BUSY)
{
if (m_ownerClientID)
RenewLease();
return true;
}
FinishTarget(result == ClipboardChannelResult::ACCEPTED);
if (acknowledge)
{
const LGMP_STATUS status = lgmpHostAckData(m_queue);
if (status != LGMP_OK)
{
Fail("lgmpHostAckData", status);
return false;
}
}
return true;
}
void CLGMPClipboardTransport::DropPendingTarget()
{
if (!m_pendingTarget.valid)
return;
const bool acknowledge = m_pendingTarget.acknowledge;
const int grant = m_pendingTarget.grant;
if (grant >= 0 && static_cast<unsigned>(grant) < MEMORY_COUNT)
m_grants[grant] = {};
m_pendingTarget.Clear();
if (acknowledge && m_queue)
{
const LGMP_STATUS status = lgmpHostAckData(m_queue);
if (status != LGMP_OK)
Fail("lgmpHostAckData", status);
}
}
bool CLGMPClipboardTransport::ProcessMessage(
uint32_t clientID, const KVMFRClipboardMessage& message)
{
if (message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM)
{
if (!m_available || !ValidateClaim(message))
return true;
if (m_ownerClientID)
{
if (!m_ownerReleasing &&
IsOwner(clientID, message.generation))
RenewLease();
return true;
}
if (message.flags == KVMFR_CLIPBOARD_TRANSPORT_STREAM &&
!BindStreams(clientID))
{
DeInitStreams();
m_failed = true;
if (m_stopEvent)
SetEvent(m_stopEvent);
return true;
}
m_ownerClientID = clientID;
m_ownerGeneration = message.generation;
m_ownerTransport = message.flags;
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 (message.type == KVMFR_CLIPBOARD_MESSAGE_COMMIT)
{
const unsigned grantIndex = message.token - 1;
Grant& grant = m_grants[grantIndex];
if (!grant.offered || grant.committed ||
grant.generation != m_ownerGeneration)
{
ReleaseOwner("invalid clipboard grant", true);
return true;
}
const uint8_t * slot = static_cast<const uint8_t *>(
lgmpHostMemPtr(m_grantMemory[grantIndex]));
KVMFRClipboardMessage dataMessage = {};
memcpy(&dataMessage, slot, sizeof(dataMessage));
const bool fileData = dataMessage.type ==
KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
const bool matchesCommit =
dataMessage.version == KVMFR_CLIPBOARD_VERSION &&
(dataMessage.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
fileData) &&
dataMessage.generation == m_ownerGeneration &&
dataMessage.clipboardGeneration == message.clipboardGeneration &&
dataMessage.transfer == message.transfer &&
dataMessage.offset == message.offset &&
dataMessage.size == message.size &&
dataMessage.format == message.format &&
dataMessage.flags == message.flags &&
dataMessage.length == message.length &&
dataMessage.sequence == message.sequence;
const bool staleFileData = fileData && m_files.IsStaleTerminal(
dataMessage, CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER);
const bool validData = fileData ?
(staleFileData || m_files.Validate(dataMessage,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER, 0, false)) :
(!dataMessage.token &&
kvmfrClipboardTransferFromHelper(dataMessage.transfer) &&
ValidateChunk(m_clientToHelper, dataMessage));
if (!matchesCommit || !validData)
{
ReleaseOwner("invalid clipboard commit", true);
return true;
}
if (staleFileData)
{
grant = {};
RenewLease();
return true;
}
grant.committed = true;
dataMessage.generation = m_endpointGeneration;
RenewLease();
BeginTarget(dataMessage,
slot + sizeof(KVMFRClipboardSlotHeader),
static_cast<int>(grantIndex));
return false;
}
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, nullptr, -1);
return false;
}
bool CLGMPClipboardTransport::DrainMessage()
{
for (unsigned count = 0; count < 64 && !m_pendingTarget.valid; ++count)
{
uint8_t data[LGMP_MSGS_SIZE] = {};
size_t size = 0;
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
m_queue, data, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
return true;
if (status != LGMP_OK)
{
Fail("lgmpHostReadDataWithSource", status);
return false;
}
bool acknowledge = true;
if (size != sizeof(KVMFRClipboardMessage))
{
if (clientID == m_ownerClientID)
ReleaseOwner("invalid clipboard message size", true);
}
else
{
KVMFRClipboardMessage message = {};
memcpy(&message, data, sizeof(message));
acknowledge = ProcessMessage(clientID, message);
}
if (acknowledge)
{
const LGMP_STATUS ackStatus = lgmpHostAckData(m_queue);
if (ackStatus != LGMP_OK)
{
Fail("lgmpHostAckData", ackStatus);
return false;
}
}
}
return true;
}
ClipboardChannelResult CLGMPClipboardTransport::SendControl(
const KVMFRClipboardMessage& record)
{
PLGMPMemory memory = FindAvailable(m_messageMemory);
if (!memory)
return ClipboardChannelResult::BUSY;
KVMFRClipboardMessage message = record;
message.generation = m_ownerGeneration;
memcpy(lgmpHostMemPtr(memory), &message, sizeof(message));
const uint32_t serial = Seq::Next(m_messageSerial);
const PostResult result = PostForOwner(
KVMFR_CLIPBOARD_QUEUE_UDATA(
KVMFR_CLIPBOARD_QUEUE_MESSAGE, serial), memory);
if (result == PostResult::POSTED)
{
m_messageSerial = serial;
ApplyOutbound(record);
return ClipboardChannelResult::ACCEPTED;
}
if (result == PostResult::BUSY)
return ClipboardChannelResult::BUSY;
if (result == PostResult::GONE)
{
ReleaseOwner("subscriber disappeared", true, 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;
if (m_ownerTransport == KVMFR_CLIPBOARD_TRANSPORT_STREAM)
{
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;
}
if (m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_LEGACY)
return ClipboardChannelResult::FAILED;
PLGMPMemory memory = FindAvailable(m_dataMemory);
if (!memory)
return ClipboardChannelResult::BUSY;
uint8_t * slot = static_cast<uint8_t *>(lgmpHostMemPtr(memory));
memcpy(slot, &message, sizeof(message));
if (message.length)
memcpy(slot + sizeof(KVMFRClipboardSlotHeader), data, message.length);
const uint32_t serial = Seq::Next(m_dataSerial);
const PostResult result = PostForOwner(
KVMFR_CLIPBOARD_QUEUE_UDATA(
KVMFR_CLIPBOARD_QUEUE_DATA, serial), memory);
if (result == PostResult::POSTED)
{
m_dataSerial = serial;
ApplyOutbound(record);
return ClipboardChannelResult::ACCEPTED;
}
if (result == PostResult::BUSY)
return ClipboardChannelResult::BUSY;
if (result == PostResult::GONE)
{
ReleaseOwner("subscriber disappeared", true, true);
return ClipboardChannelResult::ACCEPTED;
}
return ClipboardChannelResult::FAILED;
}
ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
CSRWExclusiveLock lock(m_lock);
if (m_failed || !m_target)
return ClipboardChannelResult::FAILED;
// The channel retains a BUSY record for an exact retry. Once the owner
// epoch that caused the backpressure is gone, that record cannot be
// delivered to a replacement owner. Consume it and promptly cancel a
// stale request back toward Helper instead.
if (BlockedOwnerLost(record))
{
QueueStaleRequestCancel(record);
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()
{
bool notifyFailed = false;
for (;;)
{
DWORD timeout = IDLE_POLL_MS;
bool streamReceived = false;
bool notifyReady = false;
IClipboardTarget * readyTarget = nullptr;
{
CSRWExclusiveLock lock(m_lock);
if (m_failed)
{
notifyFailed = true;
break;
}
if (m_ownerClientID && !OwnerSubscribed())
{
DropPendingTarget();
ReleaseOwner("subscriber disappeared", true, 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() || !PumpInternalTarget() ||
!PumpStreamTarget() ||
(m_internalTargetCount == 0 && !DrainMessage()) ||
!DrainStream(streamReceived) ||
!RetryOwnerRelease() ||
!PublishStatus() ||
!ReplayClipboard() || !PostGrants())
{
notifyFailed = true;
break;
}
if (m_outboundBlocked)
{
notifyReady = m_target != nullptr;
readyTarget = m_target;
}
if (m_pendingTarget.valid || m_internalTargetCount ||
m_streamTargetCount || m_ownerClientID)
timeout = ACTIVE_POLL_MS;
if (m_ownerTransport == KVMFR_CLIPBOARD_TRANSPORT_STREAM)
timeout = STREAM_POLL_MS;
if (streamReceived)
timeout = 0;
}
// ClipboardReceiveReady may synchronously retry SendClipboard, which
// acquires m_lock. Never invoke it while holding the transport lock.
if (notifyReady)
readyTarget->ClipboardReceiveReady();
const HANDLE handles[] = { m_stopEvent, m_wakeEvent };
const DWORD wait = WaitForMultipleObjects(
_countof(handles), handles, FALSE, timeout);
if (wait == WAIT_FIRST_OBJECT_VALUE)
break;
if (wait != WAIT_FIRST_OBJECT_VALUE + 1 && wait != WAIT_TIMEOUT)
{
DEBUG_ERROR_HR(GetLastError(),
"LGMP clipboard worker wait failed");
notifyFailed = true;
break;
}
}
IClipboardTarget * target = nullptr;
{
CSRWExclusiveLock lock(m_lock);
DropPendingTarget();
if (m_ownerClientID)
ReleaseOwner("transport stopped", false, true);
target = m_target;
}
if (notifyFailed && target)
target->ClipboardFailed();
}