[idd] clipboard: arbitrate ownership by active client

This commit is contained in:
Geoffrey McRae
2026-10-01 18:36:51 +10:00
parent 450d20c8d8
commit cbb7835158
6 changed files with 918 additions and 355 deletions

View File

@@ -60,6 +60,8 @@ namespace
CClipboardHub::CClipboardHub(CClipboardChannel& channel) :
m_channel(channel)
{
for (Source& source : m_sources)
source.owner = this;
m_channel.SetHandler(this);
}
@@ -68,27 +70,107 @@ CClipboardHub::~CClipboardHub()
Stop();
}
void CClipboardHub::AdvanceGenerationNL()
ClipboardClaimResult CClipboardHub::Source::Claim(
const ClipboardSourceId& source, KVMFRClipboardMessage& cached)
{
Seq::Inc(m_generation);
return owner->Claim(*this, source, cached);
}
bool CClipboardHub::MarkFailed(IClipboardSource& source)
void CClipboardHub::Source::Release(const ClipboardSourceId& source)
{
owner->Release(*this, source);
}
ClipboardChannelResult CClipboardHub::Source::SendClipboard(
const KVMFRClipboardMessage& record, const uint8_t * data)
{
return owner->SendClipboard(*this, record, data);
}
ClipboardChannelResult CClipboardHub::Source::SendClipboardBatch(
const ClipboardChannelWrite * records, size_t count, size_t& accepted)
{
return owner->SendClipboardBatch(*this, records, count, accepted);
}
void CClipboardHub::Source::ClipboardReceiveReady()
{
owner->ClipboardReceiveReady(*this);
}
void CClipboardHub::Source::ClipboardFailed()
{
owner->ClipboardFailed(*this);
}
uint32_t CClipboardHub::NextGenerationNL()
{
Seq::Inc(m_generation);
return m_generation;
}
bool CClipboardHub::BindingPresentNL(const Source& source) const
{
return source.active && source.endpoint && source.backend && source.epoch;
}
bool CClipboardHub::BindingValidNL(const Source& source) const
{
return BindingPresentNL(source) && source.running && !source.reserved &&
!source.failed && source.generation;
}
bool CClipboardHub::OwnerIsNL(
const Source& source, const ClipboardSourceId& id) const
{
return m_owner == &source && m_ownerId.client == id.client &&
m_ownerId.generation == id.generation;
}
bool CClipboardHub::MarkFailed(Source& source)
{
CSRWExclusiveLock lock(m_lock);
if (m_source != &source || (!m_active && !m_reserved))
if (!BindingPresentNL(source) && !source.reserved)
return false;
const bool changed = !m_failed;
m_failed = true;
m_failurePending = true;
const bool changed = !source.failed;
source.failed = true;
source.failurePending = true;
return changed;
}
void CClipboardHub::CachePublicationNL(
const KVMFRClipboardMessage& record, Source * source)
{
if (record.type != KVMFR_CLIPBOARD_MESSAGE_OFFER &&
record.type != KVMFR_CLIPBOARD_MESSAGE_CLEAR)
return;
m_cachedClipboard = record;
m_cachedSource = source;
m_cachedValid = true;
}
void CClipboardHub::InvalidateSourceCacheNL(const Source& source)
{
if (m_cachedSource != &source)
return;
if (m_cachedValid &&
m_cachedClipboard.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR)
{
m_cachedSource = nullptr;
return;
}
m_cachedClipboard = {};
m_cachedSource = nullptr;
m_cachedValid = false;
}
ClipboardChannelResult CClipboardHub::HoldOrDiscard(
const KVMFRClipboardMessage& record)
{
// Preserve the pending clipboard publication until a source is rebound.
// Preserve the pending clipboard publication until a client claims it.
// Transfer-scoped records belong to the source which admitted them and
// must never cross a transport generation.
if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
@@ -139,47 +221,58 @@ bool CClipboardHub::Bind(
if (!backend || !epoch)
return false;
bool available;
uint32_t generation;
uint64_t channelEpoch;
Source * selected = nullptr;
bool available = false;
uint32_t generation = 0;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped || m_source || m_active || m_reserved)
if (m_stopped)
return false;
AdvanceGenerationNL();
m_source = &source;
m_backend = backend;
m_epoch = epoch;
m_active = true;
m_reserved = true;
m_failed = false;
m_failurePending = false;
available = m_available;
generation = m_generation;
channelEpoch = m_channelEpoch;
for (Source& slot : m_sources)
if ((slot.active || slot.failed || slot.reserved) &&
slot.backend == backend && slot.epoch == epoch)
return false;
for (Source& slot : m_sources)
if (!slot.active && !slot.failed && !slot.reserved)
{
slot.backend = backend;
slot.epoch = epoch;
slot.generation = NextGenerationNL();
slot.endpoint = &source;
slot.running = false;
slot.active = true;
slot.reserved = true;
slot.failurePending = false;
selected = &slot;
available = m_available && !m_resetting;
generation = slot.generation;
break;
}
}
if (!selected)
return false;
CSRWExclusiveLock callbackLock(m_callbackLock);
const bool started = source.Start(*this);
bool attached = false;
const bool started = source.Start(*selected);
bool attached = false;
{
CSRWExclusiveLock lock(m_lock);
if (started && m_source == &source && m_backend == backend &&
m_epoch == epoch && m_active && m_reserved && !m_failed &&
if (started && selected->endpoint == &source &&
selected->backend == backend && selected->epoch == epoch &&
selected->active && selected->reserved && !selected->failed &&
!m_stopped)
{
m_running = true;
m_reserved = false;
available = m_available;
generation = m_generation;
channelEpoch = m_channelEpoch;
attached = true;
selected->running = true;
selected->reserved = false;
available = m_available && !m_resetting;
generation = selected->generation;
attached = true;
}
else
{
m_active = false;
m_running = false;
selected->active = false;
selected->running = false;
}
}
@@ -187,99 +280,133 @@ bool CClipboardHub::Bind(
{
source.Stop();
CSRWExclusiveLock lock(m_lock);
if (m_source == &source && m_backend == backend && m_epoch == epoch)
if (selected->endpoint == &source && selected->backend == backend &&
selected->epoch == epoch)
{
m_source = nullptr;
m_backend = 0;
m_epoch = 0;
m_active = false;
m_running = false;
m_reserved = false;
m_failed = false;
m_failurePending = false;
selected->backend = 0;
selected->epoch = 0;
selected->generation = 0;
selected->endpoint = nullptr;
selected->running = false;
selected->active = false;
selected->reserved = false;
selected->failed = false;
selected->failurePending = false;
}
return false;
}
source.ClipboardState(available, generation);
callbackLock.Unlock();
if (available && channelEpoch)
m_channel.Kick(channelEpoch);
return true;
}
void CClipboardHub::Unbind(BackendId backend, uint32_t epoch)
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
IClipboardSource * source = nullptr;
bool stop = false;
Source * selected = nullptr;
IClipboardSource * source = nullptr;
bool stop = false;
bool resetChannel = false;
uint64_t channelEpoch = 0;
{
CSRWExclusiveLock lock(m_lock);
if ((!m_active && !m_failed && !m_reserved) ||
m_backend != backend || m_epoch != epoch || !m_source)
return;
source = m_source;
stop = m_running || m_reserved;
m_active = false;
m_running = false;
m_reserved = true;
m_failed = false;
m_failurePending = false;
AdvanceGenerationNL();
for (Source& slot : m_sources)
if ((slot.active || slot.failed || slot.reserved) &&
slot.backend == backend && slot.epoch == epoch && slot.endpoint)
{
selected = &slot;
source = slot.endpoint;
stop = slot.running || slot.reserved;
resetChannel = m_owner == &slot;
channelEpoch = m_channelEpoch;
slot.active = false;
slot.running = false;
slot.reserved = true;
slot.failed = false;
slot.failurePending = false;
InvalidateSourceCacheNL(slot);
if (resetChannel)
{
m_owner = nullptr;
m_ownerId = {};
m_resetting = true;
}
break;
}
}
if (!selected)
return;
CSRWExclusiveLock callbackLock(m_callbackLock);
if (stop)
source->Stop();
CSRWExclusiveLock lock(m_lock);
if (m_source == source && m_backend == backend && m_epoch == epoch)
if (selected->endpoint == source && selected->backend == backend &&
selected->epoch == epoch)
{
m_source = nullptr;
m_backend = 0;
m_epoch = 0;
m_active = false;
m_running = false;
m_reserved = false;
m_failed = false;
m_failurePending = false;
selected->backend = 0;
selected->epoch = 0;
selected->generation = 0;
selected->endpoint = nullptr;
selected->running = false;
selected->active = false;
selected->reserved = false;
selected->failed = false;
selected->failurePending = false;
}
lock.Unlock();
callbackLock.Unlock();
lifecycleLock.Unlock();
if (resetChannel && channelEpoch)
m_channel.Reset(channelEpoch, ERROR_DEVICE_NOT_CONNECTED);
}
bool CClipboardHub::TakeFailure(SourceKey& source)
{
CSRWSharedLock lifecycleLock(m_lifecycleLock);
CSRWExclusiveLock lock(m_lock);
if (!m_failurePending)
return false;
source = {};
source.backend = m_backend;
source.epoch = m_epoch;
m_failurePending = false;
return true;
for (Source& slot : m_sources)
{
if (!slot.failurePending)
continue;
source = {};
source.backend = slot.backend;
source.epoch = slot.epoch;
slot.failurePending = false;
return true;
}
return false;
}
void CClipboardHub::Stop()
{
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
IClipboardSource * source = nullptr;
bool stop = false;
Source * sources[MAX_SOURCES] = {};
unsigned count = 0;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped)
return;
m_stopped = true;
source = m_source;
stop = source && (m_running || m_reserved);
m_active = false;
m_running = false;
m_reserved = source != nullptr;
m_failed = false;
m_failurePending = false;
AdvanceGenerationNL();
m_stopped = true;
m_owner = nullptr;
m_cachedSource = nullptr;
m_ownerId = {};
m_cachedClipboard = {};
m_cachedValid = false;
m_resetting = false;
for (Source& source : m_sources)
if (source.endpoint)
{
source.active = false;
source.running = false;
source.reserved = true;
source.failed = false;
source.failurePending = false;
sources[count++] = &source;
}
}
// Clearing the channel handler is a callback-quiescence barrier. It must
@@ -288,41 +415,114 @@ void CClipboardHub::Stop()
m_channel.ClearHandler(this);
CSRWExclusiveLock callbackLock(m_callbackLock);
if (stop)
source->Stop();
for (unsigned i = count; i > 0; --i)
sources[i - 1]->endpoint->Stop();
CSRWExclusiveLock lock(m_lock);
m_source = nullptr;
m_backend = 0;
m_epoch = 0;
m_active = false;
m_running = false;
m_reserved = false;
m_failed = false;
m_failurePending = false;
for (Source& source : m_sources)
{
source.backend = 0;
source.epoch = 0;
source.generation = 0;
source.endpoint = nullptr;
source.running = false;
source.active = false;
source.reserved = false;
source.failed = false;
source.failurePending = false;
}
}
ClipboardClaimResult CClipboardHub::Claim(Source& source,
const ClipboardSourceId& id, KVMFRClipboardMessage& cached)
{
cached = {};
if (!id.client || !id.generation)
return ClipboardClaimResult::STALE;
uint64_t channelEpoch = 0;
{
CSRWExclusiveLock lock(m_lock);
if (!BindingValidNL(source))
return ClipboardClaimResult::STALE;
if (!m_available || m_resetting || !m_channelEpoch ||
!m_channel.Available())
return ClipboardClaimResult::UNAVAILABLE;
if (m_owner)
return OwnerIsNL(source, id) ?
ClipboardClaimResult::ACCEPTED : ClipboardClaimResult::BUSY;
m_owner = &source;
m_ownerId = id;
// A client-originated OFFER cannot be served by a replacement client.
// Its matching cleanup CLEAR is safe to replay, as is any
// Helper-originated publication.
if (m_cachedValid && (!m_cachedSource ||
m_cachedClipboard.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR))
{
cached = m_cachedClipboard;
cached.generation = source.generation;
}
channelEpoch = m_channelEpoch;
}
m_channel.Kick(channelEpoch);
return ClipboardClaimResult::ACCEPTED;
}
void CClipboardHub::Release(
Source& source, const ClipboardSourceId& id)
{
uint64_t channelEpoch = 0;
{
CSRWExclusiveLock lock(m_lock);
if (!OwnerIsNL(source, id))
return;
m_owner = nullptr;
m_ownerId = {};
channelEpoch = m_channelEpoch;
}
if (channelEpoch)
m_channel.Kick(channelEpoch);
}
void CClipboardHub::ClipboardState(bool available, uint64_t epoch)
{
CSRWExclusiveLock callbackLock(m_callbackLock);
IClipboardSource * source = nullptr;
uint32_t generation;
Source * sources[MAX_SOURCES] = {};
uint32_t generations[MAX_SOURCES] = {};
unsigned count = 0;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped)
return;
if (m_available != available || m_channelEpoch != epoch)
AdvanceGenerationNL();
m_available = available;
m_channelEpoch = epoch;
generation = m_generation;
if (m_active && m_running && !m_failed)
source = m_source;
const bool changed = m_available != available ||
m_channelEpoch != epoch || m_resetting;
m_available = available;
m_channelEpoch = epoch;
m_resetting = false;
if (!changed)
return;
m_owner = nullptr;
m_cachedSource = nullptr;
m_ownerId = {};
m_cachedClipboard = {};
m_cachedValid = false;
for (Source& source : m_sources)
if (BindingValidNL(source))
{
source.generation = NextGenerationNL();
sources[count] = &source;
generations[count] = source.generation;
++count;
}
}
if (source)
source->ClipboardState(available, generation);
for (unsigned i = 0; i < count; ++i)
sources[i]->endpoint->ClipboardState(available, generations[i]);
}
ClipboardChannelResult CClipboardHub::ClipboardRecord(
@@ -332,24 +532,39 @@ ClipboardChannelResult CClipboardHub::ClipboardRecord(
return ClipboardChannelResult::FAILED;
CSRWExclusiveLock callbackLock(m_callbackLock);
IClipboardSource * source = nullptr;
uint32_t generation;
Source * source = nullptr;
IClipboardSource * endpoint = nullptr;
ClipboardSourceId ownerId = {};
uint32_t generation = 0;
{
CSRWSharedLock lock(m_lock);
if (m_stopped || !m_available)
return ClipboardChannelResult::ACCEPTED;
if (m_failed || !m_active || !m_running || !m_source)
source = m_owner;
if (!source || !BindingValidNL(*source))
return HoldOrDiscard(record);
source = m_source;
generation = m_generation;
endpoint = source->endpoint;
ownerId = m_ownerId;
generation = source->generation;
}
// Helper records are transport-agnostic. Stamp them only after selecting
// the active transport binding.
KVMFRClipboardMessage stamped = record;
stamped.generation = generation;
const ClipboardChannelResult result =
source->SendClipboard(stamped,
endpoint->SendClipboard(stamped,
data.empty() ? nullptr : data.data());
if (result != ClipboardChannelResult::FAILED)
if (result == ClipboardChannelResult::ACCEPTED)
{
CSRWExclusiveLock lock(m_lock);
CachePublicationNL(stamped, nullptr);
const bool publication = stamped.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
stamped.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR;
return publication && m_owner && !OwnerIsNL(*source, ownerId) ?
ClipboardChannelResult::BUSY : result;
}
if (result == ClipboardChannelResult::BUSY)
return result;
if (MarkFailed(*source))
@@ -364,59 +579,78 @@ ClipboardChannelResult CClipboardHub::ClipboardRecord(
void CClipboardHub::ClipboardReset(uint64_t epoch, uint32_t reason)
{
CSRWExclusiveLock callbackLock(m_callbackLock);
IClipboardSource * source = nullptr;
uint32_t generation;
Source * sources[MAX_SOURCES] = {};
uint32_t generations[MAX_SOURCES] = {};
unsigned count = 0;
{
CSRWExclusiveLock lock(m_lock);
if (m_stopped || m_channelEpoch != epoch)
return;
AdvanceGenerationNL();
generation = m_generation;
if (m_active && m_running && !m_failed)
source = m_source;
m_owner = nullptr;
m_cachedSource = nullptr;
m_ownerId = {};
m_cachedClipboard = {};
m_cachedValid = false;
m_resetting = true;
for (Source& source : m_sources)
if (BindingValidNL(source))
{
source.generation = NextGenerationNL();
sources[count] = &source;
generations[count] = source.generation;
++count;
}
}
if (source)
source->ClipboardReset(generation, reason);
for (unsigned i = 0; i < count; ++i)
sources[i]->endpoint->ClipboardReset(generations[i], reason);
}
ClipboardChannelResult CClipboardHub::SendClipboard(
ClipboardChannelResult CClipboardHub::SendClipboard(Source& source,
const KVMFRClipboardMessage& record, const uint8_t * data)
{
const ClipboardChannelWrite write = { record, data };
size_t accepted = 0;
return SendClipboardBatch(&write, 1, accepted);
const ClipboardChannelWrite write = { record, data };
size_t accepted = 0;
return SendClipboardBatch(source, &write, 1, accepted);
}
ClipboardChannelResult CClipboardHub::SendClipboardBatch(
ClipboardChannelResult CClipboardHub::SendClipboardBatch(Source& source,
const ClipboardChannelWrite * records, size_t count, size_t& accepted)
{
accepted = 0;
if (!records || !count || count > KVMFR_CLIPBOARD_SLOT_COUNT)
return ClipboardChannelResult::FAILED;
// Keep the shared lock through Send so Unbind cannot advance the binding
// Keep the lock through Send so Unbind cannot advance the binding
// generation after validation but before the records enter the channel.
CSRWSharedLock lock(m_lock);
if (m_stopped || !m_available || !m_active || !m_running || m_failed ||
!m_source)
CSRWExclusiveLock lock(m_lock);
if (m_stopped || !BindingValidNL(source) || m_owner != &source)
return ClipboardChannelResult::FAILED;
if (!m_available || m_resetting || !m_channel.Available())
return ClipboardChannelResult::BUSY;
for (size_t i = 0; i < count; ++i)
if (!ValidClientDirection(records[i].record) ||
records[i].record.generation != m_generation)
records[i].record.generation != source.generation)
return ClipboardChannelResult::FAILED;
return m_channel.SendBatch(records, count, accepted);
const ClipboardChannelResult result =
m_channel.SendBatch(records, count, accepted);
for (size_t i = 0; i < accepted; ++i)
CachePublicationNL(records[i].record, &source);
if (result == ClipboardChannelResult::FAILED &&
(m_resetting || !m_channel.Available()))
return ClipboardChannelResult::BUSY;
return result;
}
void CClipboardHub::ClipboardReceiveReady()
void CClipboardHub::ClipboardReceiveReady(Source& source)
{
{
CSRWSharedLock lock(m_lock);
if (m_stopped || !m_available || !m_active || m_failed ||
!m_running)
if (m_stopped || !m_available || !BindingValidNL(source) ||
m_owner != &source)
return;
}
@@ -425,14 +659,9 @@ void CClipboardHub::ClipboardReceiveReady()
m_channel.Kick(epoch);
}
void CClipboardHub::ClipboardFailed()
void CClipboardHub::ClipboardFailed(Source& source)
{
IClipboardSource * source;
{
CSRWSharedLock lock(m_lock);
source = m_source;
}
if (!source || !MarkFailed(*source))
if (!MarkFailed(source))
return;
const uint64_t epoch = m_channel.Epoch();