From 3525796a003dc7c950a50e47c3ba0d6a571e72d1 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 14 Aug 2026 13:46:17 +1000 Subject: [PATCH] [idd] lgmp: route clipboard file operations --- idd/LGIdd/LGIdd.vcxproj | 2 + idd/LGIdd/LGIdd.vcxproj.filters | 6 + .../transport/lgmp/CLGMPClipboardFiles.cpp | 431 ++++++++++++++++++ .../transport/lgmp/CLGMPClipboardFiles.h | 114 +++++ .../lgmp/CLGMPClipboardTransport.cpp | 192 +++++++- .../transport/lgmp/CLGMPClipboardTransport.h | 10 +- 6 files changed, 736 insertions(+), 19 deletions(-) create mode 100644 idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.cpp create mode 100644 idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.h diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj index 6963fd1e..c1aca813 100644 --- a/idd/LGIdd/LGIdd.vcxproj +++ b/idd/LGIdd/LGIdd.vcxproj @@ -78,6 +78,7 @@ + @@ -154,6 +155,7 @@ + diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters index 1e6cf0bc..e547c041 100644 --- a/idd/LGIdd/LGIdd.vcxproj.filters +++ b/idd/LGIdd/LGIdd.vcxproj.filters @@ -250,6 +250,9 @@ Transport\LGMP + + Transport\LGMP + Transport\LGMP @@ -414,6 +417,9 @@ Transport\LGMP + + Transport\LGMP + Transport\LGMP diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.cpp b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.cpp new file mode 100644 index 00000000..d721b5e8 --- /dev/null +++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.cpp @@ -0,0 +1,431 @@ +/** + * 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/CLGMPClipboardFiles.h" + +bool CLGMPClipboardFiles::FromHelper(Direction direction) +{ + return direction == Direction::HELPER_TO_CLIENT; +} + +bool CLGMPClipboardFiles::RequesterHelper(uint64_t transfer) +{ + return kvmfrClipboardTransferFromHelper(transfer) != 0; +} + +bool CLGMPClipboardFiles::SameRequester( + Direction direction, uint64_t transfer) +{ + return FromHelper(direction) == RequesterHelper(transfer); +} + +CLGMPClipboardFiles::Acquisition * +CLGMPClipboardFiles::FindAcquisition(uint64_t transfer) +{ + for (Acquisition& acquisition : m_acquisitions) + if (acquisition.transfer == transfer) + return &acquisition; + return nullptr; +} + +const CLGMPClipboardFiles::Acquisition * +CLGMPClipboardFiles::FindAcquisition(uint64_t transfer) const +{ + for (const Acquisition& acquisition : m_acquisitions) + if (acquisition.transfer == transfer) + return &acquisition; + return nullptr; +} + +const CLGMPClipboardFiles::Acquisition * +CLGMPClipboardFiles::FindAcquired( + uint64_t dataset, bool requesterHelper) const +{ + for (const Acquisition& acquisition : m_acquisitions) + if (acquisition.dataset == dataset && acquisition.requesterHelper == + requesterHelper && acquisition.acquired) + return &acquisition; + return nullptr; +} + +bool CLGMPClipboardFiles::HasAcquisition( + uint64_t dataset, bool requesterHelper) const +{ + for (const Acquisition& acquisition : m_acquisitions) + if (acquisition.dataset == dataset && + acquisition.requesterHelper == requesterHelper) + return true; + return false; +} + +CLGMPClipboardFiles::Acquisition * CLGMPClipboardFiles::FreeAcquisition() +{ + for (Acquisition& acquisition : m_acquisitions) + if (!acquisition.Active()) + return &acquisition; + return nullptr; +} + +bool CLGMPClipboardFiles::HasFreeAcquisition() const +{ + for (const Acquisition& acquisition : m_acquisitions) + if (!acquisition.Active()) + return true; + return false; +} + +CLGMPClipboardFiles::Request * CLGMPClipboardFiles::FindRequest( + uint64_t transfer) +{ + for (Request& request : m_requests) + if (request.transfer == transfer) + return &request; + return nullptr; +} + +const CLGMPClipboardFiles::Request * CLGMPClipboardFiles::FindRequest( + uint64_t transfer) const +{ + for (const Request& request : m_requests) + if (request.transfer == transfer) + return &request; + return nullptr; +} + +CLGMPClipboardFiles::Request * CLGMPClipboardFiles::FreeRequest() +{ + for (Request& request : m_requests) + if (!request.Active()) + return &request; + return nullptr; +} + +bool CLGMPClipboardFiles::HasFreeRequest() const +{ + for (const Request& request : m_requests) + if (!request.Active()) + return true; + return false; +} + +bool CLGMPClipboardFiles::TransferInUse(uint64_t transfer) const +{ + return FindAcquisition(transfer) || FindRequest(transfer); +} + +bool CLGMPClipboardFiles::ValidateData(const Request& request, + const KVMFRClipboardMessage& message) const +{ + if (message.clipboardGeneration != request.dataset || + message.token != request.operation || + message.offset != request.expectedOffset || + message.sequence != request.expectedSequence) + return false; + + const uint64_t end = message.offset + message.length; + if (!request.began) + { + if (message.offset || message.sequence || + !(message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) + return false; + } + else if (message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN) + return false; + + const uint64_t hint = request.began ? request.sizeHint : message.size; + if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint) + return false; + if (request.operation == KVMFR_CLIPBOARD_FILE_OP_READ && + end > request.requestedBytes) + return false; + if (message.flags & KVMFR_CLIPBOARD_FLAG_END) + return message.size == end && + (hint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || hint == end); + return true; +} + +void CLGMPClipboardFiles::RemoveAcquisition(Acquisition& acquisition) +{ + const uint64_t dataset = acquisition.dataset; + const bool requesterHelper = acquisition.requesterHelper; + acquisition.Clear(); + for (Request& request : m_requests) + if (request.dataset == dataset && + request.requesterHelper == requesterHelper) + request.Clear(); +} + +bool CLGMPClipboardFiles::IsRecord(const KVMFRClipboardMessage& message) +{ + return kvmfrClipboardFileMessageType(message.type) != 0; +} + +bool CLGMPClipboardFiles::IsData(const KVMFRClipboardMessage& message) +{ + return message.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA; +} + +bool CLGMPClipboardFiles::DirectionValid( + const KVMFRClipboardMessage& message, Direction direction) +{ + if (!kvmfrClipboardFileMessageValid(&message)) + return false; + + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + return SameRequester(direction, message.transfer); + + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA: + return !SameRequester(direction, message.transfer); + + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + return true; + + default: + return false; + } +} + +bool CLGMPClipboardFiles::TransferActive(uint64_t transfer) const +{ + return TransferInUse(transfer); +} + +bool CLGMPClipboardFiles::IsStaleTerminal( + const KVMFRClipboardMessage& message, Direction direction) const +{ + if (!DirectionValid(message, direction) || + TransferInUse(message.transfer)) + return false; + + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA: + return !SameRequester(direction, message.transfer); + + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + return SameRequester(direction, message.transfer); + + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + return true; + + default: + return false; + } +} + +bool CLGMPClipboardFiles::Validate(const KVMFRClipboardMessage& message, + Direction direction, uint64_t offeredDataset, bool filesOffered) const +{ + if (!DirectionValid(message, direction)) + return false; + + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + return SameRequester(direction, message.transfer) && filesOffered && + message.clipboardGeneration == offeredDataset && + !TransferInUse(message.transfer) && + !HasAcquisition(message.clipboardGeneration, + FromHelper(direction)) && HasFreeAcquisition(); + + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + { + const Acquisition * acquisition = + FindAcquisition(message.transfer); + return acquisition && !acquisition->acquired && + acquisition->dataset == message.clipboardGeneration && + FromHelper(direction) != acquisition->requesterHelper; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + { + const Acquisition * acquisition = + FindAcquisition(message.transfer); + return acquisition && acquisition->acquired && + acquisition->dataset == message.clipboardGeneration && + FromHelper(direction) == acquisition->requesterHelper; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + return SameRequester(direction, message.transfer) && + !TransferInUse(message.transfer) && HasFreeRequest() && + FindAcquired(message.clipboardGeneration, + FromHelper(direction)); + + case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA: + { + const Request * request = FindRequest(message.transfer); + return request && + FromHelper(direction) != request->requesterHelper && + ValidateData(*request, message); + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + { + const Acquisition * acquisition = + FindAcquisition(message.transfer); + if (acquisition) + return acquisition->dataset == message.clipboardGeneration; + const Request * request = FindRequest(message.transfer); + return request && request->dataset == message.clipboardGeneration; + } + + default: + return false; + } +} + +void CLGMPClipboardFiles::Apply( + const KVMFRClipboardMessage& message, Direction direction) +{ + switch (message.type) + { + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE: + { + Acquisition * acquisition = FreeAcquisition(); + if (!acquisition) + return; + acquisition->dataset = message.clipboardGeneration; + acquisition->transfer = message.transfer; + acquisition->requesterHelper = FromHelper(direction); + acquisition->acquired = false; + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED: + { + Acquisition * acquisition = FindAcquisition(message.transfer); + if (!acquisition) + return; + if (message.token == KVMFR_CLIPBOARD_FILE_ERROR_NONE) + acquisition->acquired = true; + else + RemoveAcquisition(*acquisition); + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE: + case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL: + { + Acquisition * acquisition = FindAcquisition(message.transfer); + if (acquisition) + { + RemoveAcquisition(*acquisition); + break; + } + Request * request = FindRequest(message.transfer); + if (request) + request->Clear(); + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST: + { + Request * request = FreeRequest(); + if (!request) + return; + request->dataset = message.clipboardGeneration; + request->transfer = message.transfer; + request->operation = message.token; + request->requestedBytes = message.token == + KVMFR_CLIPBOARD_FILE_OP_READ ? message.flags : 0; + request->requesterHelper = FromHelper(direction); + request->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + break; + } + + case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA: + { + Request * request = FindRequest(message.transfer); + if (!request) + return; + if (!request->began) + { + request->began = true; + request->sizeHint = message.size; + } + request->expectedOffset += message.length; + ++request->expectedSequence; + if (message.flags & KVMFR_CLIPBOARD_FLAG_END) + request->Clear(); + break; + } + } +} + +void CLGMPClipboardFiles::Reset() +{ + for (Acquisition& acquisition : m_acquisitions) + acquisition.Clear(); + for (Request& request : m_requests) + request.Clear(); +} + +unsigned CLGMPClipboardFiles::Disconnect(KVMFRClipboardMessage * records, + unsigned capacity, uint32_t endpointGeneration) +{ + unsigned count = 0; + for (const Request& request : m_requests) + { + if (!request.Active() || count == capacity) + continue; + KVMFRClipboardMessage& record = records[count++]; + record = {}; + record.version = KVMFR_CLIPBOARD_VERSION; + record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL; + record.generation = endpointGeneration; + record.clipboardGeneration = request.dataset; + record.transfer = request.transfer; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + record.token = request.requesterHelper ? + KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED : + KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED; + } + + for (const Acquisition& acquisition : m_acquisitions) + { + if (!acquisition.Active() || count == capacity) + continue; + KVMFRClipboardMessage& record = records[count++]; + record = {}; + record.version = KVMFR_CLIPBOARD_VERSION; + record.type = acquisition.requesterHelper ? + (acquisition.acquired ? KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL : + KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED) : + (acquisition.acquired ? KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE : + KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL); + record.generation = endpointGeneration; + record.clipboardGeneration = acquisition.dataset; + record.transfer = acquisition.transfer; + record.format = KVMFR_CLIPBOARD_FORMAT_FILES; + if (acquisition.requesterHelper) + record.token = KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED; + else if (!acquisition.acquired) + record.token = KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED; + } + Reset(); + return count; +} diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.h b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.h new file mode 100644 index 00000000..41f4bb51 --- /dev/null +++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardFiles.h @@ -0,0 +1,114 @@ +/** + * 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 + */ + +#pragma once + +#include "common/KVMFRClipboard.h" + +#include + +class CLGMPClipboardFiles +{ +public: + enum class Direction + { + CLIENT_TO_HELPER, + HELPER_TO_CLIENT, + }; + + static constexpr unsigned MAX_ACQUISITIONS = + KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS; + static constexpr unsigned MAX_REQUESTS = + KVMFR_CLIPBOARD_FILE_MAX_REQUESTS; + +private: + struct Acquisition + { + uint64_t dataset = 0; + uint64_t transfer = 0; + bool requesterHelper = false; + bool acquired = false; + + bool Active() const { return transfer != 0; } + void Clear() { *this = {}; } + }; + + struct Request + { + uint64_t dataset = 0; + uint64_t transfer = 0; + uint64_t expectedOffset = 0; + uint64_t sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + uint32_t expectedSequence = 0; + uint32_t requestedBytes = 0; + KVMFRClipboardFileOperation operation = 0; + bool requesterHelper = false; + bool began = false; + + bool Active() const { return transfer != 0; } + void Clear() { *this = {}; } + }; + + Acquisition m_acquisitions[MAX_ACQUISITIONS] = {}; + Request m_requests[MAX_REQUESTS] = {}; + + static bool FromHelper(Direction direction); + static bool RequesterHelper(uint64_t transfer); + static bool SameRequester(Direction direction, uint64_t transfer); + + Acquisition * FindAcquisition(uint64_t transfer); + const Acquisition * FindAcquisition(uint64_t transfer) const; + const Acquisition * FindAcquired( + uint64_t dataset, bool requesterHelper) const; + bool HasAcquisition(uint64_t dataset, bool requesterHelper) const; + Acquisition * FreeAcquisition(); + bool HasFreeAcquisition() const; + + Request * FindRequest(uint64_t transfer); + const Request * FindRequest(uint64_t transfer) const; + Request * FreeRequest(); + bool HasFreeRequest() const; + + bool TransferInUse(uint64_t transfer) const; + bool ValidateData(const Request& request, + const KVMFRClipboardMessage& message) const; + void RemoveAcquisition(Acquisition& acquisition); + +public: + static bool IsRecord(const KVMFRClipboardMessage& message); + static bool IsData(const KVMFRClipboardMessage& message); + static bool DirectionValid(const KVMFRClipboardMessage& message, + Direction direction); + bool TransferActive(uint64_t transfer) const; + bool IsStaleTerminal(const KVMFRClipboardMessage& message, + Direction direction) const; + + bool Validate(const KVMFRClipboardMessage& message, + Direction direction, uint64_t offeredDataset, + bool filesOffered) const; + void Apply(const KVMFRClipboardMessage& message, Direction direction); + void Reset(); + + // Builds the records which release Helper-owned resources or fail Helper + // requests when an LGMP clipboard owner disappears. The state is reset even + // when capacity is smaller than the number of generated records. + unsigned Disconnect(KVMFRClipboardMessage * records, unsigned capacity, + uint32_t endpointGeneration); +}; diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp index 50462e4a..694f1382 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.cpp @@ -75,7 +75,7 @@ namespace kvmfrClipboardTransferFromHelper(message.transfer) : kvmfrClipboardTransferFromClient(message.transfer); return message.clipboardGeneration && validTransfer && - kvmfrClipboardFormatValid(message.format) && + kvmfrClipboardRepresentationFormatValid(message.format) && !message.offset && !message.size && !message.flags && !message.token && !message.length && !message.sequence; } @@ -83,7 +83,8 @@ namespace bool ValidCancel(const KVMFRClipboardMessage& message) { return message.transfer && !message.offset && !message.size && - (!message.format || kvmfrClipboardFormatValid(message.format)) && + (!message.format || + kvmfrClipboardRepresentationFormatValid(message.format)) && !message.flags && !message.length && !message.sequence; } @@ -91,7 +92,8 @@ namespace { return message.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST || message.type == KVMFR_CLIPBOARD_MESSAGE_DATA || - message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL; + message.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL || + CLGMPClipboardFiles::IsRecord(message); } } @@ -367,6 +369,39 @@ void CLGMPClipboardTransport::QueueStaleRequestCancel( 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) { @@ -423,12 +458,14 @@ void CLGMPClipboardTransport::ReleaseOwner( clientID, generation, reason); if (clearHelper) { + QueueFileDisconnect(); QueueTransferCancel(clientToHelper); QueueTransferCancel(helperToClient); QueueHelperClear(); } else { + m_files.Reset(); m_clientClipboardGeneration = 0; m_clientFormats = 0; } @@ -441,6 +478,7 @@ void CLGMPClipboardTransport::ResetProtocol(bool keepClipboard) ReleaseOwner("endpoint reset", false); m_clientToHelper.Clear(); m_helperToClient.Clear(); + m_files.Reset(); m_clientClipboardGeneration = 0; m_clientFormats = 0; m_discardHelperToClient = 0; @@ -661,10 +699,18 @@ bool CLGMPClipboardTransport::ValidateOwnedControl( 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 && - kvmfrClipboardFormatValid(message.format) && + (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)) && @@ -678,6 +724,21 @@ bool CLGMPClipboardTransport::ValidateOwnedControl( 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: @@ -704,6 +765,15 @@ bool CLGMPClipboardTransport::ValidateOutboundRecord( 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: @@ -718,7 +788,7 @@ bool CLGMPClipboardTransport::ValidateOutboundRecord( case KVMFR_CLIPBOARD_MESSAGE_DATA: return message.clipboardGeneration && kvmfrClipboardTransferFromClient(message.transfer) && - kvmfrClipboardFormatValid(message.format) && + kvmfrClipboardRepresentationFormatValid(message.format) && message.length <= KVMFR_CLIPBOARD_DATA_BYTES && !(message.flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | KVMFR_CLIPBOARD_FLAG_END)) && !message.token && @@ -786,6 +856,13 @@ void CLGMPClipboardTransport::AdvanceChunk( 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: @@ -824,6 +901,13 @@ void CLGMPClipboardTransport::ApplyInbound( 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: @@ -977,6 +1061,22 @@ bool CLGMPClipboardTransport::ProcessMessage( 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); @@ -1009,9 +1109,12 @@ bool CLGMPClipboardTransport::ProcessMessage( 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 && + (dataMessage.type == KVMFR_CLIPBOARD_MESSAGE_DATA || + fileData) && dataMessage.generation == m_ownerGeneration && dataMessage.clipboardGeneration == message.clipboardGeneration && dataMessage.transfer == message.transfer && @@ -1020,16 +1123,28 @@ bool CLGMPClipboardTransport::ProcessMessage( dataMessage.format == message.format && dataMessage.flags == message.flags && dataMessage.length == message.length && - dataMessage.sequence == message.sequence && - !dataMessage.token; - if (!matchesCommit || - !kvmfrClipboardTransferFromHelper(dataMessage.transfer) || - !ValidateChunk(m_clientToHelper, dataMessage)) + 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(); @@ -1039,6 +1154,13 @@ bool CLGMPClipboardTransport::ProcessMessage( 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()) { @@ -1144,7 +1266,11 @@ ClipboardChannelResult CLGMPClipboardTransport::SendControl( ClipboardChannelResult CLGMPClipboardTransport::SendData( const KVMFRClipboardMessage& record, const uint8_t * data) { - if (!ValidateChunk(m_helperToClient, record)) + 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); @@ -1192,6 +1318,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard( if (BlockedOwnerLost(record)) { QueueStaleRequestCancel(record); + QueueStaleFileRecord(record); ClearOutboundBlock(); Wake(); return ClipboardChannelResult::ACCEPTED; @@ -1201,8 +1328,31 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard( record.generation != m_endpointGeneration) return ClipboardChannelResult::FAILED; - if (!ValidateOutboundRecord(record) || - (record.length && !data)) + 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 && @@ -1248,6 +1398,7 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard( if (OwnerScopedLifecycle(record)) { QueueStaleRequestCancel(record); + QueueStaleFileRecord(record); ClearOutboundBlock(); Wake(); return ClipboardChannelResult::ACCEPTED; @@ -1265,7 +1416,9 @@ ClipboardChannelResult CLGMPClipboardTransport::SendClipboard( } ClipboardChannelResult result; - if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA) + 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) @@ -1316,6 +1469,7 @@ void CLGMPClipboardTransport::ClipboardState( ReleaseOwner("Helper endpoint changed", false); m_clientToHelper.Clear(); m_helperToClient.Clear(); + m_files.Reset(); m_clientClipboardGeneration = 0; m_clientFormats = 0; m_discardHelperToClient = 0; @@ -1344,6 +1498,7 @@ void CLGMPClipboardTransport::ClipboardReset( ReleaseOwner("Helper reset", false); m_clientToHelper.Clear(); m_helperToClient.Clear(); + m_files.Reset(); m_clientClipboardGeneration = 0; m_clientFormats = 0; m_discardHelperToClient = 0; @@ -1385,8 +1540,11 @@ void CLGMPClipboardTransport::Thread() 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() || - !DrainMessage() || !PublishStatus() || + (m_internalTargetCount == 0 && !DrainMessage()) || + !PublishStatus() || !ReplayClipboard() || !PostGrants()) { notifyFailed = true; @@ -1398,7 +1556,7 @@ void CLGMPClipboardTransport::Thread() notifyReady = m_target != nullptr; readyTarget = m_target; } - if (m_pendingTarget.valid || m_ownerClientID) + if (m_pendingTarget.valid || m_internalTargetCount || m_ownerClientID) timeout = ACTIVE_POLL_MS; } diff --git a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h index c7ea59f5..0c8f8d92 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h +++ b/idd/LGIdd/transport/lgmp/CLGMPClipboardTransport.h @@ -22,6 +22,7 @@ #include "CSRWLock.h" #include "transport/IClipboardSource.h" +#include "transport/lgmp/CLGMPClipboardFiles.h" #include "common/KVMFRClipboard.h" #include "common/LGMPConfig.h" @@ -39,8 +40,10 @@ class CLGMPHost; class CLGMPClipboardTransport final : public IClipboardSource { private: - static constexpr unsigned MEMORY_COUNT = 2; - static constexpr unsigned INTERNAL_TARGET_COUNT = 3; + static constexpr unsigned MEMORY_COUNT = 8; + static constexpr unsigned INTERNAL_TARGET_COUNT = + CLGMPClipboardFiles::MAX_ACQUISITIONS + + CLGMPClipboardFiles::MAX_REQUESTS + 3; static constexpr ULONGLONG OWNER_LEASE_MS = 1000; static constexpr DWORD ACTIVE_POLL_MS = 5; static constexpr DWORD IDLE_POLL_MS = 50; @@ -135,6 +138,7 @@ private: PendingTarget m_pendingTarget; KVMFRClipboardMessage m_internalTarget[INTERNAL_TARGET_COUNT] = {}; unsigned m_internalTargetCount = 0; + CLGMPClipboardFiles m_files; bool Initialize(); void DeInit(); @@ -156,6 +160,8 @@ private: void QueueTransferCancel(const Transfer& transfer); void QueueStaleRequestCancel( const KVMFRClipboardMessage& request); + void QueueStaleFileRecord(const KVMFRClipboardMessage& record); + void QueueFileDisconnect(); bool QueueInternalTarget(const KVMFRClipboardMessage& record); bool PumpInternalTarget();