/** * 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&& 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); }