mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 15:11:31 +00:00
Add a reusable copy path for moving data from write-combined mappings into normal cacheable memory. Dispatch to MOVNTDQA-based SSE4.1 or AVX2 loads when the processor and OS support them, with a fenced fallback. Use the optimized path when staging inbound clipboard stream records from the IVSHMEM mapping.
1996 lines
56 KiB
C++
1996 lines
56 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 = ⌖
|
|
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_internalTargetCount)
|
|
return BeginTarget(record, nullptr, 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_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, false);
|
|
}
|
|
|
|
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)
|
|
WCCopy::Copy(target.data, data, record.length);
|
|
++m_streamTargetCount;
|
|
return true;
|
|
}
|
|
|
|
bool CLGMPClipboardTransport::ProcessStreamTarget(
|
|
const KVMFRClipboardMessage& record, const uint8_t * data)
|
|
{
|
|
if (!m_ownerClientID)
|
|
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, false))
|
|
{
|
|
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_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;
|
|
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 (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, const uint8_t * data,
|
|
bool acknowledge)
|
|
{
|
|
if (m_pendingTarget.valid ||
|
|
(message.length && !data) ||
|
|
message.length > KVMFR_CLIPBOARD_DATA_BYTES)
|
|
return false;
|
|
|
|
m_pendingTarget.valid = true;
|
|
m_pendingTarget.acknowledge = acknowledge;
|
|
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;
|
|
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 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;
|
|
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, nullptr, 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();
|
|
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())
|
|
{
|
|
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();
|
|
if (m_ownerClientID)
|
|
ReleaseOwner("transport stopped", false, true);
|
|
target = m_target;
|
|
}
|
|
if (notifyFailed && target)
|
|
target->ClipboardFailed();
|
|
}
|