mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 15:11:31 +00:00
1593 lines
46 KiB
C++
1593 lines
46 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 const LGMPQueueConfig CLIPBOARD_QUEUE_CONFIG =
|
|
{
|
|
LGMP_Q_CLIPBOARD,
|
|
LGMP_Q_CLIPBOARD_LEN,
|
|
1000,
|
|
};
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
for (PLGMPMemory& memory : m_grantMemory)
|
|
{
|
|
status = m_host.Allocate(SLOT_BYTES, &memory);
|
|
if (status != LGMP_OK)
|
|
goto fail;
|
|
memset(lgmpHostMemPtr(memory), 0, SLOT_BYTES);
|
|
}
|
|
|
|
for (PLGMPMemory& memory : m_dataMemory)
|
|
{
|
|
status = m_host.Allocate(SLOT_BYTES, &memory);
|
|
if (status != LGMP_OK)
|
|
goto fail;
|
|
memset(lgmpHostMemPtr(memory), 0, SLOT_BYTES);
|
|
}
|
|
|
|
return true;
|
|
|
|
fail:
|
|
DEBUG_ERROR("lgmpHostMemAlloc Failed (Clipboard): %s",
|
|
lgmpStatusString(status));
|
|
DeInit();
|
|
return false;
|
|
}
|
|
|
|
void CLGMPClipboardTransport::DeInit()
|
|
{
|
|
Stop();
|
|
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;
|
|
}
|
|
|
|
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 = ⌖
|
|
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 || 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::ReleaseOwner(
|
|
const char * reason, bool clearHelper)
|
|
{
|
|
if (!m_ownerClientID)
|
|
return;
|
|
|
|
const uint32_t clientID = m_ownerClientID;
|
|
const uint32_t generation = m_ownerGeneration;
|
|
const Transfer clientToHelper = m_clientToHelper;
|
|
const Transfer helperToClient = m_helperToClient;
|
|
m_ownerClientID = 0;
|
|
m_ownerGeneration = 0;
|
|
m_ownerDeadline = 0;
|
|
m_replayPending = false;
|
|
m_clientToHelper.Clear();
|
|
m_helperToClient.Clear();
|
|
if (helperToClient.Active())
|
|
m_discardHelperToClient = helperToClient.transfer;
|
|
for (Grant& grant : m_grants)
|
|
{
|
|
grant.generation = 0;
|
|
grant.offered = false;
|
|
grant.committed = false;
|
|
}
|
|
m_statusDirty = true;
|
|
|
|
DEBUG_INFO("Clipboard owner %u generation %u released (%s)",
|
|
clientID, generation, reason);
|
|
if (clearHelper)
|
|
{
|
|
QueueFileDisconnect();
|
|
QueueTransferCancel(clientToHelper);
|
|
QueueTransferCancel(helperToClient);
|
|
QueueHelperClear();
|
|
}
|
|
else
|
|
{
|
|
m_files.Reset();
|
|
m_clientClipboardGeneration = 0;
|
|
m_clientFormats = 0;
|
|
}
|
|
}
|
|
|
|
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_discardHelperToClient = 0;
|
|
m_replayPending = false;
|
|
ClearOutboundBlock();
|
|
m_internalTargetCount = 0;
|
|
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_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::PostGrants()
|
|
{
|
|
if (!m_available || !m_ownerClientID)
|
|
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);
|
|
return true;
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CLGMPClipboardTransport::ReplayClipboard()
|
|
{
|
|
if (!m_replayPending || !m_ownerClientID)
|
|
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);
|
|
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 &&
|
|
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);
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_COMMIT:
|
|
return 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_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_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);
|
|
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 (IsOwner(clientID, message.generation))
|
|
RenewLease();
|
|
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 (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);
|
|
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;
|
|
|
|
PLGMPMemory memory = FindAvailable(m_dataMemory);
|
|
if (!memory)
|
|
return ClipboardChannelResult::BUSY;
|
|
|
|
KVMFRClipboardMessage message = record;
|
|
message.generation = m_ownerGeneration;
|
|
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);
|
|
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)
|
|
{
|
|
ClearOutboundBlock();
|
|
Wake();
|
|
return ClipboardChannelResult::ACCEPTED;
|
|
}
|
|
}
|
|
|
|
if (!m_ownerClientID)
|
|
{
|
|
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();
|
|
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_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();
|
|
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_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 notifyReady = false;
|
|
IClipboardTarget * readyTarget = nullptr;
|
|
{
|
|
CSRWExclusiveLock lock(m_lock);
|
|
if (m_failed)
|
|
{
|
|
notifyFailed = true;
|
|
break;
|
|
}
|
|
|
|
if (m_ownerClientID && !OwnerSubscribed())
|
|
{
|
|
DropPendingTarget();
|
|
ReleaseOwner("subscriber disappeared", true);
|
|
}
|
|
else if (m_ownerClientID && 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() ||
|
|
(m_internalTargetCount == 0 && !DrainMessage()) ||
|
|
!PublishStatus() ||
|
|
!ReplayClipboard() || !PostGrants())
|
|
{
|
|
notifyFailed = true;
|
|
break;
|
|
}
|
|
|
|
if (m_outboundBlocked)
|
|
{
|
|
notifyReady = m_target != nullptr;
|
|
readyTarget = m_target;
|
|
}
|
|
if (m_pendingTarget.valid || m_internalTargetCount || m_ownerClientID)
|
|
timeout = ACTIVE_POLL_MS;
|
|
}
|
|
|
|
// 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);
|
|
target = m_target;
|
|
}
|
|
if (notifyFailed && target)
|
|
target->ClipboardFailed();
|
|
}
|