[idd] clipboard: carry payloads over streams

Create one reliable four-slot lane in each direction and advertise the
stream data plane for clipboard protocol v4.

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

Drain reliable records before releasing an owner. Force stale or
unresponsive bindings after a bounded deadline while preserving the
existing cancellation and clipboard-clear cleanup.
This commit is contained in:
Geoffrey McRae
2026-08-15 13:55:34 +10:00
parent 10b32cb883
commit 72522dadb8
2 changed files with 599 additions and 63 deletions

View File

@@ -31,6 +31,16 @@ namespace
{ {
static constexpr DWORD WAIT_FIRST_OBJECT_VALUE = 0; static constexpr DWORD WAIT_FIRST_OBJECT_VALUE = 0;
static_assert(sizeof(LGMPStreamDescriptor) ==
sizeof(KVMFRStreamDescriptor),
"LGMP and KVMFR stream descriptor sizes differ");
static_assert(offsetof(LGMPStreamDescriptor, offset) ==
offsetof(KVMFRStreamDescriptor, offset),
"LGMP and KVMFR stream descriptor offsets differ");
static_assert(offsetof(LGMPStreamDescriptor, slotSize) ==
offsetof(KVMFRStreamDescriptor, slotSize),
"LGMP and KVMFR stream descriptor geometry differs");
static const LGMPQueueConfig CLIPBOARD_QUEUE_CONFIG = static const LGMPQueueConfig CLIPBOARD_QUEUE_CONFIG =
{ {
LGMP_Q_CLIPBOARD, LGMP_Q_CLIPBOARD,
@@ -39,12 +49,12 @@ namespace
}; };
bool EmptyControl(const KVMFRClipboardMessage& message, bool EmptyControl(const KVMFRClipboardMessage& message,
bool keepToken = false) bool keepToken = false, bool keepFlags = false)
{ {
return !message.clipboardGeneration && !message.transfer && return !message.clipboardGeneration && !message.transfer &&
!message.offset && !message.size && !message.format && !message.offset && !message.size && !message.format &&
!message.flags && (keepToken || !message.token) && !message.length && (keepFlags || !message.flags) && (keepToken || !message.token) &&
!message.sequence; !message.length && !message.sequence;
} }
bool AddValid(uint64_t offset, uint32_t length) bool AddValid(uint64_t offset, uint32_t length)
@@ -95,6 +105,22 @@ namespace
message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL || message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL ||
CLGMPClipboardFiles::IsRecord(message); 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() CLGMPClipboardTransport::~CLGMPClipboardTransport()
@@ -132,20 +158,10 @@ bool CLGMPClipboardTransport::Initialize()
memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardMessage)); memset(lgmpHostMemPtr(memory), 0, sizeof(KVMFRClipboardMessage));
} }
for (PLGMPMemory& memory : m_grantMemory) if (!InitializeStreams())
{ {
status = m_host.Allocate(SLOT_BYTES, &memory); DeInit();
if (status != LGMP_OK) return false;
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; return true;
@@ -160,6 +176,7 @@ fail:
void CLGMPClipboardTransport::DeInit() void CLGMPClipboardTransport::DeInit()
{ {
Stop(); Stop();
DeInitStreams();
for (PLGMPMemory& memory : m_dataMemory) for (PLGMPMemory& memory : m_dataMemory)
lgmpHostMemFree(&memory); lgmpHostMemFree(&memory);
for (PLGMPMemory& memory : m_grantMemory) for (PLGMPMemory& memory : m_grantMemory)
@@ -171,6 +188,139 @@ void CLGMPClipboardTransport::DeInit()
m_queue = nullptr; m_queue = nullptr;
} }
bool CLGMPClipboardTransport::InitializeStreams()
{
if (m_hostToClientStream && m_clientToHostStream)
return true;
const LGMPStreamConfig hostToClient =
{
LGMP_STREAM_HOST_TO_CLIENT,
LGMP_STREAM_RELIABLE_FIFO,
KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
SLOT_BYTES,
};
LGMP_STATUS status = m_host.CreateStream(
hostToClient, &m_hostToClientStream);
if (status != LGMP_OK)
{
DEBUG_ERROR("Failed to create host-to-client clipboard stream: %s",
lgmpStatusString(status));
return false;
}
const LGMPStreamConfig clientToHost =
{
LGMP_STREAM_CLIENT_TO_HOST,
LGMP_STREAM_RELIABLE_FIFO,
KVMFR_CLIPBOARD_STREAM_SLOT_COUNT,
SLOT_BYTES,
};
status = m_host.CreateStream(clientToHost, &m_clientToHostStream);
if (status != LGMP_OK)
{
DEBUG_ERROR("Failed to create client-to-host clipboard stream: %s",
lgmpStatusString(status));
lgmpHostStreamFree(&m_hostToClientStream);
return false;
}
LGMPStreamDescriptor descriptor = {};
lgmpHostStreamGetDescriptor(m_hostToClientStream, &descriptor);
m_hostToClientDescriptor = StreamDescriptorToWire(descriptor);
lgmpHostStreamGetDescriptor(m_clientToHostStream, &descriptor);
m_clientToHostDescriptor = StreamDescriptorToWire(descriptor);
return true;
}
void CLGMPClipboardTransport::DeInitStreams()
{
ForceUnbindStreams();
lgmpHostStreamFree(&m_clientToHostStream);
lgmpHostStreamFree(&m_hostToClientStream);
m_clientToHostDescriptor = {};
m_hostToClientDescriptor = {};
}
bool CLGMPClipboardTransport::BindStreams(uint32_t clientID)
{
if (!m_hostToClientStream || !m_clientToHostStream)
return false;
LGMP_STATUS status = lgmpHostStreamBind(
m_hostToClientStream, clientID, nullptr);
if (status != LGMP_OK)
{
DEBUG_ERROR("Failed to bind host-to-client clipboard stream for "
"client %u: %s", clientID, lgmpStatusString(status));
return false;
}
status = lgmpHostStreamBind(m_clientToHostStream, clientID, nullptr);
if (status == LGMP_OK)
return true;
DEBUG_ERROR("Failed to bind client-to-host clipboard stream for "
"client %u: %s", clientID, lgmpStatusString(status));
const LGMP_STATUS rollback =
lgmpHostStreamUnbind(m_hostToClientStream);
if (rollback != LGMP_OK && rollback != LGMP_ERR_STREAM_UNBOUND &&
rollback != LGMP_ERR_STREAM_BUSY)
DEBUG_ERROR("Failed to roll back host-to-client clipboard stream "
"binding: %s", lgmpStatusString(rollback));
return false;
}
bool CLGMPClipboardTransport::TryUnbindStreams()
{
bool complete = true;
if (m_clientToHostStream)
{
const LGMP_STATUS status =
lgmpHostStreamUnbind(m_clientToHostStream);
if (status == LGMP_ERR_STREAM_BUSY)
complete = false;
else if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
{
Fail("lgmpHostStreamUnbind(client-to-host)", status);
complete = false;
}
}
if (m_hostToClientStream)
{
const LGMP_STATUS status =
lgmpHostStreamUnbind(m_hostToClientStream);
if (status == LGMP_ERR_STREAM_BUSY)
complete = false;
else if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
{
Fail("lgmpHostStreamUnbind(host-to-client)", status);
complete = false;
}
}
return complete;
}
void CLGMPClipboardTransport::ForceUnbindStreams()
{
if (m_clientToHostStream)
{
const LGMP_STATUS status =
lgmpHostStreamForceUnbind(m_clientToHostStream);
if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
DEBUG_ERROR("Failed to force-unbind client-to-host clipboard "
"stream: %s", lgmpStatusString(status));
}
if (m_hostToClientStream)
{
const LGMP_STATUS status =
lgmpHostStreamForceUnbind(m_hostToClientStream);
if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND)
DEBUG_ERROR("Failed to force-unbind host-to-client clipboard "
"stream: %s", lgmpStatusString(status));
}
}
void CLGMPClipboardTransport::Wake() void CLGMPClipboardTransport::Wake()
{ {
if (m_wakeEvent) if (m_wakeEvent)
@@ -312,7 +462,8 @@ bool CLGMPClipboardTransport::BlockedOwnerLost(
{ {
return m_outboundBlocked && OwnerScopedLifecycle(record) && return m_outboundBlocked && OwnerScopedLifecycle(record) &&
memcmp(&m_blockedOutbound, &record, sizeof(record)) == 0 && memcmp(&m_blockedOutbound, &record, sizeof(record)) == 0 &&
(!m_available || record.generation != m_endpointGeneration || (!m_available || m_ownerReleasing ||
record.generation != m_endpointGeneration ||
m_blockedOwnerClientID != m_ownerClientID || m_blockedOwnerClientID != m_ownerClientID ||
m_blockedOwnerGeneration != m_ownerGeneration); m_blockedOwnerGeneration != m_ownerGeneration);
} }
@@ -428,34 +579,231 @@ bool CLGMPClipboardTransport::PumpInternalTarget()
return BeginTarget(record, nullptr, -2); return BeginTarget(record, nullptr, -2);
} }
void CLGMPClipboardTransport::ClearStreamTargets()
{
for (StreamTarget& target : m_streamTarget)
target.Clear();
m_streamTargetHead = 0;
m_streamTargetCount = 0;
}
bool CLGMPClipboardTransport::QueueStreamTarget(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
if (m_streamTargetCount == STREAM_TARGET_COUNT ||
(record.length && !data))
return false;
StreamTarget& target = m_streamTarget[
(m_streamTargetHead + m_streamTargetCount) % STREAM_TARGET_COUNT];
target.record = record;
if (record.length)
memcpy(target.data, data, record.length);
++m_streamTargetCount;
return true;
}
bool CLGMPClipboardTransport::ProcessStreamTarget(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
if (!m_ownerClientID ||
m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM)
return true;
if (m_ownerReleasing && !m_releaseClearHelper)
return true;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA &&
record.transfer == m_discardClientToHelper)
return true;
const bool fileData =
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
const bool staleFileData = fileData && m_files.IsStaleTerminal(
record, CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER);
const bool validData = fileData ?
(staleFileData || m_files.Validate(record,
CLGMPClipboardFiles::Direction::CLIENT_TO_HELPER, 0, false)) :
(!record.token && kvmfrClipboardTransferFromHelper(record.transfer) &&
ValidateChunk(m_clientToHelper, record));
if (!validData)
{
ReleaseOwner("invalid clipboard stream data", true);
return true;
}
if (!m_ownerReleasing)
RenewLease();
if (staleFileData)
return true;
KVMFRClipboardMessage forwarded = record;
forwarded.generation = m_endpointGeneration;
if (!BeginTarget(forwarded, data, -2))
{
ReleaseOwner("failed to stage clipboard stream data", true);
return true;
}
return true;
}
bool CLGMPClipboardTransport::PumpStreamTarget()
{
while (!m_pendingTarget.valid && m_streamTargetCount)
{
StreamTarget& target = m_streamTarget[m_streamTargetHead];
const KVMFRClipboardMessage record = target.record;
const uint8_t * data = record.length ? target.data : nullptr;
target.Clear();
m_streamTargetHead =
(m_streamTargetHead + 1) % STREAM_TARGET_COUNT;
--m_streamTargetCount;
if (!ProcessStreamTarget(record, data))
return false;
}
return true;
}
bool CLGMPClipboardTransport::DrainStream(bool& received)
{
received = false;
if (!m_ownerClientID ||
m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM ||
!m_clientToHostStream)
return true;
for (unsigned drained = 0;
drained < STREAM_DRAIN_MAX &&
m_streamTargetCount < STREAM_TARGET_COUNT; ++drained)
{
LGMPStreamBuffer buffer = {};
LGMP_STATUS status =
lgmpHostStreamReadPeek(m_clientToHostStream, &buffer);
if (status == LGMP_ERR_STREAM_EMPTY)
break;
if (status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
{
if (m_ownerReleasing && status == LGMP_ERR_STREAM_UNBOUND)
break;
ReleaseOwner("clipboard stream binding lost", true);
return true;
}
if (status != LGMP_OK)
{
Fail("lgmpHostStreamReadPeek", status);
return false;
}
received = true;
KVMFRClipboardMessage record = {};
if (buffer.data && buffer.size >= sizeof(record))
memcpy(&record, buffer.data, sizeof(record));
const bool valid = buffer.data &&
buffer.size == sizeof(record) + record.length &&
record.version == KVMFR_CLIPBOARD_VERSION &&
record.generation == m_ownerGeneration &&
(record.type == KVMFR_CLIPBOARD_MESSAGE_DATA ||
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA) &&
record.length <= KVMFR_CLIPBOARD_DATA_BYTES &&
!(record.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)) &&
AddValid(record.offset, record.length);
const bool queued = valid && QueueStreamTarget(record,
static_cast<const uint8_t *>(buffer.data) + sizeof(record));
status = lgmpHostStreamReadRelease(m_clientToHostStream, &buffer);
if (status != LGMP_OK)
{
Fail("lgmpHostStreamReadRelease", status);
return false;
}
if (!queued)
{
ReleaseOwner("invalid clipboard stream record", true);
return true;
}
if (!PumpStreamTarget())
return false;
}
return PumpStreamTarget();
}
void CLGMPClipboardTransport::ReleaseOwner( void CLGMPClipboardTransport::ReleaseOwner(
const char * reason, bool clearHelper) const char * reason, bool clearHelper, bool force)
{ {
if (!m_ownerClientID) if (!m_ownerClientID)
return; return;
const uint32_t clientID = m_ownerClientID; if (!m_ownerReleasing)
const uint32_t generation = m_ownerGeneration; {
m_ownerReleasing = true;
m_releaseClearHelper = clearHelper;
m_ownerReleaseReason = reason;
m_ownerDeadline = 0;
m_ownerDrainDeadline = GetTickCount64() + STREAM_DRAIN_TIMEOUT_MS;
m_replayPending = false;
ClearOutboundBlock();
for (Grant& grant : m_grants)
{
grant.generation = 0;
grant.offered = false;
grant.committed = false;
}
}
else if (force)
{
m_releaseClearHelper |= clearHelper;
m_ownerReleaseReason = reason;
}
if (m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM)
{
FinishOwnerRelease();
return;
}
if (force)
{
ForceUnbindStreams();
FinishOwnerRelease();
return;
}
if (TryUnbindStreams() && !m_pendingTarget.valid &&
!m_streamTargetCount)
FinishOwnerRelease();
}
void CLGMPClipboardTransport::FinishOwnerRelease()
{
if (!m_ownerClientID)
return;
const uint32_t clientID = m_ownerClientID;
const uint32_t generation = m_ownerGeneration;
const char * const reason = m_ownerReleaseReason ?
m_ownerReleaseReason : "unspecified";
const Transfer clientToHelper = m_clientToHelper; const Transfer clientToHelper = m_clientToHelper;
const Transfer helperToClient = m_helperToClient; const Transfer helperToClient = m_helperToClient;
m_ownerClientID = 0; const bool clearHelper = m_releaseClearHelper;
m_ownerGeneration = 0; m_ownerClientID = 0;
m_ownerDeadline = 0; m_ownerGeneration = 0;
m_replayPending = false; m_ownerTransport = 0;
m_ownerReleasing = false;
m_releaseClearHelper = false;
m_ownerReleaseReason = nullptr;
m_ownerDeadline = 0;
m_ownerDrainDeadline = 0;
m_replayPending = false;
ClearStreamTargets();
m_discardClientToHelper = 0;
m_clientToHelper.Clear(); m_clientToHelper.Clear();
m_helperToClient.Clear(); m_helperToClient.Clear();
if (helperToClient.Active()) if (helperToClient.Active())
m_discardHelperToClient = helperToClient.transfer; m_discardHelperToClient = helperToClient.transfer;
for (Grant& grant : m_grants) m_statusDirty = true;
{
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) if (clearHelper)
{ {
QueueFileDisconnect(); QueueFileDisconnect();
@@ -469,6 +817,38 @@ void CLGMPClipboardTransport::ReleaseOwner(
m_clientClipboardGeneration = 0; m_clientClipboardGeneration = 0;
m_clientFormats = 0; m_clientFormats = 0;
} }
DEBUG_INFO("Clipboard owner %u generation %u released (%s)",
clientID, generation, reason);
}
bool CLGMPClipboardTransport::RetryOwnerRelease()
{
if (!m_ownerReleasing)
return true;
if (m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_STREAM)
{
FinishOwnerRelease();
return true;
}
if (GetTickCount64() >= m_ownerDrainDeadline)
{
DEBUG_WARN("Timed out draining clipboard streams for owner %u "
"generation %u; discarding in-flight records",
m_ownerClientID, m_ownerGeneration);
DropPendingTarget();
ClearStreamTargets();
ForceUnbindStreams();
FinishOwnerRelease();
return true;
}
if (TryUnbindStreams() && !m_pendingTarget.valid &&
!m_streamTargetCount)
FinishOwnerRelease();
return !m_failed;
} }
void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard) void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard)
@@ -481,10 +861,12 @@ void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard)
m_files.Reset(); m_files.Reset();
m_clientClipboardGeneration = 0; m_clientClipboardGeneration = 0;
m_clientFormats = 0; m_clientFormats = 0;
m_discardClientToHelper = 0;
m_discardHelperToClient = 0; m_discardHelperToClient = 0;
m_replayPending = false; m_replayPending = false;
ClearOutboundBlock(); ClearOutboundBlock();
m_internalTargetCount = 0; m_internalTargetCount = 0;
ClearStreamTargets();
for (KVMFRClipboardMessage& record : m_internalTarget) for (KVMFRClipboardMessage& record : m_internalTarget)
record = {}; record = {};
if (!keepClipboard) if (!keepClipboard)
@@ -561,10 +943,18 @@ bool CLGMPClipboardTransport::PublishStatus()
return true; return true;
KVMFRClipboardStatus clipboardStatus = {}; KVMFRClipboardStatus clipboardStatus = {};
clipboardStatus.version = KVMFR_CLIPBOARD_VERSION; clipboardStatus.version = KVMFR_CLIPBOARD_VERSION;
clipboardStatus.generation = m_endpointGeneration; clipboardStatus.generation = m_endpointGeneration;
clipboardStatus.lease = static_cast<uint32_t>(OWNER_LEASE_MS); clipboardStatus.lease = static_cast<uint32_t>(OWNER_LEASE_MS);
clipboardStatus.slotBytes = KVMFR_CLIPBOARD_DATA_BYTES; clipboardStatus.slotBytes = KVMFR_CLIPBOARD_DATA_BYTES;
if (m_hostToClientStream && m_clientToHostStream)
{
clipboardStatus.transports = KVMFR_CLIPBOARD_TRANSPORT_STREAM;
clipboardStatus.streamVersion = KVMFR_CLIPBOARD_STREAM_VERSION;
clipboardStatus.streamSlotCount = KVMFR_CLIPBOARD_STREAM_SLOT_COUNT;
clipboardStatus.hostToClient = m_hostToClientDescriptor;
clipboardStatus.clientToHost = m_clientToHostDescriptor;
}
if (m_available) if (m_available)
{ {
clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_AVAILABLE; clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_AVAILABLE;
@@ -575,6 +965,7 @@ bool CLGMPClipboardTransport::PublishStatus()
clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_HAS_OWNER; clipboardStatus.flags |= KVMFR_CLIPBOARD_STATUS_HAS_OWNER;
clipboardStatus.ownerClientID = m_ownerClientID; clipboardStatus.ownerClientID = m_ownerClientID;
clipboardStatus.ownerGeneration = m_ownerGeneration; clipboardStatus.ownerGeneration = m_ownerGeneration;
clipboardStatus.ownerTransport = m_ownerTransport;
} }
memcpy(lgmpHostMemPtr(memory), &clipboardStatus, memcpy(lgmpHostMemPtr(memory), &clipboardStatus,
sizeof(clipboardStatus)); sizeof(clipboardStatus));
@@ -601,7 +992,8 @@ bool CLGMPClipboardTransport::PublishStatus()
bool CLGMPClipboardTransport::PostGrants() bool CLGMPClipboardTransport::PostGrants()
{ {
if (!m_available || !m_ownerClientID) if (!m_available || !m_ownerClientID || m_ownerReleasing ||
m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_LEGACY)
return true; return true;
for (unsigned i = 0; i < MEMORY_COUNT; ++i) for (unsigned i = 0; i < MEMORY_COUNT; ++i)
@@ -631,7 +1023,7 @@ bool CLGMPClipboardTransport::PostGrants()
return true; return true;
else if (result == PostResult::GONE) else if (result == PostResult::GONE)
{ {
ReleaseOwner("subscriber disappeared", true); ReleaseOwner("subscriber disappeared", true, true);
return true; return true;
} }
else else
@@ -642,7 +1034,7 @@ bool CLGMPClipboardTransport::PostGrants()
bool CLGMPClipboardTransport::ReplayClipboard() bool CLGMPClipboardTransport::ReplayClipboard()
{ {
if (!m_replayPending || !m_ownerClientID) if (!m_replayPending || !m_ownerClientID || m_ownerReleasing)
return true; return true;
if (!m_cachedValid) if (!m_cachedValid)
{ {
@@ -667,17 +1059,22 @@ bool CLGMPClipboardTransport::ReplayClipboard()
m_replayPending = false; m_replayPending = false;
} }
else if (result == PostResult::GONE) else if (result == PostResult::GONE)
ReleaseOwner("subscriber disappeared", true); ReleaseOwner("subscriber disappeared", true, true);
return result != PostResult::FAILED; return result != PostResult::FAILED;
} }
bool CLGMPClipboardTransport::ValidateClaim( bool CLGMPClipboardTransport::ValidateClaim(
const KVMFRClipboardMessage& message) const const KVMFRClipboardMessage& message) const
{ {
KVMFRClipboardTransportFlags transports = 0;
if (m_hostToClientStream && m_clientToHostStream)
transports = KVMFR_CLIPBOARD_TRANSPORT_STREAM;
return message.version == KVMFR_CLIPBOARD_VERSION && return message.version == KVMFR_CLIPBOARD_VERSION &&
message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM && message.type == KVMFR_CLIPBOARD_MESSAGE_CLAIM &&
message.generation && message.token == m_endpointGeneration && message.generation && message.token == m_endpointGeneration &&
EmptyControl(message, true); kvmfrClipboardTransportValid(message.flags) &&
(message.flags & transports) &&
EmptyControl(message, true, true);
} }
bool CLGMPClipboardTransport::ValidateOwnedControl( bool CLGMPClipboardTransport::ValidateOwnedControl(
@@ -707,7 +1104,8 @@ bool CLGMPClipboardTransport::ValidateOwnedControl(
return ValidateInboundRecord(message); return ValidateInboundRecord(message);
case KVMFR_CLIPBOARD_MESSAGE_COMMIT: case KVMFR_CLIPBOARD_MESSAGE_COMMIT:
return message.token >= 1 && message.token <= MEMORY_COUNT && return m_ownerTransport == KVMFR_CLIPBOARD_TRANSPORT_LEGACY &&
message.token >= 1 && message.token <= MEMORY_COUNT &&
message.clipboardGeneration && message.transfer && message.clipboardGeneration && message.transfer &&
(kvmfrClipboardRepresentationFormatValid(message.format) || (kvmfrClipboardRepresentationFormatValid(message.format) ||
message.format == KVMFR_CLIPBOARD_FORMAT_FILES) && message.format == KVMFR_CLIPBOARD_FORMAT_FILES) &&
@@ -891,7 +1289,10 @@ void CLGMPClipboardTransport::ApplyInbound(
case KVMFR_CLIPBOARD_MESSAGE_CANCEL: case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
if (m_clientToHelper.transfer == message.transfer) if (m_clientToHelper.transfer == message.transfer)
{
m_discardClientToHelper = message.transfer;
m_clientToHelper.Clear(); m_clientToHelper.Clear();
}
if (m_helperToClient.transfer == message.transfer) if (m_helperToClient.transfer == message.transfer)
m_helperToClient.Clear(); m_helperToClient.Clear();
break; break;
@@ -930,6 +1331,7 @@ void CLGMPClipboardTransport::ApplyOutbound(
case KVMFR_CLIPBOARD_MESSAGE_REQUEST: case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
m_clientToHelper.Clear(); m_clientToHelper.Clear();
m_discardClientToHelper = 0;
m_clientToHelper.transfer = message.transfer; m_clientToHelper.transfer = message.transfer;
m_clientToHelper.clipboardGeneration = m_clientToHelper.clipboardGeneration =
message.clipboardGeneration; message.clipboardGeneration;
@@ -973,7 +1375,8 @@ void CLGMPClipboardTransport::FinishTarget(bool accepted)
if (accepted) if (accepted)
{ {
ApplyInbound(message); ApplyInbound(message);
RenewLease(); if (m_ownerClientID && !m_ownerReleasing)
RenewLease();
} }
if (grant >= 0 && static_cast<unsigned>(grant) < MEMORY_COUNT) if (grant >= 0 && static_cast<unsigned>(grant) < MEMORY_COUNT)
m_grants[grant] = {}; m_grants[grant] = {};
@@ -1044,13 +1447,25 @@ bool CLGMPClipboardTransport::ProcessMessage(
return true; return true;
if (m_ownerClientID) if (m_ownerClientID)
{ {
if (IsOwner(clientID, message.generation)) if (!m_ownerReleasing &&
IsOwner(clientID, message.generation))
RenewLease(); RenewLease();
return true; return true;
} }
if (message.flags == KVMFR_CLIPBOARD_TRANSPORT_STREAM &&
!BindStreams(clientID))
{
DeInitStreams();
m_failed = true;
if (m_stopEvent)
SetEvent(m_stopEvent);
return true;
}
m_ownerClientID = clientID; m_ownerClientID = clientID;
m_ownerGeneration = message.generation; m_ownerGeneration = message.generation;
m_ownerTransport = message.flags;
RenewLease(); RenewLease();
m_statusDirty = true; m_statusDirty = true;
m_replayPending = m_cachedValid; m_replayPending = m_cachedValid;
@@ -1061,6 +1476,8 @@ bool CLGMPClipboardTransport::ProcessMessage(
if (!IsOwner(clientID, message.generation)) if (!IsOwner(clientID, message.generation))
return true; return true;
if (m_ownerReleasing)
return true;
if (CLGMPClipboardFiles::IsRecord(message) && if (CLGMPClipboardFiles::IsRecord(message) &&
CLGMPClipboardFiles::DirectionValid(message, CLGMPClipboardFiles::DirectionValid(message,
@@ -1255,7 +1672,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendControl(
return ClipboardChannelResult::BUSY; return ClipboardChannelResult::BUSY;
if (result == PostResult::GONE) if (result == PostResult::GONE)
{ {
ReleaseOwner("subscriber disappeared", true); ReleaseOwner("subscriber disappeared", true, true);
return record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER || return record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR ? record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR ?
ClipboardChannelResult::ACCEPTED : ClipboardChannelResult::BUSY; ClipboardChannelResult::ACCEPTED : ClipboardChannelResult::BUSY;
@@ -1273,12 +1690,75 @@ ClipboardChannelResult CLGMPClipboardTransport::SendData(
if (!valid) if (!valid)
return ClipboardChannelResult::FAILED; return ClipboardChannelResult::FAILED;
KVMFRClipboardMessage message = record;
message.generation = m_ownerGeneration;
if (m_ownerTransport == KVMFR_CLIPBOARD_TRANSPORT_STREAM)
{
LGMPStreamBuffer buffer = {};
LGMP_STATUS status =
lgmpHostStreamWriteAcquire(m_hostToClientStream, &buffer);
if (status == LGMP_ERR_STREAM_FULL)
return ClipboardChannelResult::BUSY;
if (status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
{
ReleaseOwner("clipboard stream binding lost", true);
return ClipboardChannelResult::ACCEPTED;
}
if (status != LGMP_OK)
{
Fail("lgmpHostStreamWriteAcquire", status);
return ClipboardChannelResult::FAILED;
}
const uint32_t bytes = sizeof(message) + message.length;
if (!buffer.data || buffer.capacity < bytes)
{
const LGMP_STATUS cancel =
lgmpHostStreamWriteCancel(m_hostToClientStream, &buffer);
if (cancel != LGMP_OK)
DEBUG_ERROR("Failed to cancel an undersized host-to-client "
"clipboard stream reservation: %s", lgmpStatusString(cancel));
DEBUG_ERROR("Host-to-client clipboard stream returned an "
"undersized slot: capacity=%u required=%u",
buffer.capacity, bytes);
m_failed = true;
if (m_stopEvent)
SetEvent(m_stopEvent);
return ClipboardChannelResult::FAILED;
}
memcpy(buffer.data, &message, sizeof(message));
if (message.length)
memcpy(static_cast<uint8_t *>(buffer.data) + sizeof(message), data,
message.length);
status = lgmpHostStreamWriteCommit(
m_hostToClientStream, &buffer, bytes);
if (status == LGMP_OK)
{
ApplyOutbound(record);
return ClipboardChannelResult::ACCEPTED;
}
DEBUG_ERROR("Failed to commit host-to-client clipboard stream data: %s",
lgmpStatusString(status));
if (status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
{
ReleaseOwner("clipboard stream binding lost", true);
return ClipboardChannelResult::ACCEPTED;
}
Fail("lgmpHostStreamWriteCommit", status);
return ClipboardChannelResult::FAILED;
}
if (m_ownerTransport != KVMFR_CLIPBOARD_TRANSPORT_LEGACY)
return ClipboardChannelResult::FAILED;
PLGMPMemory memory = FindAvailable(m_dataMemory); PLGMPMemory memory = FindAvailable(m_dataMemory);
if (!memory) if (!memory)
return ClipboardChannelResult::BUSY; return ClipboardChannelResult::BUSY;
KVMFRClipboardMessage message = record;
message.generation = m_ownerGeneration;
uint8_t * slot = static_cast<uint8_t *>(lgmpHostMemPtr(memory)); uint8_t * slot = static_cast<uint8_t *>(lgmpHostMemPtr(memory));
memcpy(slot, &message, sizeof(message)); memcpy(slot, &message, sizeof(message));
if (message.length) if (message.length)
@@ -1298,7 +1778,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendData(
return ClipboardChannelResult::BUSY; return ClipboardChannelResult::BUSY;
if (result == PostResult::GONE) if (result == PostResult::GONE)
{ {
ReleaseOwner("subscriber disappeared", true); ReleaseOwner("subscriber disappeared", true, true);
return ClipboardChannelResult::ACCEPTED; return ClipboardChannelResult::ACCEPTED;
} }
return ClipboardChannelResult::FAILED; return ClipboardChannelResult::FAILED;
@@ -1385,7 +1865,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
m_helperFormats = record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ? m_helperFormats = record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ?
record.token : 0; record.token : 0;
m_statusDirty = true; m_statusDirty = true;
if (!m_ownerClientID) if (!m_ownerClientID || m_ownerReleasing)
{ {
ClearOutboundBlock(); ClearOutboundBlock();
Wake(); Wake();
@@ -1393,7 +1873,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard(
} }
} }
if (!m_ownerClientID) if (!m_ownerClientID || m_ownerReleasing)
{ {
if (OwnerScopedLifecycle(record)) if (OwnerScopedLifecycle(record))
{ {
@@ -1462,6 +1942,7 @@ void CLGMPClipboardTransport::ClipboardState(
if (generation != m_endpointGeneration || available != m_available) if (generation != m_endpointGeneration || available != m_available)
{ {
DropPendingTarget(); DropPendingTarget();
ClearStreamTargets();
m_internalTargetCount = 0; m_internalTargetCount = 0;
for (KVMFRClipboardMessage& record : m_internalTarget) for (KVMFRClipboardMessage& record : m_internalTarget)
record = {}; record = {};
@@ -1472,6 +1953,7 @@ void CLGMPClipboardTransport::ClipboardState(
m_files.Reset(); m_files.Reset();
m_clientClipboardGeneration = 0; m_clientClipboardGeneration = 0;
m_clientFormats = 0; m_clientFormats = 0;
m_discardClientToHelper = 0;
m_discardHelperToClient = 0; m_discardHelperToClient = 0;
m_cachedClipboard = {}; m_cachedClipboard = {};
m_cachedValid = false; m_cachedValid = false;
@@ -1491,6 +1973,7 @@ void CLGMPClipboardTransport::ClipboardReset(
if (!generation) if (!generation)
return; return;
DropPendingTarget(); DropPendingTarget();
ClearStreamTargets();
m_internalTargetCount = 0; m_internalTargetCount = 0;
for (KVMFRClipboardMessage& record : m_internalTarget) for (KVMFRClipboardMessage& record : m_internalTarget)
record = {}; record = {};
@@ -1501,6 +1984,7 @@ void CLGMPClipboardTransport::ClipboardReset(
m_files.Reset(); m_files.Reset();
m_clientClipboardGeneration = 0; m_clientClipboardGeneration = 0;
m_clientFormats = 0; m_clientFormats = 0;
m_discardClientToHelper = 0;
m_discardHelperToClient = 0; m_discardHelperToClient = 0;
m_cachedClipboard = {}; m_cachedClipboard = {};
m_cachedValid = false; m_cachedValid = false;
@@ -1522,6 +2006,7 @@ void CLGMPClipboardTransport::Thread()
for (;;) for (;;)
{ {
DWORD timeout = IDLE_POLL_MS; DWORD timeout = IDLE_POLL_MS;
bool streamReceived = false;
bool notifyReady = false; bool notifyReady = false;
IClipboardTarget * readyTarget = nullptr; IClipboardTarget * readyTarget = nullptr;
{ {
@@ -1535,15 +2020,19 @@ void CLGMPClipboardTransport::Thread()
if (m_ownerClientID && !OwnerSubscribed()) if (m_ownerClientID && !OwnerSubscribed())
{ {
DropPendingTarget(); DropPendingTarget();
ReleaseOwner("subscriber disappeared", true); ReleaseOwner("subscriber disappeared", true, true);
} }
else if (m_ownerClientID && GetTickCount64() >= m_ownerDeadline) else if (m_ownerClientID && !m_ownerReleasing &&
GetTickCount64() >= m_ownerDeadline)
ReleaseOwner("lease expired", true); ReleaseOwner("lease expired", true);
// A restarted client can reuse dataset and transfer IDs. Deliver every // A restarted client can reuse dataset and transfer IDs. Deliver every
// old-owner cleanup record before admitting messages from a new owner. // old-owner cleanup record before admitting messages from a new owner.
if (!RetryTarget() || !PumpInternalTarget() || if (!RetryTarget() || !PumpInternalTarget() ||
!PumpStreamTarget() ||
(m_internalTargetCount == 0 && !DrainMessage()) || (m_internalTargetCount == 0 && !DrainMessage()) ||
!DrainStream(streamReceived) ||
!RetryOwnerRelease() ||
!PublishStatus() || !PublishStatus() ||
!ReplayClipboard() || !PostGrants()) !ReplayClipboard() || !PostGrants())
{ {
@@ -1556,8 +2045,13 @@ void CLGMPClipboardTransport::Thread()
notifyReady = m_target != nullptr; notifyReady = m_target != nullptr;
readyTarget = m_target; readyTarget = m_target;
} }
if (m_pendingTarget.valid || m_internalTargetCount || m_ownerClientID) if (m_pendingTarget.valid || m_internalTargetCount ||
m_streamTargetCount || m_ownerClientID)
timeout = ACTIVE_POLL_MS; timeout = ACTIVE_POLL_MS;
if (m_ownerTransport == KVMFR_CLIPBOARD_TRANSPORT_STREAM)
timeout = STREAM_POLL_MS;
if (streamReceived)
timeout = 0;
} }
// ClipboardReceiveReady may synchronously retry SendClipboard, which // ClipboardReceiveReady may synchronously retry SendClipboard, which
@@ -1584,7 +2078,7 @@ void CLGMPClipboardTransport::Thread()
CSRWExclusiveLock lock(m_lock); CSRWExclusiveLock lock(m_lock);
DropPendingTarget(); DropPendingTarget();
if (m_ownerClientID) if (m_ownerClientID)
ReleaseOwner("transport stopped", false); ReleaseOwner("transport stopped", false, true);
target = m_target; target = m_target;
} }
if (notifyFailed && target) if (notifyFailed && target)

View File

@@ -33,6 +33,7 @@
extern "C" { extern "C" {
#include "lgmp/host.h" #include "lgmp/host.h"
#include "lgmp/stream.h"
} }
class CLGMPHost; class CLGMPHost;
@@ -45,10 +46,15 @@ private:
CLGMPClipboardFiles::MAX_ACQUISITIONS + CLGMPClipboardFiles::MAX_ACQUISITIONS +
CLGMPClipboardFiles::MAX_REQUESTS + 3; CLGMPClipboardFiles::MAX_REQUESTS + 3;
static constexpr ULONGLONG OWNER_LEASE_MS = 1000; static constexpr ULONGLONG OWNER_LEASE_MS = 1000;
static constexpr ULONGLONG STREAM_DRAIN_TIMEOUT_MS = 1000;
static constexpr DWORD ACTIVE_POLL_MS = 5; static constexpr DWORD ACTIVE_POLL_MS = 5;
static constexpr DWORD STREAM_POLL_MS = 1;
static constexpr DWORD IDLE_POLL_MS = 50; static constexpr DWORD IDLE_POLL_MS = 50;
static constexpr unsigned STREAM_TARGET_COUNT =
KVMFR_CLIPBOARD_STREAM_SLOT_COUNT;
static constexpr unsigned STREAM_DRAIN_MAX = 64;
static constexpr uint32_t SLOT_BYTES = static constexpr uint32_t SLOT_BYTES =
sizeof(KVMFRClipboardSlotHeader) + KVMFR_CLIPBOARD_DATA_BYTES; KVMFR_CLIPBOARD_STREAM_RECORD_BYTES;
enum class PostResult enum class PostResult
{ {
@@ -96,9 +102,19 @@ private:
} }
}; };
struct StreamTarget
{
KVMFRClipboardMessage record = {};
uint8_t data[KVMFR_CLIPBOARD_DATA_BYTES] = {};
void Clear() { record = {}; }
};
CLGMPHost& m_host; CLGMPHost& m_host;
PLGMPHostQueue m_queue = nullptr; PLGMPHostQueue m_queue = nullptr;
PLGMPHostStream m_hostToClientStream = nullptr;
PLGMPHostStream m_clientToHostStream = nullptr;
PLGMPMemory m_statusMemory [MEMORY_COUNT] = {}; PLGMPMemory m_statusMemory [MEMORY_COUNT] = {};
PLGMPMemory m_messageMemory[MEMORY_COUNT] = {}; PLGMPMemory m_messageMemory[MEMORY_COUNT] = {};
PLGMPMemory m_grantMemory [MEMORY_COUNT] = {}; PLGMPMemory m_grantMemory [MEMORY_COUNT] = {};
@@ -111,15 +127,18 @@ private:
HANDLE m_wakeEvent = nullptr; HANDLE m_wakeEvent = nullptr;
HANDLE m_thread = nullptr; HANDLE m_thread = nullptr;
bool m_available = false; bool m_available = false;
bool m_statusDirty = true; bool m_statusDirty = true;
bool m_cachedValid = false; bool m_cachedValid = false;
bool m_replayPending = false; bool m_replayPending = false;
bool m_outboundBlocked = false; bool m_outboundBlocked = false;
bool m_failed = false; bool m_ownerReleasing = false;
bool m_releaseClearHelper = false;
bool m_failed = false;
uint32_t m_endpointGeneration = 0; uint32_t m_endpointGeneration = 0;
uint32_t m_ownerClientID = 0; uint32_t m_ownerClientID = 0;
uint32_t m_ownerGeneration = 0; uint32_t m_ownerGeneration = 0;
KVMFRClipboardTransportFlags m_ownerTransport = 0;
uint32_t m_statusSerial = 0; uint32_t m_statusSerial = 0;
uint32_t m_messageSerial = 0; uint32_t m_messageSerial = 0;
uint32_t m_dataSerial = 0; uint32_t m_dataSerial = 0;
@@ -128,20 +147,33 @@ private:
uint32_t m_blockedOwnerClientID = 0; uint32_t m_blockedOwnerClientID = 0;
uint32_t m_blockedOwnerGeneration = 0; uint32_t m_blockedOwnerGeneration = 0;
uint64_t m_ownerDeadline = 0; uint64_t m_ownerDeadline = 0;
uint64_t m_ownerDrainDeadline = 0;
uint64_t m_clientClipboardGeneration = 0; uint64_t m_clientClipboardGeneration = 0;
uint64_t m_discardClientToHelper = 0;
uint64_t m_discardHelperToClient = 0; uint64_t m_discardHelperToClient = 0;
const char * m_ownerReleaseReason = nullptr;
KVMFRClipboardMessage m_cachedClipboard = {}; KVMFRClipboardMessage m_cachedClipboard = {};
KVMFRClipboardMessage m_blockedOutbound = {}; KVMFRClipboardMessage m_blockedOutbound = {};
KVMFRStreamDescriptor m_hostToClientDescriptor = {};
KVMFRStreamDescriptor m_clientToHostDescriptor = {};
Transfer m_clientToHelper; Transfer m_clientToHelper;
Transfer m_helperToClient; Transfer m_helperToClient;
Grant m_grants[MEMORY_COUNT]; Grant m_grants[MEMORY_COUNT];
PendingTarget m_pendingTarget; PendingTarget m_pendingTarget;
StreamTarget m_streamTarget[STREAM_TARGET_COUNT];
unsigned m_streamTargetHead = 0;
unsigned m_streamTargetCount = 0;
KVMFRClipboardMessage m_internalTarget[INTERNAL_TARGET_COUNT] = {}; KVMFRClipboardMessage m_internalTarget[INTERNAL_TARGET_COUNT] = {};
unsigned m_internalTargetCount = 0; unsigned m_internalTargetCount = 0;
CLGMPClipboardFiles m_files; CLGMPClipboardFiles m_files;
bool Initialize(); bool Initialize();
void DeInit(); void DeInit();
bool InitializeStreams();
void DeInitStreams();
bool BindStreams(uint32_t clientID);
bool TryUnbindStreams();
void ForceUnbindStreams();
static DWORD CALLBACK ThreadProc(void * context); static DWORD CALLBACK ThreadProc(void * context);
void Thread(); void Thread();
@@ -154,7 +186,10 @@ private:
uint32_t ownerClientID, uint32_t ownerGeneration); uint32_t ownerClientID, uint32_t ownerGeneration);
void ClearOutboundBlock(); void ClearOutboundBlock();
bool BlockedOwnerLost(const KVMFRClipboardMessage& record) const; bool BlockedOwnerLost(const KVMFRClipboardMessage& record) const;
void ReleaseOwner(const char * reason, bool clearHelper); void ReleaseOwner(const char * reason, bool clearHelper,
bool force = false);
void FinishOwnerRelease();
bool RetryOwnerRelease();
void ResetProtocol(bool keepClipboard); void ResetProtocol(bool keepClipboard);
void QueueHelperClear(); void QueueHelperClear();
void QueueTransferCancel(const Transfer& transfer); void QueueTransferCancel(const Transfer& transfer);
@@ -164,6 +199,13 @@ private:
void QueueFileDisconnect(); void QueueFileDisconnect();
bool QueueInternalTarget(const KVMFRClipboardMessage& record); bool QueueInternalTarget(const KVMFRClipboardMessage& record);
bool PumpInternalTarget(); bool PumpInternalTarget();
void ClearStreamTargets();
bool QueueStreamTarget(const KVMFRClipboardMessage& record,
const uint8_t * data);
bool PumpStreamTarget();
bool DrainStream(bool& received);
bool ProcessStreamTarget(const KVMFRClipboardMessage& record,
const uint8_t * data);
PLGMPMemory FindAvailable(PLGMPMemory (&memory)[MEMORY_COUNT]) const; PLGMPMemory FindAvailable(PLGMPMemory (&memory)[MEMORY_COUNT]) const;
PostResult PostForOwner(uint64_t udata, PLGMPMemory memory); PostResult PostForOwner(uint64_t udata, PLGMPMemory memory);