mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 15:11:31 +00:00
2295 lines
63 KiB
C++
2295 lines
63 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 "CClipboardManager.h"
|
|
|
|
#include <CDebug.h>
|
|
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <new>
|
|
#include <string.h>
|
|
#include <strsafe.h>
|
|
#include <utility>
|
|
|
|
namespace
|
|
{
|
|
static constexpr size_t MEMORY_SPOOL_LIMIT = 1024U * 1024U;
|
|
static constexpr size_t TEXT_CONVERSION_CHUNK = 16384;
|
|
static constexpr uint64_t MAX_SPOOL_BYTES =
|
|
UINT64_C(512) * 1024U * 1024U;
|
|
static constexpr DWORD CHANNEL_RETRY_MS = 10;
|
|
static constexpr DWORD REMOTE_RETRY_MS = 250;
|
|
static constexpr DWORD REMOTE_RETRY_TIMEOUT_MS = 5000;
|
|
static constexpr UINT_PTR REMOTE_RETRY_TIMER = 0x4c47;
|
|
static constexpr uint32_t ORIGIN_MAGIC = 0x4c47434fU;
|
|
// BI_ALPHABITFIELDS is a serialized DIB value, but desktop SDKs omit it.
|
|
static constexpr DWORD DIB_ALPHA_BITFIELDS = 6U;
|
|
|
|
struct ClipboardOrigin
|
|
{
|
|
uint32_t magic;
|
|
uint32_t reserved;
|
|
uint64_t epoch;
|
|
uint64_t generation;
|
|
};
|
|
|
|
bool WriteAll(HANDLE file, const void * data, size_t size)
|
|
{
|
|
const uint8_t * current = static_cast<const uint8_t *>(data);
|
|
while (size)
|
|
{
|
|
const DWORD chunk = static_cast<DWORD>((std::min<size_t>)(
|
|
size, (std::numeric_limits<DWORD>::max)()));
|
|
DWORD written = 0;
|
|
if (!WriteFile(file, current, chunk, &written, nullptr))
|
|
return false;
|
|
if (written != chunk)
|
|
{
|
|
SetLastError(ERROR_WRITE_FAULT);
|
|
return false;
|
|
}
|
|
current += written;
|
|
size -= written;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ReadAll(HANDLE file, void * data, size_t size)
|
|
{
|
|
uint8_t * current = static_cast<uint8_t *>(data);
|
|
while (size)
|
|
{
|
|
const DWORD chunk = static_cast<DWORD>((std::min<size_t>)(
|
|
size, (std::numeric_limits<DWORD>::max)()));
|
|
DWORD read = 0;
|
|
if (!ReadFile(file, current, chunk, &read, nullptr))
|
|
return false;
|
|
if (read != chunk)
|
|
{
|
|
SetLastError(ERROR_HANDLE_EOF);
|
|
return false;
|
|
}
|
|
current += read;
|
|
size -= read;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void AppendUTF8(std::vector<uint8_t>& output, uint32_t codepoint)
|
|
{
|
|
if (codepoint <= 0x7f)
|
|
output.push_back(static_cast<uint8_t>(codepoint));
|
|
else if (codepoint <= 0x7ff)
|
|
{
|
|
output.push_back(static_cast<uint8_t>(0xc0 | (codepoint >> 6)));
|
|
output.push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3f)));
|
|
}
|
|
else if (codepoint <= 0xffff)
|
|
{
|
|
output.push_back(static_cast<uint8_t>(0xe0 | (codepoint >> 12)));
|
|
output.push_back(static_cast<uint8_t>(0x80 |
|
|
((codepoint >> 6) & 0x3f)));
|
|
output.push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3f)));
|
|
}
|
|
else
|
|
{
|
|
output.push_back(static_cast<uint8_t>(0xf0 | (codepoint >> 18)));
|
|
output.push_back(static_cast<uint8_t>(0x80 |
|
|
((codepoint >> 12) & 0x3f)));
|
|
output.push_back(static_cast<uint8_t>(0x80 |
|
|
((codepoint >> 6) & 0x3f)));
|
|
output.push_back(static_cast<uint8_t>(0x80 | (codepoint & 0x3f)));
|
|
}
|
|
}
|
|
|
|
uint32_t DIBPixelOffset(const uint8_t * data, size_t size)
|
|
{
|
|
if (size < sizeof(DWORD))
|
|
return 0;
|
|
|
|
const DWORD headerSize = *reinterpret_cast<const DWORD *>(data);
|
|
if (headerSize < sizeof(BITMAPCOREHEADER) || headerSize > size)
|
|
return 0;
|
|
|
|
uint64_t offset = headerSize;
|
|
if (headerSize == sizeof(BITMAPCOREHEADER))
|
|
{
|
|
const BITMAPCOREHEADER * header =
|
|
reinterpret_cast<const BITMAPCOREHEADER *>(data);
|
|
if (header->bcBitCount <= 8)
|
|
offset += (1ULL << header->bcBitCount) * sizeof(RGBTRIPLE);
|
|
}
|
|
else if (headerSize >= sizeof(BITMAPINFOHEADER))
|
|
{
|
|
const BITMAPINFOHEADER * header =
|
|
reinterpret_cast<const BITMAPINFOHEADER *>(data);
|
|
if (headerSize == sizeof(BITMAPINFOHEADER) &&
|
|
(header->biCompression == BI_BITFIELDS ||
|
|
header->biCompression == DIB_ALPHA_BITFIELDS))
|
|
offset += header->biCompression == DIB_ALPHA_BITFIELDS ? 16U : 12U;
|
|
|
|
const uint32_t colors = header->biClrUsed ? header->biClrUsed :
|
|
(header->biBitCount <= 8 ? 1U << header->biBitCount : 0U);
|
|
offset += static_cast<uint64_t>(colors) * sizeof(RGBQUAD);
|
|
}
|
|
else
|
|
return 0;
|
|
|
|
return offset <= size &&
|
|
offset <= (std::numeric_limits<uint32_t>::max)() ?
|
|
static_cast<uint32_t>(offset) : 0;
|
|
}
|
|
}
|
|
|
|
class CClipboardSpool
|
|
{
|
|
private:
|
|
std::vector<uint8_t> m_memory;
|
|
HANDLE m_file = INVALID_HANDLE_VALUE;
|
|
uint64_t m_size = 0;
|
|
|
|
bool Spill()
|
|
{
|
|
WCHAR path[MAX_PATH];
|
|
const DWORD length = GetTempPathW(ARRAYSIZE(path), path);
|
|
if (!length)
|
|
return false;
|
|
if (length >= ARRAYSIZE(path))
|
|
{
|
|
SetLastError(ERROR_INSUFFICIENT_BUFFER);
|
|
return false;
|
|
}
|
|
|
|
for (unsigned int attempt = 0;
|
|
attempt != 32 && m_file == INVALID_HANDLE_VALUE; ++attempt)
|
|
{
|
|
WCHAR name[MAX_PATH];
|
|
const uint64_t nonce = GetTickCount64() ^
|
|
(static_cast<uint64_t>(GetCurrentProcessId()) << 32) ^ attempt;
|
|
if (FAILED(StringCchPrintfW(name, ARRAYSIZE(name),
|
|
L"%sLookingGlassClipboard-%08x-%016llx.tmp", path,
|
|
GetCurrentProcessId(), nonce)))
|
|
{
|
|
SetLastError(ERROR_INSUFFICIENT_BUFFER);
|
|
return false;
|
|
}
|
|
m_file = CreateFileW(name, GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
|
|
CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE |
|
|
FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
|
|
}
|
|
if (m_file == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
|
|
if (!m_memory.empty() && !WriteAll(m_file,
|
|
m_memory.data(), m_memory.size()))
|
|
{
|
|
const DWORD error = GetLastError();
|
|
CloseHandle(m_file);
|
|
m_file = INVALID_HANDLE_VALUE;
|
|
SetLastError(error ? error : ERROR_WRITE_FAULT);
|
|
return false;
|
|
}
|
|
std::vector<uint8_t>().swap(m_memory);
|
|
return true;
|
|
}
|
|
|
|
public:
|
|
~CClipboardSpool()
|
|
{
|
|
if (m_file != INVALID_HANDLE_VALUE)
|
|
CloseHandle(m_file);
|
|
}
|
|
|
|
bool Append(const void * data, size_t size)
|
|
{
|
|
if (!size)
|
|
return true;
|
|
if (size > MAX_SPOOL_BYTES || m_size > MAX_SPOOL_BYTES - size)
|
|
{
|
|
SetLastError(ERROR_FILE_TOO_LARGE);
|
|
return false;
|
|
}
|
|
const uint64_t nextSize = m_size + size;
|
|
|
|
if (m_file == INVALID_HANDLE_VALUE && nextSize <= MEMORY_SPOOL_LIMIT)
|
|
{
|
|
try
|
|
{
|
|
const uint8_t * bytes = static_cast<const uint8_t *>(data);
|
|
m_memory.insert(m_memory.end(), bytes, bytes + size);
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
SetLastError(ERROR_OUTOFMEMORY);
|
|
return false;
|
|
}
|
|
m_size = nextSize;
|
|
return true;
|
|
}
|
|
|
|
if (m_file == INVALID_HANDLE_VALUE && !Spill())
|
|
return false;
|
|
|
|
LARGE_INTEGER position = {};
|
|
position.QuadPart = m_size;
|
|
if (!SetFilePointerEx(m_file, position, nullptr, FILE_BEGIN) ||
|
|
!WriteAll(m_file, data, size))
|
|
return false;
|
|
m_size = nextSize;
|
|
return true;
|
|
}
|
|
|
|
bool Read(uint64_t offset, void * data, size_t size)
|
|
{
|
|
if (offset > m_size || size > m_size - offset)
|
|
{
|
|
SetLastError(ERROR_INVALID_DATA);
|
|
return false;
|
|
}
|
|
if (!size)
|
|
return true;
|
|
|
|
if (m_file == INVALID_HANDLE_VALUE)
|
|
{
|
|
memcpy(data, m_memory.data() + static_cast<size_t>(offset), size);
|
|
return true;
|
|
}
|
|
|
|
LARGE_INTEGER position = {};
|
|
position.QuadPart = offset;
|
|
return SetFilePointerEx(m_file, position, nullptr, FILE_BEGIN) &&
|
|
ReadAll(m_file, data, size);
|
|
}
|
|
|
|
uint64_t Size() const { return m_size; }
|
|
};
|
|
|
|
namespace
|
|
{
|
|
template<typename Callback>
|
|
bool ForEachUTF8(CClipboardSpool& spool, Callback callback)
|
|
{
|
|
uint32_t codepoint = 0;
|
|
uint32_t minimum = 0;
|
|
unsigned int remaining = 0;
|
|
std::vector<uint8_t> buffer;
|
|
try
|
|
{
|
|
buffer.resize(KVMFR_CLIPBOARD_DATA_BYTES);
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
SetLastError(ERROR_OUTOFMEMORY);
|
|
return false;
|
|
}
|
|
|
|
for (uint64_t offset = 0; offset < spool.Size();)
|
|
{
|
|
const size_t length = static_cast<size_t>((std::min<uint64_t>)(
|
|
buffer.size(), spool.Size() - offset));
|
|
if (!spool.Read(offset, buffer.data(), length))
|
|
return false;
|
|
offset += length;
|
|
|
|
for (size_t index = 0; index < length; ++index)
|
|
{
|
|
uint8_t value = buffer[index];
|
|
if (!remaining)
|
|
{
|
|
if (value < 0x80)
|
|
{
|
|
if (!callback(value))
|
|
return false;
|
|
}
|
|
else if (value >= 0xc2 && value <= 0xdf)
|
|
{
|
|
codepoint = value & 0x1f;
|
|
minimum = 0x80;
|
|
remaining = 1;
|
|
}
|
|
else if (value >= 0xe0 && value <= 0xef)
|
|
{
|
|
codepoint = value & 0x0f;
|
|
minimum = 0x800;
|
|
remaining = 2;
|
|
}
|
|
else if (value >= 0xf0 && value <= 0xf4)
|
|
{
|
|
codepoint = value & 0x07;
|
|
minimum = 0x10000;
|
|
remaining = 3;
|
|
}
|
|
else if (!callback(0xfffd))
|
|
return false;
|
|
continue;
|
|
}
|
|
|
|
if ((value & 0xc0) != 0x80)
|
|
{
|
|
if (!callback(0xfffd))
|
|
return false;
|
|
codepoint = 0;
|
|
minimum = 0;
|
|
remaining = 0;
|
|
--index;
|
|
continue;
|
|
}
|
|
|
|
codepoint = (codepoint << 6) | (value & 0x3f);
|
|
if (--remaining)
|
|
continue;
|
|
|
|
if (codepoint < minimum || codepoint > 0x10ffff ||
|
|
(codepoint >= 0xd800 && codepoint <= 0xdfff))
|
|
codepoint = 0xfffd;
|
|
if (!callback(codepoint))
|
|
return false;
|
|
codepoint = 0;
|
|
minimum = 0;
|
|
}
|
|
}
|
|
|
|
return !remaining || callback(0xfffd);
|
|
}
|
|
|
|
HGLOBAL UnicodeFromUTF8(CClipboardSpool& spool)
|
|
{
|
|
uint64_t units = 0;
|
|
uint32_t previous = 0;
|
|
SetLastError(ERROR_SUCCESS);
|
|
if (!ForEachUTF8(spool, [&units, &previous](uint32_t codepoint) {
|
|
if (codepoint == '\n' && previous != '\r')
|
|
++units;
|
|
units += codepoint > 0xffff ? 2U : 1U;
|
|
previous = codepoint;
|
|
return units < (std::numeric_limits<SIZE_T>::max)() /
|
|
sizeof(wchar_t);
|
|
}))
|
|
{
|
|
if (!GetLastError())
|
|
SetLastError(ERROR_FILE_TOO_LARGE);
|
|
return nullptr;
|
|
}
|
|
|
|
++units;
|
|
if (units > (std::numeric_limits<SIZE_T>::max)() / sizeof(wchar_t))
|
|
{
|
|
SetLastError(ERROR_FILE_TOO_LARGE);
|
|
return nullptr;
|
|
}
|
|
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE,
|
|
static_cast<SIZE_T>(units * sizeof(wchar_t)));
|
|
if (!memory)
|
|
return nullptr;
|
|
|
|
wchar_t * output = static_cast<wchar_t *>(GlobalLock(memory));
|
|
if (!output)
|
|
{
|
|
GlobalFree(memory);
|
|
return nullptr;
|
|
}
|
|
|
|
uint64_t index = 0;
|
|
previous = 0;
|
|
const bool valid = ForEachUTF8(spool,
|
|
[&output, &index, &previous](uint32_t codepoint) {
|
|
const uint32_t original = codepoint;
|
|
if (codepoint == '\n' && previous != '\r')
|
|
output[index++] = L'\r';
|
|
if (codepoint > 0xffff)
|
|
{
|
|
codepoint -= 0x10000;
|
|
output[index++] = static_cast<wchar_t>(0xd800 +
|
|
(codepoint >> 10));
|
|
output[index++] = static_cast<wchar_t>(0xdc00 +
|
|
(codepoint & 0x3ff));
|
|
}
|
|
else
|
|
output[index++] = static_cast<wchar_t>(codepoint);
|
|
previous = original;
|
|
return true;
|
|
});
|
|
if (valid)
|
|
output[index] = L'\0';
|
|
GlobalUnlock(memory);
|
|
if (!valid)
|
|
{
|
|
const DWORD error = GetLastError();
|
|
GlobalFree(memory);
|
|
SetLastError(error ? error : ERROR_INVALID_DATA);
|
|
return nullptr;
|
|
}
|
|
return memory;
|
|
}
|
|
|
|
HGLOBAL CopySpoolToGlobal(CClipboardSpool& spool, uint64_t offset)
|
|
{
|
|
if (offset > spool.Size())
|
|
{
|
|
SetLastError(ERROR_INVALID_DATA);
|
|
return nullptr;
|
|
}
|
|
const uint64_t length = spool.Size() - offset;
|
|
if (length > (std::numeric_limits<SIZE_T>::max)())
|
|
{
|
|
SetLastError(ERROR_FILE_TOO_LARGE);
|
|
return nullptr;
|
|
}
|
|
|
|
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE,
|
|
static_cast<SIZE_T>(length));
|
|
if (!memory)
|
|
return nullptr;
|
|
void * output = GlobalLock(memory);
|
|
if (!output || !spool.Read(offset, output, static_cast<size_t>(length)))
|
|
{
|
|
const DWORD error = GetLastError();
|
|
if (output)
|
|
GlobalUnlock(memory);
|
|
GlobalFree(memory);
|
|
SetLastError(error ? error : ERROR_READ_FAULT);
|
|
return nullptr;
|
|
}
|
|
GlobalUnlock(memory);
|
|
return memory;
|
|
}
|
|
}
|
|
|
|
CClipboardManager::IncomingTransfer::~IncomingTransfer()
|
|
{
|
|
if (event)
|
|
CloseHandle(event);
|
|
}
|
|
|
|
CClipboardManager::CClipboardManager(HWND hwnd,
|
|
CClipboardChannel& channel) : m_hwnd(hwnd), m_channel(channel)
|
|
{
|
|
}
|
|
|
|
CClipboardManager::~CClipboardManager()
|
|
{
|
|
Shutdown();
|
|
}
|
|
|
|
bool CClipboardManager::Initialize()
|
|
{
|
|
m_formatPNG = RegisterClipboardFormatW(L"PNG");
|
|
m_formatJPEG = RegisterClipboardFormatW(L"JFIF");
|
|
m_formatOrigin = RegisterClipboardFormatW(L"LookingGlassClipboardOrigin");
|
|
if (!m_formatPNG || !m_formatJPEG || !m_formatOrigin)
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "Failed to register clipboard formats");
|
|
return false;
|
|
}
|
|
|
|
Atomic::Store(m_shutdown, false);
|
|
|
|
m_stop = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
|
m_wake = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
|
if (!m_stop || !m_wake)
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "Failed to create clipboard events");
|
|
Shutdown();
|
|
return false;
|
|
}
|
|
|
|
m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
|
|
if (!m_thread)
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(), "Failed to create clipboard worker");
|
|
Shutdown();
|
|
return false;
|
|
}
|
|
|
|
if (!AddClipboardFormatListener(m_hwnd))
|
|
{
|
|
DEBUG_ERROR_HR(GetLastError(),
|
|
"Failed to register the clipboard listener");
|
|
Shutdown();
|
|
return false;
|
|
}
|
|
m_listener = true;
|
|
m_channel.SetHandler(this);
|
|
return true;
|
|
}
|
|
|
|
void CClipboardManager::Shutdown()
|
|
{
|
|
if (Atomic::Swap(m_shutdown, true))
|
|
return;
|
|
|
|
m_channel.ClearHandler(this);
|
|
if (m_listener && m_hwnd)
|
|
RemoveClipboardFormatListener(m_hwnd);
|
|
m_listener = false;
|
|
if (m_hwnd)
|
|
KillTimer(m_hwnd, REMOTE_RETRY_TIMER);
|
|
|
|
CancelIncoming(ERROR_OPERATION_ABORTED);
|
|
if (m_stop)
|
|
SetEvent(m_stop);
|
|
if (m_thread)
|
|
WaitForSingleObject(m_thread, INFINITE);
|
|
if (m_thread)
|
|
CloseHandle(m_thread);
|
|
if (m_wake)
|
|
CloseHandle(m_wake);
|
|
if (m_stop)
|
|
CloseHandle(m_stop);
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_workLock);
|
|
for (size_t i = 0; i < m_recordWorkCount; ++i)
|
|
m_recordWork[i].reset();
|
|
for (size_t i = 0; i < m_sendWorkCount; ++i)
|
|
m_sendWork[i].reset();
|
|
m_recordWorkCount = 0;
|
|
m_sendWorkCount = 0;
|
|
}
|
|
m_thread = nullptr;
|
|
m_wake = nullptr;
|
|
m_stop = nullptr;
|
|
}
|
|
|
|
DWORD WINAPI CClipboardManager::ThreadProc(void * context)
|
|
{
|
|
static_cast<CClipboardManager *>(context)->Thread();
|
|
return 0;
|
|
}
|
|
|
|
void CClipboardManager::Thread()
|
|
{
|
|
HANDLE events[] = { m_stop, m_wake };
|
|
for (;;)
|
|
{
|
|
const DWORD result = WaitForMultipleObjects(
|
|
ARRAYSIZE(events), events, FALSE, INFINITE);
|
|
if (result == WAIT_OBJECT_0)
|
|
return;
|
|
if (result != WAIT_OBJECT_0 + 1)
|
|
return;
|
|
|
|
for (;;)
|
|
{
|
|
Work work;
|
|
bool haveWork = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_workLock);
|
|
if (m_pendingControlCount)
|
|
{
|
|
work.type = m_pendingControl[0].type;
|
|
work.available = m_pendingControl[0].available;
|
|
work.epoch = m_pendingControl[0].epoch;
|
|
work.reason = m_pendingControl[0].reason;
|
|
for (size_t i = 1; i < m_pendingControlCount; ++i)
|
|
m_pendingControl[i - 1] = m_pendingControl[i];
|
|
--m_pendingControlCount;
|
|
haveWork = true;
|
|
}
|
|
if (!haveWork && m_recordWorkCount)
|
|
{
|
|
work = std::move(*m_recordWork[0]);
|
|
for (size_t i = 1; i < m_recordWorkCount; ++i)
|
|
m_recordWork[i - 1] = std::move(m_recordWork[i]);
|
|
m_recordWork[m_recordWorkCount - 1].reset();
|
|
--m_recordWorkCount;
|
|
haveWork = true;
|
|
}
|
|
if (!haveWork)
|
|
for (PendingCancel& pending : m_pendingCancel)
|
|
if (pending.valid)
|
|
{
|
|
work.type = WorkType::SEND;
|
|
work.record = pending.record;
|
|
work.deadline = pending.deadline;
|
|
pending.valid = false;
|
|
haveWork = true;
|
|
break;
|
|
}
|
|
if (!haveWork && m_sendWorkCount)
|
|
{
|
|
work = std::move(*m_sendWork[0]);
|
|
for (size_t i = 1; i < m_sendWorkCount; ++i)
|
|
m_sendWork[i - 1] = std::move(m_sendWork[i]);
|
|
m_sendWork[m_sendWorkCount - 1].reset();
|
|
--m_sendWorkCount;
|
|
haveWork = true;
|
|
}
|
|
}
|
|
if (!haveWork)
|
|
break;
|
|
ProcessWork(std::move(work));
|
|
if (WaitForSingleObject(m_stop, 0) == WAIT_OBJECT_0)
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CClipboardManager::QueueWork(Work&& work)
|
|
{
|
|
if (Atomic::Load(m_shutdown))
|
|
return false;
|
|
|
|
const bool publication = work.type == WorkType::SEND &&
|
|
(work.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
|
|
work.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR);
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_workLock);
|
|
// A BUSY publication is retried through this same path. Validate it while
|
|
// holding the queue lock so an older retry cannot erase a newer clipboard
|
|
// publication which became live before it entered the queue.
|
|
if (publication && work.record.clipboardGeneration != Atomic::Load(
|
|
m_liveLocalGeneration, std::memory_order_acquire))
|
|
return false;
|
|
|
|
std::array<std::optional<Work>, MAX_WORK>& queue =
|
|
work.type == WorkType::RECORD ?
|
|
m_recordWork : m_sendWork;
|
|
size_t& count = work.type == WorkType::RECORD ?
|
|
m_recordWorkCount : m_sendWorkCount;
|
|
auto erase = [&queue, &count](size_t index) {
|
|
for (size_t i = index + 1; i < count; ++i)
|
|
queue[i - 1] = std::move(queue[i]);
|
|
queue[count - 1].reset();
|
|
--count;
|
|
};
|
|
|
|
if (publication)
|
|
{
|
|
for (size_t i = 0; i < count;)
|
|
{
|
|
const Work& queued = *queue[i];
|
|
if (queued.type == WorkType::SEND &&
|
|
(queued.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
|
|
queued.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR))
|
|
erase(i);
|
|
else
|
|
++i;
|
|
}
|
|
}
|
|
|
|
if (count == MAX_WORK)
|
|
return false;
|
|
queue[count++].emplace(std::move(work));
|
|
}
|
|
SetEvent(m_wake);
|
|
return true;
|
|
}
|
|
|
|
bool CClipboardManager::QueueCancel(
|
|
const KVMFRClipboardMessage& record, uint32_t reason, uint64_t deadline)
|
|
{
|
|
KVMFRClipboardMessage cancel = record;
|
|
cancel.version = KVMFR_CLIPBOARD_VERSION;
|
|
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
|
|
cancel.token = reason;
|
|
cancel.length = 0;
|
|
cancel.flags = 0;
|
|
cancel.offset = 0;
|
|
cancel.size = 0;
|
|
cancel.sequence = 0;
|
|
if (!deadline)
|
|
deadline = GetTickCount64() + SEND_TIMEOUT_MS;
|
|
|
|
if (Atomic::Load(m_shutdown) || !cancel.transfer)
|
|
return false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_workLock);
|
|
for (size_t i = 0; i < m_sendWorkCount;)
|
|
{
|
|
const bool sameTransfer =
|
|
m_sendWork[i]->record.transfer == cancel.transfer;
|
|
if (sameTransfer &&
|
|
(m_sendWork[i]->type == WorkType::SEND_DATA ||
|
|
(m_sendWork[i]->type == WorkType::SEND &&
|
|
(m_sendWork[i]->record.type ==
|
|
KVMFR_CLIPBOARD_MESSAGE_REQUEST ||
|
|
m_sendWork[i]->record.type ==
|
|
KVMFR_CLIPBOARD_MESSAGE_CANCEL))))
|
|
{
|
|
for (size_t j = i + 1; j < m_sendWorkCount; ++j)
|
|
m_sendWork[j - 1] = std::move(m_sendWork[j]);
|
|
m_sendWork[m_sendWorkCount - 1].reset();
|
|
--m_sendWorkCount;
|
|
}
|
|
else
|
|
++i;
|
|
}
|
|
|
|
PendingCancel * free = nullptr;
|
|
for (PendingCancel& pending : m_pendingCancel)
|
|
{
|
|
if (pending.valid &&
|
|
pending.record.transfer == cancel.transfer)
|
|
{
|
|
pending.record = cancel;
|
|
pending.deadline = deadline;
|
|
SetEvent(m_wake);
|
|
return true;
|
|
}
|
|
if (!pending.valid && !free)
|
|
free = &pending;
|
|
}
|
|
if (!free)
|
|
{
|
|
// CANCELs are idempotent and bounded by their transfer ID. Under a
|
|
// pathological peer stall retain the newest cancellation instead of
|
|
// allocating or failing a producer-side error path.
|
|
free = &m_pendingCancel[
|
|
m_pendingCancelCursor++ % MAX_PENDING_CANCEL];
|
|
}
|
|
free->record = cancel;
|
|
free->deadline = deadline;
|
|
free->valid = true;
|
|
}
|
|
SetEvent(m_wake);
|
|
return true;
|
|
}
|
|
|
|
void CClipboardManager::QueueControl(WorkType type, bool available,
|
|
uint64_t epoch, uint32_t reason)
|
|
{
|
|
if (Atomic::Load(m_shutdown))
|
|
return;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_workLock);
|
|
if (m_pendingControlCount)
|
|
{
|
|
PendingControl& last = m_pendingControl[m_pendingControlCount - 1];
|
|
if (last.type == type &&
|
|
(type == WorkType::RESET || last.available == available))
|
|
{
|
|
last.available = available;
|
|
last.epoch = epoch;
|
|
last.reason = reason;
|
|
SetEvent(m_wake);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (m_pendingControlCount == MAX_PENDING_CONTROL)
|
|
{
|
|
// Collapse a pathologically noisy lifecycle into one reset. RESET's
|
|
// worker path republishes the channel's current state after teardown.
|
|
m_pendingControlCount = 1;
|
|
m_pendingControl[0].type = WorkType::RESET;
|
|
m_pendingControl[0].available = false;
|
|
m_pendingControl[0].epoch = epoch;
|
|
m_pendingControl[0].reason = reason ? reason :
|
|
ERROR_OPERATION_ABORTED;
|
|
}
|
|
else
|
|
{
|
|
PendingControl& pending = m_pendingControl[m_pendingControlCount++];
|
|
pending.type = type;
|
|
pending.available = available;
|
|
pending.epoch = epoch;
|
|
pending.reason = reason;
|
|
}
|
|
}
|
|
SetEvent(m_wake);
|
|
}
|
|
|
|
bool CClipboardManager::QueueUI(UIWork&& work)
|
|
{
|
|
if (Atomic::Load(m_shutdown))
|
|
return false;
|
|
|
|
std::array<KVMFRClipboardMessage, MAX_UI_WORK> dropped = {};
|
|
size_t droppedCount = 0;
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_uiLock);
|
|
auto erase = [this, &dropped, &droppedCount](size_t index) {
|
|
if (m_uiWork[index].type == UIType::REQUEST)
|
|
dropped[droppedCount++] = m_uiWork[index].record;
|
|
for (size_t i = index + 1; i < m_uiWorkCount; ++i)
|
|
m_uiWork[i - 1] = m_uiWork[i];
|
|
--m_uiWorkCount;
|
|
m_uiWork[m_uiWorkCount] = UIWork {};
|
|
};
|
|
|
|
if (work.type == UIType::STATE && !work.available)
|
|
while (m_uiWorkCount)
|
|
erase(m_uiWorkCount - 1);
|
|
else if (work.type == UIType::STATE)
|
|
{
|
|
for (size_t i = 0; i < m_uiWorkCount;)
|
|
{
|
|
if (m_uiWork[i].type == UIType::STATE &&
|
|
m_uiWork[i].available == work.available)
|
|
erase(i);
|
|
else
|
|
++i;
|
|
}
|
|
}
|
|
else if (work.type == UIType::OFFER || work.type == UIType::CLEAR)
|
|
{
|
|
for (size_t i = 0; i < m_uiWorkCount;)
|
|
{
|
|
if (m_uiWork[i].type == UIType::OFFER ||
|
|
m_uiWork[i].type == UIType::CLEAR)
|
|
erase(i);
|
|
else
|
|
++i;
|
|
}
|
|
}
|
|
|
|
if (m_uiWorkCount == MAX_UI_WORK)
|
|
{
|
|
if (work.type == UIType::REQUEST)
|
|
return false;
|
|
erase(0);
|
|
}
|
|
m_uiWork[m_uiWorkCount++] = work;
|
|
}
|
|
|
|
for (size_t i = 0; i < droppedCount; ++i)
|
|
QueueCancel(dropped[i], ERROR_BUSY);
|
|
if (PostMessageW(m_hwnd, WM_CLIPBOARD_WORK, 0, 0))
|
|
return true;
|
|
|
|
bool removed = false;
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lock(m_uiLock);
|
|
for (size_t i = m_uiWorkCount; i; --i)
|
|
{
|
|
UIWork& queued = m_uiWork[i - 1];
|
|
bool same = queued.type == work.type;
|
|
if (same && work.type == UIType::STATE)
|
|
same = queued.available == work.available &&
|
|
queued.epoch == work.epoch;
|
|
else if (same && (work.type == UIType::OFFER ||
|
|
work.type == UIType::CLEAR))
|
|
same = queued.record.clipboardGeneration ==
|
|
work.record.clipboardGeneration;
|
|
else if (same && work.type == UIType::REQUEST)
|
|
same = queued.record.transfer == work.record.transfer;
|
|
if (!same)
|
|
continue;
|
|
|
|
for (size_t j = i; j < m_uiWorkCount; ++j)
|
|
m_uiWork[j - 1] = m_uiWork[j];
|
|
--m_uiWorkCount;
|
|
m_uiWork[m_uiWorkCount] = UIWork {};
|
|
removed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// A previously posted drain may already have consumed the record.
|
|
return !removed;
|
|
}
|
|
|
|
void CClipboardManager::ProcessWork(Work&& work)
|
|
{
|
|
switch (work.type)
|
|
{
|
|
case WorkType::STATE:
|
|
{
|
|
if (!work.available)
|
|
{
|
|
Atomic::Store(m_outgoingTransfer, 0, std::memory_order_release);
|
|
CancelIncoming(ERROR_DEVICE_NOT_CONNECTED);
|
|
}
|
|
UIWork ui;
|
|
ui.type = UIType::STATE;
|
|
ui.available = work.available;
|
|
ui.epoch = work.epoch;
|
|
QueueUI(std::move(ui));
|
|
break;
|
|
}
|
|
|
|
case WorkType::RESET:
|
|
{
|
|
Atomic::Store(m_outgoingTransfer, 0, std::memory_order_release);
|
|
CancelIncoming(work.reason ? work.reason : ERROR_OPERATION_ABORTED);
|
|
UIWork ui;
|
|
ui.type = UIType::STATE;
|
|
ui.available = false;
|
|
ui.epoch = work.epoch;
|
|
QueueUI(std::move(ui));
|
|
if (m_channel.Available())
|
|
{
|
|
UIWork resume;
|
|
resume.type = UIType::STATE;
|
|
resume.available = true;
|
|
resume.epoch = m_channel.Epoch();
|
|
QueueUI(std::move(resume));
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WorkType::RECORD:
|
|
ProcessRecord(work.record,
|
|
work.data.empty() ? nullptr : work.data.data());
|
|
break;
|
|
|
|
case WorkType::SEND:
|
|
ProcessSend(std::move(work));
|
|
break;
|
|
|
|
case WorkType::SEND_DATA:
|
|
ProcessSendData(std::move(work));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CClipboardManager::ProcessRecord(
|
|
const KVMFRClipboardMessage& record, const uint8_t * data)
|
|
{
|
|
switch (record.type)
|
|
{
|
|
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
|
|
{
|
|
CancelIncoming(ERROR_OPERATION_ABORTED);
|
|
InvalidateOutgoing(ERROR_OPERATION_ABORTED);
|
|
UIWork ui;
|
|
ui.type = UIType::OFFER;
|
|
ui.record = record;
|
|
if (!QueueUI(std::move(ui)))
|
|
DEBUG_WARN("Failed to queue remote clipboard offer");
|
|
break;
|
|
}
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
|
|
{
|
|
CancelIncoming(ERROR_OPERATION_ABORTED);
|
|
InvalidateOutgoing(ERROR_OPERATION_ABORTED);
|
|
UIWork ui;
|
|
ui.type = UIType::CLEAR;
|
|
ui.record = record;
|
|
if (!QueueUI(std::move(ui)))
|
|
DEBUG_WARN("Failed to queue remote clipboard clear");
|
|
break;
|
|
}
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
|
|
{
|
|
UIWork ui;
|
|
ui.type = UIType::REQUEST;
|
|
ui.record = record;
|
|
if (!QueueUI(std::move(ui)))
|
|
QueueCancel(record, ERROR_BUSY);
|
|
break;
|
|
}
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_DATA:
|
|
ProcessData(record, data);
|
|
break;
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
|
|
{
|
|
ReleaseOutgoing(record.transfer);
|
|
CancelIncoming(record.token ? record.token : ERROR_OPERATION_ABORTED,
|
|
record.transfer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CClipboardManager::ProcessData(
|
|
const KVMFRClipboardMessage& record, const uint8_t * data)
|
|
{
|
|
bool cancel = false;
|
|
uint32_t cancelReason = ERROR_INVALID_DATA;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_transferLock);
|
|
const std::shared_ptr<IncomingTransfer> transfer = m_incoming;
|
|
if (!transfer || transfer->complete ||
|
|
transfer->transfer != record.transfer ||
|
|
transfer->generation != record.clipboardGeneration ||
|
|
transfer->format != record.format)
|
|
cancel = true;
|
|
else
|
|
{
|
|
bool valid = record.offset == transfer->nextOffset &&
|
|
record.sequence == transfer->nextSequence;
|
|
if (!transfer->began)
|
|
{
|
|
valid = valid && record.offset == 0 &&
|
|
(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN);
|
|
transfer->began = true;
|
|
transfer->sizeHint = record.size;
|
|
if (record.size != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
|
|
record.size > MAX_SPOOL_BYTES)
|
|
{
|
|
transfer->error = ERROR_FILE_TOO_LARGE;
|
|
valid = false;
|
|
}
|
|
}
|
|
else if (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN)
|
|
valid = false;
|
|
|
|
if (valid && record.length &&
|
|
!transfer->spool->Append(data, record.length))
|
|
{
|
|
const DWORD error = GetLastError();
|
|
transfer->error = error ? error : ERROR_DISK_FULL;
|
|
valid = false;
|
|
}
|
|
|
|
if (valid)
|
|
{
|
|
transfer->nextOffset += record.length;
|
|
++transfer->nextSequence;
|
|
if (record.flags & KVMFR_CLIPBOARD_FLAG_END)
|
|
{
|
|
valid = record.size == transfer->nextOffset &&
|
|
(transfer->sizeHint == KVMFR_CLIPBOARD_SIZE_UNKNOWN ||
|
|
transfer->sizeHint == transfer->nextOffset);
|
|
if (valid)
|
|
{
|
|
transfer->complete = true;
|
|
transfer->error = ERROR_SUCCESS;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!valid)
|
|
{
|
|
transfer->complete = true;
|
|
if (transfer->error == ERROR_SUCCESS)
|
|
transfer->error = ERROR_INVALID_DATA;
|
|
cancel = true;
|
|
cancelReason = transfer->error;
|
|
}
|
|
if (transfer->complete)
|
|
SetEvent(transfer->event);
|
|
}
|
|
}
|
|
|
|
if (cancel)
|
|
QueueCancel(record, cancelReason);
|
|
}
|
|
|
|
void CClipboardManager::ProcessSend(Work&& work)
|
|
{
|
|
if (!work.deadline)
|
|
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
|
|
|
|
const bool publication =
|
|
work.record.type == KVMFR_CLIPBOARD_MESSAGE_OFFER ||
|
|
work.record.type == KVMFR_CLIPBOARD_MESSAGE_CLEAR;
|
|
ClipboardChannelResult result;
|
|
if (publication)
|
|
{
|
|
// Serialize the final generation check with invalidation. Once a remote
|
|
// offer has invalidated this generation, no queued retry may enter the
|
|
// channel after that invalidation point.
|
|
std::lock_guard<std::mutex> lock(m_outgoingLock);
|
|
if (work.record.clipboardGeneration != Atomic::Load(
|
|
m_liveLocalGeneration, std::memory_order_acquire))
|
|
return;
|
|
result = m_channel.Send(work.record, nullptr);
|
|
}
|
|
else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_transferLock);
|
|
if (!m_incoming || m_incoming->complete ||
|
|
m_incoming->transfer != work.record.transfer ||
|
|
m_incoming->generation != work.record.clipboardGeneration ||
|
|
m_incoming->format != work.record.format)
|
|
return;
|
|
if (GetTickCount64() >= work.deadline)
|
|
{
|
|
m_incoming->complete = true;
|
|
m_incoming->error = ERROR_TIMEOUT;
|
|
SetEvent(m_incoming->event);
|
|
return;
|
|
}
|
|
result = m_channel.Send(work.record, nullptr);
|
|
}
|
|
else
|
|
result = m_channel.Send(work.record, nullptr);
|
|
if (result == ClipboardChannelResult::ACCEPTED)
|
|
return;
|
|
|
|
const KVMFRClipboardMessageType publicationType = work.record.type;
|
|
const uint64_t publicationGeneration = work.record.clipboardGeneration;
|
|
auto failLocalPublication = [this, publicationType,
|
|
publicationGeneration]() {
|
|
if (publicationType != KVMFR_CLIPBOARD_MESSAGE_OFFER &&
|
|
publicationType != KVMFR_CLIPBOARD_MESSAGE_CLEAR)
|
|
return;
|
|
uint64_t generation = publicationGeneration;
|
|
if (Atomic::CAS(m_liveLocalGeneration, generation, UINT64_C(0),
|
|
std::memory_order_acq_rel))
|
|
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
|
|
};
|
|
|
|
if (result == ClipboardChannelResult::BUSY &&
|
|
GetTickCount64() < work.deadline &&
|
|
WaitForSingleObject(m_stop, CHANNEL_RETRY_MS) != WAIT_OBJECT_0)
|
|
{
|
|
if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_CANCEL)
|
|
QueueCancel(work.record, work.record.token, work.deadline);
|
|
else if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
|
|
{
|
|
const KVMFRClipboardMessage record = work.record;
|
|
bool live = false;
|
|
bool queued = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_transferLock);
|
|
live = m_incoming && !m_incoming->complete &&
|
|
m_incoming->transfer == record.transfer &&
|
|
m_incoming->generation == record.clipboardGeneration &&
|
|
m_incoming->format == record.format;
|
|
if (live)
|
|
queued = QueueWork(std::move(work));
|
|
}
|
|
if (live && !queued)
|
|
CancelIncoming(ERROR_BUSY, record.transfer);
|
|
}
|
|
else if (!QueueWork(std::move(work)))
|
|
failLocalPublication();
|
|
return;
|
|
}
|
|
|
|
if (work.record.type == KVMFR_CLIPBOARD_MESSAGE_REQUEST)
|
|
{
|
|
const uint32_t reason = result == ClipboardChannelResult::BUSY ?
|
|
ERROR_TIMEOUT : ERROR_DEVICE_NOT_CONNECTED;
|
|
CancelIncoming(reason, work.record.transfer);
|
|
}
|
|
else
|
|
failLocalPublication();
|
|
}
|
|
|
|
void CClipboardManager::ProcessSendData(Work&& work)
|
|
{
|
|
if (!work.spool)
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, ERROR_INVALID_DATA);
|
|
return;
|
|
}
|
|
|
|
if (Atomic::Load(m_outgoingTransfer, std::memory_order_acquire) !=
|
|
work.record.transfer)
|
|
return;
|
|
if (!work.deadline)
|
|
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
|
|
if (GetTickCount64() >= work.deadline)
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, ERROR_TIMEOUT);
|
|
return;
|
|
}
|
|
|
|
if (work.record.clipboardGeneration !=
|
|
Atomic::Load(m_liveLocalGeneration, std::memory_order_acquire))
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, ERROR_OPERATION_ABORTED);
|
|
return;
|
|
}
|
|
|
|
const uint64_t total = work.spool->Size();
|
|
if (work.record.offset > total)
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, ERROR_INVALID_DATA);
|
|
return;
|
|
}
|
|
const size_t length = static_cast<size_t>((std::min<uint64_t>)(
|
|
KVMFR_CLIPBOARD_DATA_BYTES, total - work.record.offset));
|
|
std::vector<uint8_t> data;
|
|
if (length)
|
|
{
|
|
try
|
|
{
|
|
data.resize(length);
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, ERROR_OUTOFMEMORY);
|
|
return;
|
|
}
|
|
if (!work.spool->Read(work.record.offset, data.data(), length))
|
|
{
|
|
const DWORD error = GetLastError();
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, error ? error : ERROR_READ_FAULT);
|
|
return;
|
|
}
|
|
}
|
|
|
|
KVMFRClipboardMessage message = work.record;
|
|
message.type = KVMFR_CLIPBOARD_MESSAGE_DATA;
|
|
message.token = 0;
|
|
message.length = static_cast<uint32_t>(length);
|
|
message.flags = 0;
|
|
if (!message.offset)
|
|
message.flags |= KVMFR_CLIPBOARD_FLAG_BEGIN;
|
|
if (message.offset + length == total)
|
|
message.flags |= KVMFR_CLIPBOARD_FLAG_END;
|
|
message.size = message.flags & KVMFR_CLIPBOARD_FLAG_END ? total :
|
|
(message.flags & KVMFR_CLIPBOARD_FLAG_BEGIN ? total :
|
|
KVMFR_CLIPBOARD_SIZE_UNKNOWN);
|
|
|
|
ClipboardChannelResult result = ClipboardChannelResult::FAILED;
|
|
bool stale = false;
|
|
bool timedOut = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_outgoingLock);
|
|
stale = Atomic::Load(m_outgoingTransfer, std::memory_order_acquire) !=
|
|
message.transfer ||
|
|
message.clipboardGeneration != Atomic::Load(
|
|
m_liveLocalGeneration, std::memory_order_acquire);
|
|
timedOut = GetTickCount64() >= work.deadline;
|
|
if (!stale && !timedOut)
|
|
result = m_channel.Send(message,
|
|
data.empty() ? nullptr : data.data());
|
|
}
|
|
if (stale)
|
|
{
|
|
ReleaseOutgoing(message.transfer);
|
|
QueueCancel(message, ERROR_OPERATION_ABORTED);
|
|
return;
|
|
}
|
|
if (timedOut)
|
|
{
|
|
ReleaseOutgoing(message.transfer);
|
|
QueueCancel(message, ERROR_TIMEOUT);
|
|
return;
|
|
}
|
|
if (result == ClipboardChannelResult::BUSY)
|
|
{
|
|
if (GetTickCount64() < work.deadline &&
|
|
WaitForSingleObject(m_stop, CHANNEL_RETRY_MS) != WAIT_OBJECT_0)
|
|
{
|
|
const KVMFRClipboardMessage record = work.record;
|
|
if (!QueueWork(std::move(work)))
|
|
{
|
|
ReleaseOutgoing(record.transfer);
|
|
QueueCancel(record, ERROR_BUSY);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, ERROR_TIMEOUT);
|
|
}
|
|
return;
|
|
}
|
|
if (result != ClipboardChannelResult::ACCEPTED)
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
QueueCancel(work.record, ERROR_DEVICE_NOT_CONNECTED);
|
|
return;
|
|
}
|
|
work.deadline = GetTickCount64() + SEND_TIMEOUT_MS;
|
|
if (message.flags & KVMFR_CLIPBOARD_FLAG_END)
|
|
{
|
|
ReleaseOutgoing(work.record.transfer);
|
|
return;
|
|
}
|
|
|
|
work.record.offset += length;
|
|
++work.record.sequence;
|
|
const KVMFRClipboardMessage record = work.record;
|
|
if (!QueueWork(std::move(work)))
|
|
{
|
|
ReleaseOutgoing(record.transfer);
|
|
QueueCancel(record, ERROR_BUSY);
|
|
}
|
|
}
|
|
|
|
void CClipboardManager::CancelIncoming(uint32_t reason, uint64_t transferID)
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_transferLock);
|
|
const std::shared_ptr<IncomingTransfer> transfer = m_incoming;
|
|
if (!transfer || (transferID && transferID != transfer->transfer))
|
|
return;
|
|
transfer->complete = true;
|
|
transfer->error = reason;
|
|
SetEvent(transfer->event);
|
|
}
|
|
|
|
void CClipboardManager::ReleaseOutgoing(uint64_t transfer)
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_outgoingLock);
|
|
Atomic::CAS(m_outgoingTransfer, transfer, UINT64_C(0),
|
|
std::memory_order_acq_rel);
|
|
}
|
|
|
|
void CClipboardManager::ClipboardState(bool available, uint64_t epoch)
|
|
{
|
|
QueueControl(WorkType::STATE, available, epoch, 0);
|
|
}
|
|
|
|
ClipboardChannelResult CClipboardManager::ClipboardRecord(
|
|
const KVMFRClipboardMessage& record, const uint8_t * data)
|
|
{
|
|
switch (record.type)
|
|
{
|
|
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
|
|
if (!record.clipboardGeneration || !record.token ||
|
|
(record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) ||
|
|
record.format || record.transfer || record.length || record.flags ||
|
|
record.offset || record.size || record.sequence)
|
|
return ClipboardChannelResult::FAILED;
|
|
break;
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
|
|
if (!record.clipboardGeneration || record.format || record.transfer ||
|
|
record.length || record.flags || record.offset || record.size ||
|
|
record.sequence || record.token)
|
|
return ClipboardChannelResult::FAILED;
|
|
break;
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
|
|
if (!record.clipboardGeneration || !record.transfer ||
|
|
!kvmfrClipboardTransferFromClient(record.transfer) ||
|
|
!kvmfrClipboardFormatValid(record.format) || record.length ||
|
|
record.flags || record.offset || record.size || record.sequence ||
|
|
record.token)
|
|
return ClipboardChannelResult::FAILED;
|
|
break;
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_DATA:
|
|
if (!record.clipboardGeneration || !record.transfer ||
|
|
!kvmfrClipboardTransferFromHelper(record.transfer) ||
|
|
!kvmfrClipboardFormatValid(record.format) ||
|
|
(record.length && !data))
|
|
return ClipboardChannelResult::FAILED;
|
|
break;
|
|
|
|
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
|
|
if (!record.transfer || record.length || record.flags)
|
|
return ClipboardChannelResult::FAILED;
|
|
break;
|
|
default:
|
|
return ClipboardChannelResult::FAILED;
|
|
}
|
|
|
|
Work work;
|
|
work.type = WorkType::RECORD;
|
|
work.record = record;
|
|
if (record.length)
|
|
{
|
|
try
|
|
{
|
|
work.data.assign(data, data + record.length);
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
return ClipboardChannelResult::BUSY;
|
|
}
|
|
}
|
|
if (QueueWork(std::move(work)))
|
|
return ClipboardChannelResult::ACCEPTED;
|
|
return Atomic::Load(m_shutdown) ? ClipboardChannelResult::FAILED :
|
|
ClipboardChannelResult::BUSY;
|
|
}
|
|
|
|
void CClipboardManager::ClipboardReset(uint64_t epoch, uint32_t reason)
|
|
{
|
|
QueueControl(WorkType::RESET, false, epoch, reason);
|
|
}
|
|
|
|
bool CClipboardManager::HandleMessage(UINT message, WPARAM wParam,
|
|
LPARAM, LRESULT& result)
|
|
{
|
|
switch (message)
|
|
{
|
|
case WM_CLIPBOARD_WORK:
|
|
DrainUI();
|
|
result = 0;
|
|
return true;
|
|
|
|
case WM_CLIPBOARDUPDATE:
|
|
HandleClipboardUpdate();
|
|
result = 0;
|
|
return true;
|
|
|
|
case WM_RENDERFORMAT:
|
|
RenderFormat(static_cast<UINT>(wParam));
|
|
result = 0;
|
|
return true;
|
|
|
|
case WM_RENDERALLFORMATS:
|
|
RenderAllFormats();
|
|
result = 0;
|
|
return true;
|
|
|
|
case WM_TIMER:
|
|
if (wParam == REMOTE_RETRY_TIMER)
|
|
{
|
|
RetryRemoteOffer();
|
|
result = 0;
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case WM_DESTROYCLIPBOARD:
|
|
HandleDestroyClipboard();
|
|
result = 0;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CClipboardManager::DrainUI()
|
|
{
|
|
std::lock_guard<std::recursive_mutex> dispatchLock(m_uiLock);
|
|
for (;;)
|
|
{
|
|
UIWork work;
|
|
if (!m_uiWorkCount)
|
|
return;
|
|
work = m_uiWork[0];
|
|
for (size_t i = 1; i < m_uiWorkCount; ++i)
|
|
m_uiWork[i - 1] = m_uiWork[i];
|
|
--m_uiWorkCount;
|
|
m_uiWork[m_uiWorkCount] = UIWork {};
|
|
|
|
switch (work.type)
|
|
{
|
|
case UIType::STATE:
|
|
HandleState(work.available, work.epoch);
|
|
break;
|
|
case UIType::OFFER:
|
|
HandleOffer(work.record);
|
|
break;
|
|
case UIType::CLEAR:
|
|
HandleClear(work.record);
|
|
break;
|
|
case UIType::REQUEST:
|
|
HandleRequest(work.record);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CClipboardManager::HandleState(bool available, uint64_t epoch)
|
|
{
|
|
m_available = available;
|
|
m_epoch = available ? epoch : 0;
|
|
if (!available)
|
|
{
|
|
ClearRemoteRetry();
|
|
InvalidateLocalClipboard(ERROR_DEVICE_NOT_CONNECTED);
|
|
ClearOwnedClipboard();
|
|
m_remoteControlGeneration = 0;
|
|
return;
|
|
}
|
|
|
|
PublishLocalClipboard();
|
|
}
|
|
|
|
void CClipboardManager::HandleOffer(
|
|
const KVMFRClipboardMessage& record)
|
|
{
|
|
if (!m_available || !record.clipboardGeneration ||
|
|
!record.token || (record.token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL))
|
|
return;
|
|
if (m_remoteControlGeneration &&
|
|
record.clipboardGeneration < m_remoteControlGeneration)
|
|
return;
|
|
|
|
const bool newOffer =
|
|
m_pendingRemoteOffer.clipboardGeneration != record.clipboardGeneration;
|
|
if (newOffer)
|
|
{
|
|
ClearRemoteRetry();
|
|
m_remoteControlGeneration = record.clipboardGeneration;
|
|
m_pendingRemoteOffer = record;
|
|
m_remoteRetryDeadline = GetTickCount64() + REMOTE_RETRY_TIMEOUT_MS;
|
|
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
|
|
}
|
|
|
|
if (ApplyRemoteOffer(record.token, record.clipboardGeneration))
|
|
{
|
|
ClearRemoteRetry();
|
|
return;
|
|
}
|
|
|
|
if (GetTickCount64() < m_remoteRetryDeadline &&
|
|
SetTimer(m_hwnd, REMOTE_RETRY_TIMER, REMOTE_RETRY_MS, nullptr))
|
|
return;
|
|
|
|
DEBUG_WARN("Failed to apply remote clipboard offer");
|
|
ClearRemoteRetry();
|
|
PublishLocalClipboard();
|
|
}
|
|
|
|
void CClipboardManager::HandleClear(
|
|
const KVMFRClipboardMessage& record)
|
|
{
|
|
if (record.clipboardGeneration && m_remoteControlGeneration &&
|
|
record.clipboardGeneration < m_remoteControlGeneration)
|
|
return;
|
|
ClearRemoteRetry();
|
|
m_remoteControlGeneration = record.clipboardGeneration;
|
|
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
|
|
ClearOwnedClipboard();
|
|
}
|
|
|
|
void CClipboardManager::HandleRequest(
|
|
const KVMFRClipboardMessage& record)
|
|
{
|
|
if (!m_available || !record.transfer ||
|
|
!kvmfrClipboardFormatValid(record.format) ||
|
|
record.clipboardGeneration != m_localGeneration ||
|
|
record.clipboardGeneration != Atomic::Load(
|
|
m_liveLocalGeneration, std::memory_order_acquire) ||
|
|
GetClipboardSequenceNumber() != m_localSequence)
|
|
{
|
|
QueueCancel(record, ERROR_NOT_FOUND);
|
|
return;
|
|
}
|
|
|
|
uint64_t noTransfer = 0;
|
|
if (!Atomic::CAS(m_outgoingTransfer, noTransfer,
|
|
record.transfer, std::memory_order_acq_rel))
|
|
{
|
|
QueueCancel(record, ERROR_BUSY);
|
|
return;
|
|
}
|
|
|
|
std::shared_ptr<CClipboardSpool> spool =
|
|
CaptureFormat(record.format, m_localSequence);
|
|
if (!spool)
|
|
{
|
|
const DWORD error = GetLastError();
|
|
ReleaseOutgoing(record.transfer);
|
|
QueueCancel(record, error ? error : ERROR_NOT_FOUND);
|
|
return;
|
|
}
|
|
|
|
Work data;
|
|
data.type = WorkType::SEND_DATA;
|
|
data.record = record;
|
|
data.record.version = KVMFR_CLIPBOARD_VERSION;
|
|
data.record.type = KVMFR_CLIPBOARD_MESSAGE_DATA;
|
|
data.record.sequence = 0;
|
|
data.record.offset = 0;
|
|
data.record.size = 0;
|
|
data.record.token = 0;
|
|
data.record.length = 0;
|
|
data.record.flags = 0;
|
|
data.spool = std::move(spool);
|
|
if (!QueueWork(std::move(data)))
|
|
{
|
|
ReleaseOutgoing(record.transfer);
|
|
QueueCancel(record, ERROR_BUSY);
|
|
}
|
|
}
|
|
|
|
bool CClipboardManager::OpenClipboardRetry() const
|
|
{
|
|
for (unsigned int attempt = 0; attempt != 8; ++attempt)
|
|
{
|
|
if (OpenClipboard(m_hwnd))
|
|
return true;
|
|
Sleep(5U << (std::min)(attempt, 5U));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CClipboardManager::IsOurClipboard()
|
|
{
|
|
if (GetClipboardOwner() == m_hwnd)
|
|
{
|
|
m_ownedSequence = GetClipboardSequenceNumber();
|
|
return true;
|
|
}
|
|
|
|
if (!m_formatOrigin || !m_remoteGeneration ||
|
|
!IsClipboardFormatAvailable(m_formatOrigin) || !OpenClipboardRetry())
|
|
return false;
|
|
|
|
bool ours = false;
|
|
HANDLE data = GetClipboardData(m_formatOrigin);
|
|
if (data && GlobalSize(data) >= sizeof(ClipboardOrigin))
|
|
{
|
|
const ClipboardOrigin * origin =
|
|
static_cast<const ClipboardOrigin *>(GlobalLock(data));
|
|
if (origin)
|
|
{
|
|
ours = origin->magic == ORIGIN_MAGIC && origin->epoch == m_epoch &&
|
|
origin->generation == m_remoteGeneration;
|
|
GlobalUnlock(data);
|
|
}
|
|
}
|
|
CloseClipboard();
|
|
return ours;
|
|
}
|
|
|
|
uint32_t CClipboardManager::EnumerateFormats() const
|
|
{
|
|
uint32_t formats = 0;
|
|
if (IsClipboardFormatAvailable(CF_UNICODETEXT))
|
|
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_TEXT;
|
|
if (m_formatPNG && IsClipboardFormatAvailable(m_formatPNG))
|
|
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_PNG;
|
|
if (IsClipboardFormatAvailable(CF_DIBV5) ||
|
|
IsClipboardFormatAvailable(CF_DIB))
|
|
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_BMP;
|
|
if (IsClipboardFormatAvailable(CF_TIFF))
|
|
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_TIFF;
|
|
if (m_formatJPEG && IsClipboardFormatAvailable(m_formatJPEG))
|
|
formats |= KVMFR_CLIPBOARD_FORMAT_MASK_JPEG;
|
|
return formats;
|
|
}
|
|
|
|
void CClipboardManager::HandleClipboardUpdate()
|
|
{
|
|
if (m_applyingRemote || IsOurClipboard())
|
|
return;
|
|
|
|
ClearRemoteRetry();
|
|
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
|
|
m_remoteGeneration = 0;
|
|
m_remoteFormats = 0;
|
|
m_ownedSequence = 0;
|
|
CancelIncoming(ERROR_OPERATION_ABORTED);
|
|
PublishLocalClipboard();
|
|
}
|
|
|
|
void CClipboardManager::HandleDestroyClipboard()
|
|
{
|
|
if (m_applyingRemote)
|
|
return;
|
|
ClearRemoteRetry();
|
|
InvalidateLocalClipboard(ERROR_OPERATION_ABORTED);
|
|
m_remoteGeneration = 0;
|
|
m_remoteFormats = 0;
|
|
m_ownedSequence = 0;
|
|
CancelIncoming(ERROR_OPERATION_ABORTED);
|
|
}
|
|
|
|
void CClipboardManager::PublishLocalClipboard()
|
|
{
|
|
if (!m_available)
|
|
{
|
|
Atomic::Store(m_liveLocalGeneration, UINT64_C(0),
|
|
std::memory_order_release);
|
|
m_localSequence = GetClipboardSequenceNumber();
|
|
return;
|
|
}
|
|
|
|
const DWORD before = GetClipboardSequenceNumber();
|
|
const uint32_t formats = EnumerateFormats();
|
|
const DWORD after = GetClipboardSequenceNumber();
|
|
if (before != after)
|
|
{
|
|
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
|
|
return;
|
|
}
|
|
|
|
uint64_t generation = ++m_localGeneration;
|
|
if (!generation)
|
|
generation = ++m_localGeneration;
|
|
m_localSequence = after;
|
|
if (!formats)
|
|
{
|
|
PublishClear(generation);
|
|
return;
|
|
}
|
|
|
|
Work work;
|
|
work.type = WorkType::SEND;
|
|
work.record.version = KVMFR_CLIPBOARD_VERSION;
|
|
work.record.type = KVMFR_CLIPBOARD_MESSAGE_OFFER;
|
|
work.record.clipboardGeneration = generation;
|
|
work.record.token = formats;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_outgoingLock);
|
|
Atomic::Store(m_liveLocalGeneration, generation,
|
|
std::memory_order_release);
|
|
}
|
|
if (!QueueWork(std::move(work)))
|
|
{
|
|
uint64_t live = generation;
|
|
Atomic::CAS(m_liveLocalGeneration, live, UINT64_C(0),
|
|
std::memory_order_acq_rel);
|
|
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
|
|
}
|
|
}
|
|
|
|
void CClipboardManager::PublishClear(uint64_t generation)
|
|
{
|
|
Work work;
|
|
work.type = WorkType::SEND;
|
|
work.record.version = KVMFR_CLIPBOARD_VERSION;
|
|
work.record.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR;
|
|
work.record.clipboardGeneration = generation;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_outgoingLock);
|
|
Atomic::Store(m_liveLocalGeneration, generation,
|
|
std::memory_order_release);
|
|
}
|
|
if (!QueueWork(std::move(work)))
|
|
{
|
|
uint64_t live = generation;
|
|
Atomic::CAS(m_liveLocalGeneration, live, UINT64_C(0),
|
|
std::memory_order_acq_rel);
|
|
PostMessageW(m_hwnd, WM_CLIPBOARDUPDATE, 0, 0);
|
|
}
|
|
}
|
|
|
|
void CClipboardManager::InvalidateOutgoing(uint32_t reason)
|
|
{
|
|
uint64_t generation;
|
|
uint64_t transfer;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_outgoingLock);
|
|
generation = Atomic::Swap(m_liveLocalGeneration,
|
|
UINT64_C(0), std::memory_order_acq_rel);
|
|
transfer = Atomic::Swap(
|
|
m_outgoingTransfer, UINT64_C(0), std::memory_order_acq_rel);
|
|
}
|
|
if (!transfer)
|
|
return;
|
|
|
|
KVMFRClipboardMessage cancel = {};
|
|
cancel.clipboardGeneration = generation;
|
|
cancel.transfer = transfer;
|
|
QueueCancel(cancel, reason);
|
|
}
|
|
|
|
void CClipboardManager::InvalidateLocalClipboard(uint32_t reason)
|
|
{
|
|
m_localSequence = 0;
|
|
InvalidateOutgoing(reason);
|
|
}
|
|
|
|
void CClipboardManager::ClearRemoteRetry()
|
|
{
|
|
if (m_hwnd)
|
|
KillTimer(m_hwnd, REMOTE_RETRY_TIMER);
|
|
m_pendingRemoteOffer = {};
|
|
m_remoteRetryDeadline = 0;
|
|
}
|
|
|
|
void CClipboardManager::RetryRemoteOffer()
|
|
{
|
|
KillTimer(m_hwnd, REMOTE_RETRY_TIMER);
|
|
if (!m_pendingRemoteOffer.clipboardGeneration || !m_available)
|
|
return;
|
|
const KVMFRClipboardMessage offer = m_pendingRemoteOffer;
|
|
HandleOffer(offer);
|
|
}
|
|
|
|
bool CClipboardManager::SetOriginMarker(uint64_t generation) const
|
|
{
|
|
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(ClipboardOrigin));
|
|
if (!memory)
|
|
return false;
|
|
ClipboardOrigin * origin =
|
|
static_cast<ClipboardOrigin *>(GlobalLock(memory));
|
|
if (!origin)
|
|
{
|
|
const DWORD error = GetLastError();
|
|
GlobalFree(memory);
|
|
SetLastError(error ? error : ERROR_NOT_ENOUGH_MEMORY);
|
|
return false;
|
|
}
|
|
*origin = { ORIGIN_MAGIC, 0, m_epoch, generation };
|
|
GlobalUnlock(memory);
|
|
if (!SetClipboardData(m_formatOrigin, memory))
|
|
{
|
|
const DWORD error = GetLastError();
|
|
GlobalFree(memory);
|
|
SetLastError(error ? error : ERROR_INVALID_DATA);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CClipboardManager::ApplyRemoteOffer(uint32_t formats,
|
|
uint64_t generation)
|
|
{
|
|
if (!OpenClipboardRetry())
|
|
{
|
|
DEBUG_WARN_HR(GetLastError(), "Failed to open the clipboard");
|
|
return false;
|
|
}
|
|
|
|
m_applyingRemote = true;
|
|
const bool emptied = EmptyClipboard() != FALSE;
|
|
DWORD error = emptied ? ERROR_SUCCESS : GetLastError();
|
|
if (emptied)
|
|
{
|
|
auto setDelayed = [&error](UINT format) {
|
|
if (!format)
|
|
{
|
|
if (!error)
|
|
error = ERROR_INVALID_DATA;
|
|
return false;
|
|
}
|
|
SetLastError(ERROR_SUCCESS);
|
|
const HANDLE result = SetClipboardData(format, nullptr);
|
|
if (result || IsClipboardFormatAvailable(format))
|
|
return true;
|
|
error = GetLastError();
|
|
if (!error)
|
|
error = ERROR_INVALID_DATA;
|
|
return false;
|
|
};
|
|
|
|
bool complete = true;
|
|
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TEXT)
|
|
complete = setDelayed(CF_UNICODETEXT) && complete;
|
|
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG)
|
|
complete = setDelayed(m_formatPNG) && complete;
|
|
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP)
|
|
complete = setDelayed(CF_DIB) && complete;
|
|
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TIFF)
|
|
complete = setDelayed(CF_TIFF) && complete;
|
|
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_JPEG)
|
|
complete = setDelayed(m_formatJPEG) && complete;
|
|
if (!SetOriginMarker(generation))
|
|
{
|
|
if (!error)
|
|
error = GetLastError();
|
|
if (!error)
|
|
error = ERROR_INVALID_DATA;
|
|
complete = false;
|
|
}
|
|
|
|
if (complete)
|
|
{
|
|
m_remoteGeneration = generation;
|
|
m_remoteFormats = formats;
|
|
}
|
|
else
|
|
{
|
|
EmptyClipboard();
|
|
m_remoteGeneration = 0;
|
|
m_remoteFormats = 0;
|
|
}
|
|
}
|
|
CloseClipboard();
|
|
m_applyingRemote = false;
|
|
|
|
if (!emptied || error)
|
|
{
|
|
if (!error)
|
|
error = ERROR_INVALID_DATA;
|
|
SetLastError(error);
|
|
DEBUG_WARN_HR(error, "Failed to replace the clipboard");
|
|
return false;
|
|
}
|
|
m_ownedSequence = GetClipboardSequenceNumber();
|
|
return true;
|
|
}
|
|
|
|
void CClipboardManager::ClearOwnedClipboard()
|
|
{
|
|
CancelIncoming(ERROR_OPERATION_ABORTED);
|
|
if (m_hwnd && GetClipboardOwner() == m_hwnd && OpenClipboardRetry())
|
|
{
|
|
m_applyingRemote = true;
|
|
EmptyClipboard();
|
|
CloseClipboard();
|
|
m_applyingRemote = false;
|
|
}
|
|
m_remoteGeneration = 0;
|
|
m_remoteFormats = 0;
|
|
m_ownedSequence = 0;
|
|
}
|
|
|
|
KVMFRClipboardFormat CClipboardManager::ToWireFormat(UINT format) const
|
|
{
|
|
if (format == CF_UNICODETEXT)
|
|
return KVMFR_CLIPBOARD_FORMAT_TEXT;
|
|
if (format == m_formatPNG)
|
|
return KVMFR_CLIPBOARD_FORMAT_PNG;
|
|
if (format == CF_DIB || format == CF_DIBV5)
|
|
return KVMFR_CLIPBOARD_FORMAT_BMP;
|
|
if (format == CF_TIFF)
|
|
return KVMFR_CLIPBOARD_FORMAT_TIFF;
|
|
if (format == m_formatJPEG)
|
|
return KVMFR_CLIPBOARD_FORMAT_JPEG;
|
|
return KVMFR_CLIPBOARD_FORMAT_NONE;
|
|
}
|
|
|
|
UINT CClipboardManager::ToWindowsFormat(KVMFRClipboardFormat format) const
|
|
{
|
|
switch (format)
|
|
{
|
|
case KVMFR_CLIPBOARD_FORMAT_TEXT:
|
|
return CF_UNICODETEXT;
|
|
case KVMFR_CLIPBOARD_FORMAT_PNG:
|
|
return m_formatPNG;
|
|
case KVMFR_CLIPBOARD_FORMAT_BMP:
|
|
return IsClipboardFormatAvailable(CF_DIBV5) ? CF_DIBV5 : CF_DIB;
|
|
case KVMFR_CLIPBOARD_FORMAT_TIFF:
|
|
return CF_TIFF;
|
|
case KVMFR_CLIPBOARD_FORMAT_JPEG:
|
|
return m_formatJPEG;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<CClipboardSpool> CClipboardManager::CaptureFormat(
|
|
KVMFRClipboardFormat format, DWORD sequence)
|
|
{
|
|
if (GetClipboardSequenceNumber() != sequence)
|
|
{
|
|
SetLastError(ERROR_RETRY);
|
|
return nullptr;
|
|
}
|
|
if (!OpenClipboardRetry())
|
|
return nullptr;
|
|
|
|
if (GetClipboardSequenceNumber() != sequence)
|
|
{
|
|
CloseClipboard();
|
|
SetLastError(ERROR_RETRY);
|
|
return nullptr;
|
|
}
|
|
|
|
UINT windowsFormat = ToWindowsFormat(format);
|
|
HANDLE handle = windowsFormat ? GetClipboardData(windowsFormat) : nullptr;
|
|
if (!handle && format == KVMFR_CLIPBOARD_FORMAT_BMP &&
|
|
windowsFormat == CF_DIBV5)
|
|
{
|
|
windowsFormat = CF_DIB;
|
|
handle = GetClipboardData(windowsFormat);
|
|
}
|
|
if (!handle)
|
|
{
|
|
const DWORD error = GetLastError();
|
|
CloseClipboard();
|
|
SetLastError(error ? error : ERROR_NOT_FOUND);
|
|
return nullptr;
|
|
}
|
|
|
|
const SIZE_T sourceSize = GlobalSize(handle);
|
|
if (sourceSize > MAX_SPOOL_BYTES)
|
|
{
|
|
CloseClipboard();
|
|
SetLastError(ERROR_FILE_TOO_LARGE);
|
|
return nullptr;
|
|
}
|
|
const uint8_t * source = static_cast<const uint8_t *>(GlobalLock(handle));
|
|
if (!source || !sourceSize)
|
|
{
|
|
const DWORD error = GetLastError();
|
|
if (source)
|
|
GlobalUnlock(handle);
|
|
CloseClipboard();
|
|
SetLastError(error ? error : ERROR_INVALID_DATA);
|
|
return nullptr;
|
|
}
|
|
|
|
std::shared_ptr<CClipboardSpool> spool;
|
|
try
|
|
{
|
|
spool = std::make_shared<CClipboardSpool>();
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
GlobalUnlock(handle);
|
|
CloseClipboard();
|
|
SetLastError(ERROR_OUTOFMEMORY);
|
|
return nullptr;
|
|
}
|
|
|
|
SetLastError(ERROR_SUCCESS);
|
|
bool success = true;
|
|
if (format == KVMFR_CLIPBOARD_FORMAT_TEXT)
|
|
{
|
|
if (sourceSize % sizeof(wchar_t))
|
|
{
|
|
SetLastError(ERROR_INVALID_DATA);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
const wchar_t * text = reinterpret_cast<const wchar_t *>(source);
|
|
const size_t capacity = sourceSize / sizeof(wchar_t);
|
|
size_t length = 0;
|
|
while (length < capacity && text[length])
|
|
++length;
|
|
|
|
std::vector<uint8_t> output;
|
|
try
|
|
{
|
|
output.reserve(TEXT_CONVERSION_CHUNK);
|
|
for (size_t index = 0; success && index < length; ++index)
|
|
{
|
|
uint32_t codepoint = static_cast<uint16_t>(text[index]);
|
|
if (codepoint == '\r' && index + 1 < length &&
|
|
text[index + 1] == L'\n')
|
|
continue;
|
|
if (codepoint >= 0xd800 && codepoint <= 0xdbff)
|
|
{
|
|
if (index + 1 < length)
|
|
{
|
|
const uint32_t low = static_cast<uint16_t>(text[index + 1]);
|
|
if (low >= 0xdc00 && low <= 0xdfff)
|
|
{
|
|
codepoint = 0x10000 + ((codepoint - 0xd800) << 10) +
|
|
(low - 0xdc00);
|
|
++index;
|
|
}
|
|
else
|
|
codepoint = 0xfffd;
|
|
}
|
|
else
|
|
codepoint = 0xfffd;
|
|
}
|
|
else if (codepoint >= 0xdc00 && codepoint <= 0xdfff)
|
|
codepoint = 0xfffd;
|
|
|
|
AppendUTF8(output, codepoint);
|
|
if (output.size() >= TEXT_CONVERSION_CHUNK)
|
|
{
|
|
success = spool->Append(output.data(), output.size());
|
|
output.clear();
|
|
}
|
|
}
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
SetLastError(ERROR_OUTOFMEMORY);
|
|
success = false;
|
|
}
|
|
if (success && !output.empty())
|
|
success = spool->Append(output.data(), output.size());
|
|
}
|
|
}
|
|
else if (format == KVMFR_CLIPBOARD_FORMAT_BMP)
|
|
{
|
|
const uint32_t dibOffset = DIBPixelOffset(source, sourceSize);
|
|
if (!dibOffset || sourceSize >
|
|
(std::numeric_limits<uint32_t>::max)() - sizeof(BITMAPFILEHEADER))
|
|
{
|
|
SetLastError(sourceSize >
|
|
(std::numeric_limits<uint32_t>::max)() - sizeof(BITMAPFILEHEADER) ?
|
|
ERROR_FILE_TOO_LARGE : ERROR_INVALID_DATA);
|
|
success = false;
|
|
}
|
|
else
|
|
{
|
|
BITMAPFILEHEADER header = {};
|
|
header.bfType = 0x4d42;
|
|
header.bfSize = static_cast<DWORD>(
|
|
sizeof(BITMAPFILEHEADER) + sourceSize);
|
|
header.bfOffBits = sizeof(BITMAPFILEHEADER) + dibOffset;
|
|
success = spool->Append(&header, sizeof(header)) &&
|
|
spool->Append(source, sourceSize);
|
|
}
|
|
}
|
|
else
|
|
success = spool->Append(source, sourceSize);
|
|
|
|
DWORD error = success ? ERROR_SUCCESS : GetLastError();
|
|
if (!success && !error)
|
|
error = ERROR_NOT_ENOUGH_MEMORY;
|
|
GlobalUnlock(handle);
|
|
if (GetClipboardSequenceNumber() != sequence)
|
|
{
|
|
CloseClipboard();
|
|
SetLastError(ERROR_RETRY);
|
|
return nullptr;
|
|
}
|
|
CloseClipboard();
|
|
if (!success)
|
|
{
|
|
SetLastError(error);
|
|
return nullptr;
|
|
}
|
|
return spool;
|
|
}
|
|
|
|
bool CClipboardManager::MaterializeFormat(KVMFRClipboardFormat format,
|
|
CClipboardSpool& spool)
|
|
{
|
|
UINT windowsFormat = ToWindowsFormat(format);
|
|
if (format == KVMFR_CLIPBOARD_FORMAT_BMP)
|
|
windowsFormat = CF_DIB;
|
|
if (!windowsFormat)
|
|
{
|
|
SetLastError(ERROR_INVALID_DATA);
|
|
return false;
|
|
}
|
|
|
|
HGLOBAL memory = nullptr;
|
|
if (format == KVMFR_CLIPBOARD_FORMAT_TEXT)
|
|
memory = UnicodeFromUTF8(spool);
|
|
else if (format == KVMFR_CLIPBOARD_FORMAT_BMP)
|
|
{
|
|
BITMAPFILEHEADER header = {};
|
|
const bool haveHeader = spool.Size() >= sizeof(header) &&
|
|
spool.Read(0, &header, sizeof(header));
|
|
if (!haveHeader ||
|
|
header.bfType != 0x4d42 ||
|
|
header.bfOffBits < sizeof(header) ||
|
|
header.bfOffBits > spool.Size())
|
|
{
|
|
if (haveHeader && (header.bfType != 0x4d42 ||
|
|
header.bfOffBits < sizeof(header) ||
|
|
header.bfOffBits > spool.Size()))
|
|
SetLastError(ERROR_INVALID_DATA);
|
|
return false;
|
|
}
|
|
memory = CopySpoolToGlobal(spool, sizeof(header));
|
|
}
|
|
else
|
|
memory = CopySpoolToGlobal(spool, 0);
|
|
|
|
if (!memory)
|
|
return false;
|
|
if (!SetClipboardData(windowsFormat, memory))
|
|
{
|
|
const DWORD error = GetLastError();
|
|
GlobalFree(memory);
|
|
SetLastError(error ? error : ERROR_INVALID_DATA);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CClipboardManager::RenderFormat(UINT windowsFormat, uint64_t deadline)
|
|
{
|
|
const KVMFRClipboardFormat format = ToWireFormat(windowsFormat);
|
|
if (!m_available || !m_remoteGeneration ||
|
|
!kvmfrClipboardFormatValid(format) ||
|
|
!(m_remoteFormats & kvmfrClipboardFormatFlag(format)))
|
|
return;
|
|
if (deadline && GetTickCount64() >= deadline)
|
|
return;
|
|
|
|
std::shared_ptr<IncomingTransfer> transfer;
|
|
try
|
|
{
|
|
transfer = std::make_shared<IncomingTransfer>();
|
|
transfer->spool = std::make_shared<CClipboardSpool>();
|
|
}
|
|
catch (const std::bad_alloc&)
|
|
{
|
|
return;
|
|
}
|
|
transfer->event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
|
if (!transfer->event)
|
|
return;
|
|
if (!deadline)
|
|
deadline = GetTickCount64() + RENDER_TIMEOUT_MS;
|
|
|
|
transfer->generation = m_remoteGeneration;
|
|
transfer->format = format;
|
|
transfer->transfer = Atomic::FetchAdd(m_nextTransfer, UINT64_C(1),
|
|
std::memory_order_relaxed);
|
|
if (!kvmfrClipboardTransferFromHelper(transfer->transfer))
|
|
{
|
|
Atomic::Store(m_nextTransfer,
|
|
KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(2),
|
|
std::memory_order_relaxed);
|
|
transfer->transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(1);
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_transferLock);
|
|
if (m_incoming && !m_incoming->complete)
|
|
return;
|
|
m_incoming = transfer;
|
|
}
|
|
|
|
Work request;
|
|
request.type = WorkType::SEND;
|
|
request.record.version = KVMFR_CLIPBOARD_VERSION;
|
|
request.record.type = KVMFR_CLIPBOARD_MESSAGE_REQUEST;
|
|
request.record.clipboardGeneration = transfer->generation;
|
|
request.record.transfer = transfer->transfer;
|
|
request.record.format = transfer->format;
|
|
request.deadline = deadline;
|
|
if (!QueueWork(std::move(request)))
|
|
CancelIncoming(ERROR_BUSY, transfer->transfer);
|
|
|
|
const uint64_t now = GetTickCount64();
|
|
const DWORD waitMs = now >= deadline ? 0 : static_cast<DWORD>(
|
|
(std::min<uint64_t>)(deadline - now, MAXDWORD));
|
|
const DWORD wait = WaitForSingleObject(transfer->event, waitMs);
|
|
const DWORD waitError = wait == WAIT_FAILED ? GetLastError() :
|
|
ERROR_SUCCESS;
|
|
bool requestComplete = false;
|
|
uint32_t transferError = ERROR_SUCCESS;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_transferLock);
|
|
requestComplete = transfer->complete;
|
|
transferError = transfer->error;
|
|
}
|
|
if (!requestComplete || transferError != ERROR_SUCCESS)
|
|
{
|
|
const uint32_t reason = requestComplete ? transferError :
|
|
(wait == WAIT_TIMEOUT ? ERROR_TIMEOUT :
|
|
(wait == WAIT_FAILED ? waitError : ERROR_INVALID_DATA));
|
|
CancelIncoming(reason ? reason : ERROR_OPERATION_ABORTED,
|
|
transfer->transfer);
|
|
KVMFRClipboardMessage cancel = {};
|
|
cancel.clipboardGeneration = transfer->generation;
|
|
cancel.transfer = transfer->transfer;
|
|
cancel.format = transfer->format;
|
|
QueueCancel(cancel, reason ? reason : ERROR_OPERATION_ABORTED);
|
|
}
|
|
|
|
bool render = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_transferLock);
|
|
render = transfer->complete && transfer->error == ERROR_SUCCESS &&
|
|
transfer->generation == m_remoteGeneration &&
|
|
GetClipboardOwner() == m_hwnd;
|
|
if (m_incoming == transfer)
|
|
m_incoming.reset();
|
|
}
|
|
|
|
if (render && !MaterializeFormat(format, *transfer->spool))
|
|
DEBUG_WARN_HR(GetLastError(), "Failed to render clipboard format %u",
|
|
format);
|
|
}
|
|
|
|
void CClipboardManager::RenderAllFormats()
|
|
{
|
|
if (!OpenClipboardRetry())
|
|
return;
|
|
if (GetClipboardOwner() != m_hwnd)
|
|
{
|
|
CloseClipboard();
|
|
return;
|
|
}
|
|
|
|
const uint32_t formats = m_remoteFormats;
|
|
const uint64_t deadline = GetTickCount64() + RENDER_TIMEOUT_MS;
|
|
if (formats & KVMFR_CLIPBOARD_FORMAT_MASK_TEXT)
|
|
RenderFormat(CF_UNICODETEXT, deadline);
|
|
if (GetTickCount64() < deadline &&
|
|
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_PNG))
|
|
RenderFormat(m_formatPNG, deadline);
|
|
if (GetTickCount64() < deadline &&
|
|
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_BMP))
|
|
RenderFormat(CF_DIB, deadline);
|
|
if (GetTickCount64() < deadline &&
|
|
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_TIFF))
|
|
RenderFormat(CF_TIFF, deadline);
|
|
if (GetTickCount64() < deadline &&
|
|
(formats & KVMFR_CLIPBOARD_FORMAT_MASK_JPEG))
|
|
RenderFormat(m_formatJPEG, deadline);
|
|
CloseClipboard();
|
|
}
|