mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 07:01:30 +00:00
Extend the clipboard target boundary with ordered prefix delivery. Pass up to four queued LGMP stream records to the Helper ring in one operation, apply state only to the accepted prefix, and retain the remaining suffix for exact retries. The channel holds its write lock across the prefix and emits one pipe doorbell per stream window instead of one per record.
442 lines
13 KiB
C++
442 lines
13 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/CClipboardHub.h"
|
|
|
|
#include "Seq.h"
|
|
|
|
namespace
|
|
{
|
|
bool ValidHelperDirection(const KVMFRClipboardMessage& record)
|
|
{
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
|
|
return kvmfrClipboardTransferFromHelper(record.transfer);
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
|
|
return kvmfrClipboardTransferFromClient(record.transfer);
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
|
|
return kvmfrClipboardTransferFromHelper(record.transfer);
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA)
|
|
return kvmfrClipboardTransferFromClient(record.transfer);
|
|
return true;
|
|
}
|
|
|
|
bool ValidClientDirection(const KVMFRClipboardMessage& record)
|
|
{
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
|
|
return kvmfrClipboardTransferFromClient(record.transfer);
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
|
|
return kvmfrClipboardTransferFromHelper(record.transfer);
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
|
|
return kvmfrClipboardTransferFromClient(record.transfer);
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA)
|
|
return kvmfrClipboardTransferFromHelper(record.transfer);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
CClipboardHub::CClipboardHub(CClipboardChannel& channel) :
|
|
m_channel(channel)
|
|
{
|
|
m_channel.SetHandler(this);
|
|
}
|
|
|
|
CClipboardHub::~CClipboardHub()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
void CClipboardHub::AdvanceGenerationNL()
|
|
{
|
|
Seq::Inc(m_generation);
|
|
}
|
|
|
|
bool CClipboardHub::MarkFailed(IClipboardSource& source)
|
|
{
|
|
CSRWExclusiveLock lock(m_lock);
|
|
if (m_source != &source || (!m_active && !m_reserved))
|
|
return false;
|
|
|
|
const bool changed = !m_failed;
|
|
m_failed = true;
|
|
m_failurePending = true;
|
|
return changed;
|
|
}
|
|
|
|
ClipboardChannelResult CClipboardHub::HoldOrDiscard(
|
|
const KVMFRClipboardMessage& record)
|
|
{
|
|
// Preserve the pending clipboard publication until a source is rebound.
|
|
// Transfer-scoped records belong to the source which admitted them and
|
|
// must never cross a transport generation.
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR)
|
|
return ClipboardChannelResult::BUSY;
|
|
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
|
|
{
|
|
KVMFRClipboardMessage cancel = {};
|
|
cancel.version = KVMFR_CLIPBOARD_VERSION;
|
|
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
|
|
cancel.generation = record.generation;
|
|
cancel.clipboardGeneration = record.clipboardGeneration;
|
|
cancel.transfer = record.transfer;
|
|
cancel.format = record.format;
|
|
cancel.token = ERROR_DEVICE_NOT_CONNECTED;
|
|
const ClipboardChannelResult result = m_channel.Send(cancel);
|
|
if (result != ClipboardChannelResult::ACCEPTED)
|
|
return result;
|
|
}
|
|
|
|
if (record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ||
|
|
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST)
|
|
{
|
|
KVMFRClipboardMessage reject = {};
|
|
reject.version = KVMFR_CLIPBOARD_VERSION;
|
|
reject.type = record.type ==
|
|
KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ?
|
|
KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED :
|
|
KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
|
|
reject.generation = record.generation;
|
|
reject.clipboardGeneration = record.clipboardGeneration;
|
|
reject.transfer = record.transfer;
|
|
reject.format = KVMFR_CLIPBOARD_FORMAT_FILES;
|
|
reject.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED;
|
|
const ClipboardChannelResult result = m_channel.Send(reject);
|
|
if (result != ClipboardChannelResult::ACCEPTED)
|
|
return result;
|
|
}
|
|
|
|
return ClipboardChannelResult::ACCEPTED;
|
|
}
|
|
|
|
bool CClipboardHub::Bind(
|
|
BackendId backend, uint32_t epoch, IClipboardSource& source)
|
|
{
|
|
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
|
|
if (!backend || !epoch)
|
|
return false;
|
|
|
|
bool available;
|
|
uint32_t generation;
|
|
uint64_t channelEpoch;
|
|
{
|
|
CSRWExclusiveLock lock(m_lock);
|
|
if (m_stopped || m_source || m_active || m_reserved)
|
|
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;
|
|
}
|
|
|
|
CSRWExclusiveLock callbackLock(m_callbackLock);
|
|
const bool started = source.Start(*this);
|
|
bool attached = false;
|
|
{
|
|
CSRWExclusiveLock lock(m_lock);
|
|
if (started && m_source == &source && m_backend == backend &&
|
|
m_epoch == epoch && m_active && m_reserved && !m_failed &&
|
|
!m_stopped)
|
|
{
|
|
m_running = true;
|
|
m_reserved = false;
|
|
available = m_available;
|
|
generation = m_generation;
|
|
channelEpoch = m_channelEpoch;
|
|
attached = true;
|
|
}
|
|
else
|
|
{
|
|
m_active = false;
|
|
m_running = false;
|
|
}
|
|
}
|
|
|
|
if (!attached)
|
|
{
|
|
source.Stop();
|
|
CSRWExclusiveLock lock(m_lock);
|
|
if (m_source == &source && m_backend == backend && m_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;
|
|
}
|
|
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;
|
|
{
|
|
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();
|
|
}
|
|
|
|
CSRWExclusiveLock callbackLock(m_callbackLock);
|
|
if (stop)
|
|
source->Stop();
|
|
|
|
CSRWExclusiveLock lock(m_lock);
|
|
if (m_source == source && m_backend == backend && m_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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void CClipboardHub::Stop()
|
|
{
|
|
CSRWExclusiveLock lifecycleLock(m_lifecycleLock);
|
|
IClipboardSource * source = nullptr;
|
|
bool stop = false;
|
|
{
|
|
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();
|
|
}
|
|
|
|
// Clearing the channel handler is a callback-quiescence barrier. It must
|
|
// precede taking m_callbackLock because an in-flight channel callback owns
|
|
// the channel's handler lock while waiting for m_callbackLock.
|
|
m_channel.ClearHandler(this);
|
|
|
|
CSRWExclusiveLock callbackLock(m_callbackLock);
|
|
if (stop)
|
|
source->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;
|
|
}
|
|
|
|
void CClipboardHub::ClipboardState(bool available, uint64_t epoch)
|
|
{
|
|
CSRWExclusiveLock callbackLock(m_callbackLock);
|
|
IClipboardSource * source = nullptr;
|
|
uint32_t generation;
|
|
{
|
|
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;
|
|
}
|
|
|
|
if (source)
|
|
source->ClipboardState(available, generation);
|
|
}
|
|
|
|
ClipboardChannelResult CClipboardHub::ClipboardRecord(
|
|
const KVMFRClipboardMessage& record, std::vector<uint8_t>&& data)
|
|
{
|
|
if (!ValidHelperDirection(record))
|
|
return ClipboardChannelResult::FAILED;
|
|
|
|
CSRWExclusiveLock callbackLock(m_callbackLock);
|
|
IClipboardSource * source = nullptr;
|
|
uint32_t generation;
|
|
{
|
|
CSRWSharedLock lock(m_lock);
|
|
if (m_stopped || !m_available)
|
|
return ClipboardChannelResult::ACCEPTED;
|
|
if (m_failed || !m_active || !m_running || !m_source)
|
|
return HoldOrDiscard(record);
|
|
source = m_source;
|
|
generation = m_generation;
|
|
}
|
|
|
|
KVMFRClipboardMessage stamped = record;
|
|
stamped.generation = generation;
|
|
const ClipboardChannelResult result =
|
|
source->SendClipboard(stamped,
|
|
data.empty() ? nullptr : data.data());
|
|
if (result != ClipboardChannelResult::FAILED)
|
|
return result;
|
|
|
|
if (MarkFailed(*source))
|
|
{
|
|
const uint64_t epoch = m_channel.Epoch();
|
|
if (epoch)
|
|
m_channel.Kick(epoch);
|
|
}
|
|
return HoldOrDiscard(record);
|
|
}
|
|
|
|
void CClipboardHub::ClipboardReset(uint64_t epoch, uint32_t reason)
|
|
{
|
|
CSRWExclusiveLock callbackLock(m_callbackLock);
|
|
IClipboardSource * source = nullptr;
|
|
uint32_t generation;
|
|
{
|
|
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;
|
|
}
|
|
|
|
if (source)
|
|
source->ClipboardReset(generation, reason);
|
|
}
|
|
|
|
ClipboardChannelResult CClipboardHub::SendClipboard(
|
|
const KVMFRClipboardMessage& record, const uint8_t * data)
|
|
{
|
|
const ClipboardChannelWrite write = { record, data };
|
|
size_t accepted = 0;
|
|
return SendClipboardBatch(&write, 1, accepted);
|
|
}
|
|
|
|
ClipboardChannelResult CClipboardHub::SendClipboardBatch(
|
|
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
|
|
// 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)
|
|
return ClipboardChannelResult::FAILED;
|
|
|
|
for (size_t i = 0; i < count; ++i)
|
|
if (!ValidClientDirection(records[i].record) ||
|
|
records[i].record.generation != m_generation)
|
|
return ClipboardChannelResult::FAILED;
|
|
|
|
return m_channel.SendBatch(records, count, accepted);
|
|
}
|
|
|
|
void CClipboardHub::ClipboardReceiveReady()
|
|
{
|
|
{
|
|
CSRWSharedLock lock(m_lock);
|
|
if (m_stopped || !m_available || !m_active || m_failed ||
|
|
!m_running)
|
|
return;
|
|
}
|
|
|
|
const uint64_t epoch = m_channel.Epoch();
|
|
if (epoch)
|
|
m_channel.Kick(epoch);
|
|
}
|
|
|
|
void CClipboardHub::ClipboardFailed()
|
|
{
|
|
IClipboardSource * source;
|
|
{
|
|
CSRWSharedLock lock(m_lock);
|
|
source = m_source;
|
|
}
|
|
if (!source || !MarkFailed(*source))
|
|
return;
|
|
|
|
const uint64_t epoch = m_channel.Epoch();
|
|
if (epoch)
|
|
m_channel.Kick(epoch);
|
|
}
|