From 1a4b6049bd91cafb0d4a6ed75775d557a665ba1e Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 3 Oct 2026 11:53:10 +1000 Subject: [PATCH] [client] windows: implement native clipboard support Add Win32 clipboard ownership, notifications, and data exchange for text and image formats. Marshal clipboard access onto the window thread and reject stale transfers when ownership changes. Keep file clipboard offers unsupported while clearing stale content from earlier remote offers. --- client/displayservers/Windows/CMakeLists.txt | 1 + client/displayservers/Windows/clipboard.c | 1198 ++++++++++++++++++ client/displayservers/Windows/clipboard.h | 42 + client/displayservers/Windows/windows.c | 14 + 4 files changed, 1255 insertions(+) create mode 100644 client/displayservers/Windows/clipboard.c create mode 100644 client/displayservers/Windows/clipboard.h diff --git a/client/displayservers/Windows/CMakeLists.txt b/client/displayservers/Windows/CMakeLists.txt index 108ddb0c..16a66942 100644 --- a/client/displayservers/Windows/CMakeLists.txt +++ b/client/displayservers/Windows/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.10) project(displayserver_Windows LANGUAGES C) add_library(displayserver_Windows STATIC + clipboard.c input_event.c window_size.c windows.c diff --git a/client/displayservers/Windows/clipboard.c b/client/displayservers/Windows/clipboard.c new file mode 100644 index 00000000..0ccf514d --- /dev/null +++ b/client/displayservers/Windows/clipboard.c @@ -0,0 +1,1198 @@ +/** + * 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 "clipboard.h" + +#include +#include +#include +#include +#include + +#include "common/debug.h" +#include "common/windebug.h" +#include "core/clipboard.h" +#include "platform.h" + +#define WINDOWS_CB_OPEN_ATTEMPTS 8 + +enum WindowsClipboardMessage +{ + WM_LG_CLIPBOARD_INIT = WM_APP + 0x100, + WM_LG_CLIPBOARD_DISPATCH = WM_APP + 0x101, +}; + +enum WindowsClipboardOperationType +{ + WINDOWS_CB_OPERATION_NOTICE, + WINDOWS_CB_OPERATION_RELEASE, + WINDOWS_CB_OPERATION_REQUEST, + WINDOWS_CB_OPERATION_DATA, +}; + +struct WindowsClipboardOperation +{ + struct WindowsClipboardOperation * next; + enum WindowsClipboardOperationType type; + uint64_t generation; + LG_ClipboardRequest request; + LG_ClipboardData dataType; + uint8_t * data; + size_t size; +}; + +struct WindowsClipboardTransfer +{ + uint64_t generation; + LG_ClipboardData type; + uint8_t * data; + size_t size; + size_t capacity; + bool started; +}; + +struct WindowsClipboardState +{ + struct WindowsClipboardOperation * queueHead; + struct WindowsClipboardOperation * queueTail; + HWND window; + HWND localOwner; + DWORD localSequence; + uint32_t localTypes; + UINT pngFormat; + UINT jfifFormat; + UINT jpegFormat; + LG_ClipboardRequest canceledRequest; + bool listener; + bool accepting; +}; + +static SRWLOCK windowsCBQueueLock = SRWLOCK_INIT; +static struct WindowsClipboardState windowsCB; +static _Atomic(uint64_t) windowsCBGeneration; + +static uint16_t readLE16(const uint8_t * data) +{ + return (uint16_t)data[0] | + (uint16_t)((uint16_t)data[1] << 8); +} + +static uint32_t readLE32(const uint8_t * data) +{ + return (uint32_t)data[0] | + (uint32_t)data[1] << 8 | + (uint32_t)data[2] << 16 | + (uint32_t)data[3] << 24; +} + +static void writeLE16(uint8_t * data, uint16_t value) +{ + data[0] = (uint8_t)value; + data[1] = (uint8_t)(value >> 8); +} + +static void writeLE32(uint8_t * data, uint32_t value) +{ + data[0] = (uint8_t)value; + data[1] = (uint8_t)(value >> 8); + data[2] = (uint8_t)(value >> 16); + data[3] = (uint8_t)(value >> 24); +} + +static void freeOperation(struct WindowsClipboardOperation * operation) +{ + if (!operation) + return; + + free(operation->data); + free(operation); +} + +static void freeOperations(struct WindowsClipboardOperation * operation) +{ + while (operation) + { + struct WindowsClipboardOperation * next = operation->next; + freeOperation(operation); + operation = next; + } +} + +static struct WindowsClipboardOperation * newOperation( + enum WindowsClipboardOperationType type) +{ + struct WindowsClipboardOperation * operation = + calloc(1, sizeof(*operation)); + if (!operation) + { + DEBUG_ERROR("Out of memory while queuing a Windows clipboard operation"); + return NULL; + } + + operation->type = type; + return operation; +} + +static bool queueOperation(struct WindowsClipboardOperation * operation) +{ + if (!operation) + return false; + + AcquireSRWLockExclusive(&windowsCBQueueLock); + if (!windowsCB.accepting || !windowsCB.window) + { + ReleaseSRWLockExclusive(&windowsCBQueueLock); + freeOperation(operation); + return false; + } + + const bool wake = !windowsCB.queueTail; + if (windowsCB.queueTail) + windowsCB.queueTail->next = operation; + else + windowsCB.queueHead = operation; + windowsCB.queueTail = operation; + + if (wake && !PostMessageW( + windowsCB.window, WM_LG_CLIPBOARD_DISPATCH, 0, 0)) + { + windowsCB.queueHead = NULL; + windowsCB.queueTail = NULL; + ReleaseSRWLockExclusive(&windowsCBQueueLock); + DEBUG_WINERROR("Failed to queue a Windows clipboard operation", + GetLastError()); + freeOperation(operation); + return false; + } + + ReleaseSRWLockExclusive(&windowsCBQueueLock); + return true; +} + +static struct WindowsClipboardOperation * takeOperation(void) +{ + AcquireSRWLockExclusive(&windowsCBQueueLock); + struct WindowsClipboardOperation * operation = windowsCB.queueHead; + if (operation) + { + windowsCB.queueHead = operation->next; + operation->next = NULL; + if (!windowsCB.queueHead) + windowsCB.queueTail = NULL; + } + ReleaseSRWLockExclusive(&windowsCBQueueLock); + return operation; +} + +static bool requestCanceled(LG_ClipboardRequest request) +{ + AcquireSRWLockShared(&windowsCBQueueLock); + const bool canceled = windowsCB.canceledRequest == request; + ReleaseSRWLockShared(&windowsCBQueueLock); + return canceled; +} + +static bool openClipboard(void) +{ + DWORD delay = 1; + for (unsigned int attempt = 0; + attempt < WINDOWS_CB_OPEN_ATTEMPTS; ++attempt) + { + if (OpenClipboard(windowsCB.window)) + return true; + + if (attempt + 1 < WINDOWS_CB_OPEN_ATTEMPTS) + { + Sleep(delay); + delay *= 2; + } + } + + DEBUG_WINERROR("Failed to open the Windows clipboard", GetLastError()); + return false; +} + +static bool checkedAdd(size_t a, size_t b, size_t * result) +{ + if (a > SIZE_MAX - b) + return false; + + *result = a + b; + return true; +} + +static bool uint64ToSize(uint64_t value, size_t * result) +{ + const size_t converted = (size_t)value; + if ((uint64_t)converted != value) + return false; + + *result = converted; + return true; +} + +static bool validateDIB(const uint8_t * data, size_t size, + size_t * pixelOffset) +{ + if (!data || size < sizeof(uint32_t)) + return false; + + const uint32_t headerSize = readLE32(data); + size_t offset; + if (headerSize == sizeof(BITMAPCOREHEADER)) + { + if (size < sizeof(BITMAPCOREHEADER) || !readLE16(data + 4) || + !readLE16(data + 6) || readLE16(data + 8) != 1) + return false; + + const uint16_t bits = readLE16(data + 10); + if (!bits || bits > 32) + return false; + + const size_t colors = bits <= 8 ? (size_t)1 << bits : 0; + if (colors > (SIZE_MAX - headerSize) / sizeof(RGBTRIPLE)) + return false; + offset = headerSize + colors * sizeof(RGBTRIPLE); + } + else + { + if (headerSize < sizeof(BITMAPINFOHEADER) || headerSize > size || + !readLE32(data + 4) || !readLE32(data + 8) || + readLE16(data + 12) != 1) + return false; + + const uint16_t bits = readLE16(data + 14); + const uint32_t compression = readLE32(data + 16); + const uint32_t imageSize = readLE32(data + 20); + const uint32_t usedColors = readLE32(data + 32); + if (bits > 32) + return false; + + size_t maskSize = 0; + if (headerSize == sizeof(BITMAPINFOHEADER)) + { + if (compression == BI_BITFIELDS) + maskSize = 3 * sizeof(uint32_t); +#ifdef BI_ALPHABITFIELDS + else if (compression == BI_ALPHABITFIELDS) + maskSize = 4 * sizeof(uint32_t); +#endif + } + + const size_t colors = usedColors ? usedColors : + (bits && bits <= 8 ? (size_t)1 << bits : 0); + if (!checkedAdd(headerSize, maskSize, &offset) || + colors > (SIZE_MAX - offset) / sizeof(RGBQUAD)) + return false; + offset += colors * sizeof(RGBQUAD); + if (imageSize && (offset > size || imageSize > size - offset)) + return false; + } + + if (offset > size) + return false; + + *pixelOffset = offset; + return true; +} + +static bool bmpToDIB(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + const uint8_t * data = source; + if (!data || sourceSize < sizeof(BITMAPFILEHEADER) || + readLE16(data) != UINT16_C(0x4D42)) + return false; + + const uint32_t fileSizeValue = readLE32(data + 2); + const uint32_t fileOffset = readLE32(data + 10); + const size_t fileSize = fileSizeValue ? fileSizeValue : sourceSize; + if (fileSize > sourceSize || fileSize < sizeof(BITMAPFILEHEADER) || + fileOffset < sizeof(BITMAPFILEHEADER) || fileOffset > fileSize) + return false; + + const uint8_t * dib = data + sizeof(BITMAPFILEHEADER); + const size_t dibSize = fileSize - sizeof(BITMAPFILEHEADER); + size_t minimumOffset; + if (!validateDIB(dib, dibSize, &minimumOffset) || + fileOffset - sizeof(BITMAPFILEHEADER) < minimumOffset) + return false; + + uint8_t * copy = malloc(dibSize ? dibSize : 1); + if (!copy) + return false; + + if (dibSize) + memcpy(copy, dib, dibSize); + *output = copy; + *outputSize = dibSize; + return true; +} + +static bool dibToBMP(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + const uint8_t * dib = source; + size_t dibOffset; + if (!validateDIB(dib, sourceSize, &dibOffset) || + sourceSize > UINT32_MAX - sizeof(BITMAPFILEHEADER)) + return false; + + const size_t size = sizeof(BITMAPFILEHEADER) + sourceSize; + uint8_t * bmp = malloc(size); + if (!bmp) + return false; + + memset(bmp, 0, sizeof(BITMAPFILEHEADER)); + writeLE16(bmp, UINT16_C(0x4D42)); + writeLE32(bmp + 2, (uint32_t)size); + writeLE32(bmp + 10, + (uint32_t)(sizeof(BITMAPFILEHEADER) + dibOffset)); + memcpy(bmp + sizeof(BITMAPFILEHEADER), dib, sourceSize); + *output = bmp; + *outputSize = size; + return true; +} + +static bool utf8ToClipboardText(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + if ((sourceSize && !source) || sourceSize > INT_MAX || + (sourceSize && memchr(source, 0, sourceSize))) + return false; + + int sourceLength = 0; + if (sourceSize) + { + sourceLength = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + source, (int)sourceSize, NULL, 0); + if (sourceLength <= 0) + return false; + } + + wchar_t * wide = malloc( + ((size_t)sourceLength + 1) * sizeof(*wide)); + if (!wide) + return false; + if (sourceLength && MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + source, (int)sourceSize, wide, sourceLength) != sourceLength) + { + free(wide); + return false; + } + + size_t lineBreaks = 0; + for (int i = 0; i < sourceLength; ++i) + if (wide[i] == L'\n' && (!i || wide[i - 1] != L'\r')) + ++lineBreaks; + if ((size_t)sourceLength > + (SIZE_MAX / sizeof(wchar_t)) - lineBreaks - 1) + { + free(wide); + return false; + } + + const size_t count = (size_t)sourceLength + lineBreaks + 1; + wchar_t * text = malloc(count * sizeof(*text)); + if (!text) + { + free(wide); + return false; + } + + size_t at = 0; + for (int i = 0; i < sourceLength; ++i) + { + if (wide[i] == L'\n' && (!i || wide[i - 1] != L'\r')) + text[at++] = L'\r'; + text[at++] = wide[i]; + } + text[at++] = L'\0'; + free(wide); + + *output = (uint8_t *)text; + *outputSize = at * sizeof(*text); + return true; +} + +static bool clipboardTextToUtf8(const void * source, size_t sourceSize, + uint8_t ** output, size_t * outputSize) +{ + if (!source || sourceSize < sizeof(wchar_t) || + sourceSize % sizeof(wchar_t)) + return false; + + const wchar_t * text = source; + const size_t capacity = sourceSize / sizeof(*text); + size_t length = 0; + while (length < capacity && text[length]) + ++length; + if (length == capacity || length > INT_MAX) + return false; + + wchar_t * normalized = malloc((length + 1) * sizeof(*normalized)); + if (!normalized) + return false; + + size_t normalizedLength = 0; + for (size_t i = 0; i < length; ++i) + { + if (text[i] == L'\r' && i + 1 < length && text[i + 1] == L'\n') + { + ++i; + normalized[normalizedLength++] = L'\n'; + } + else + normalized[normalizedLength++] = text[i]; + } + + const int size = normalizedLength ? + WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + normalized, (int)normalizedLength, NULL, 0, NULL, NULL) : 0; + if (normalizedLength && size <= 0) + { + free(normalized); + return false; + } + + uint8_t * utf8 = size ? malloc((size_t)size) : NULL; + if (size && (!utf8 || WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, + normalized, (int)normalizedLength, (char *)utf8, size, + NULL, NULL) != size)) + { + free(normalized); + free(utf8); + return false; + } + free(normalized); + + *output = utf8; + *outputSize = (size_t)size; + return true; +} + +static HGLOBAL makeGlobalData(const void * data, size_t size) +{ + if (size && !data) + return NULL; + + HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, size ? size : 1); + if (!handle) + return NULL; + + void * target = GlobalLock(handle); + if (!target) + { + GlobalFree(handle); + return NULL; + } + if (size) + memcpy(target, data, size); + GlobalUnlock(handle); + return handle; +} + +static bool copyGlobalData(HANDLE handle, uint8_t ** output, + size_t * outputSize) +{ + const SIZE_T size = GlobalSize(handle); + if (!size) + return false; + + const void * source = GlobalLock(handle); + if (!source) + return false; + + uint8_t * data = malloc(size); + if (data) + memcpy(data, source, size); + GlobalUnlock(handle); + if (!data) + return false; + + *output = data; + *outputSize = size; + return true; +} + +static bool localTypeAvailable(LG_ClipboardData type) +{ + switch (type) + { + case LG_CLIPBOARD_DATA_TEXT: + return IsClipboardFormatAvailable(CF_UNICODETEXT); + + case LG_CLIPBOARD_DATA_PNG: + return windowsCB.pngFormat && + IsClipboardFormatAvailable(windowsCB.pngFormat); + + case LG_CLIPBOARD_DATA_BMP: + return IsClipboardFormatAvailable(CF_DIBV5) || + IsClipboardFormatAvailable(CF_DIB); + + case LG_CLIPBOARD_DATA_TIFF: + return IsClipboardFormatAvailable(CF_TIFF); + + case LG_CLIPBOARD_DATA_JPEG: + return (windowsCB.jfifFormat && + IsClipboardFormatAvailable(windowsCB.jfifFormat)) || + (windowsCB.jpegFormat && + IsClipboardFormatAvailable(windowsCB.jpegFormat)); + + case LG_CLIPBOARD_DATA_FILES: + case LG_CLIPBOARD_DATA_NONE: + return false; + } + + return false; +} + +static UINT localClipboardFormat(LG_ClipboardData type) +{ + switch (type) + { + case LG_CLIPBOARD_DATA_TEXT: + return CF_UNICODETEXT; + case LG_CLIPBOARD_DATA_PNG: + return windowsCB.pngFormat; + case LG_CLIPBOARD_DATA_BMP: + if (IsClipboardFormatAvailable(CF_DIBV5)) + return CF_DIBV5; + return CF_DIB; + case LG_CLIPBOARD_DATA_TIFF: + return CF_TIFF; + case LG_CLIPBOARD_DATA_JPEG: + if (windowsCB.jfifFormat && + IsClipboardFormatAvailable(windowsCB.jfifFormat)) + return windowsCB.jfifFormat; + return windowsCB.jpegFormat; + case LG_CLIPBOARD_DATA_FILES: + case LG_CLIPBOARD_DATA_NONE: + return 0; + } + + return 0; +} + +static bool readLocalClipboard(LG_ClipboardData type, + uint8_t ** output, size_t * outputSize) +{ + const UINT format = localClipboardFormat(type); + if (!format) + return false; + + HANDLE handle = GetClipboardData(format); + if (!handle && type == LG_CLIPBOARD_DATA_BMP && format == CF_DIBV5 && + IsClipboardFormatAvailable(CF_DIB)) + handle = GetClipboardData(CF_DIB); + else if (!handle && type == LG_CLIPBOARD_DATA_JPEG) + { + const UINT alternate = format == windowsCB.jfifFormat ? + windowsCB.jpegFormat : windowsCB.jfifFormat; + if (alternate && IsClipboardFormatAvailable(alternate)) + handle = GetClipboardData(alternate); + } + if (!handle) + return false; + + uint8_t * data = NULL; + size_t size = 0; + if (type == LG_CLIPBOARD_DATA_TEXT) + { + const SIZE_T globalSize = GlobalSize(handle); + const void * source = globalSize ? GlobalLock(handle) : NULL; + const bool result = source && clipboardTextToUtf8( + source, globalSize, &data, &size); + if (source) + GlobalUnlock(handle); + if (!result) + return false; + } + else if (type == LG_CLIPBOARD_DATA_BMP) + { + const SIZE_T sourceSize = GlobalSize(handle); + if (!sourceSize || + sourceSize > UINT32_MAX - sizeof(BITMAPFILEHEADER)) + return false; + + uint8_t * dib = NULL; + size_t dibSize = 0; + if (!copyGlobalData(handle, &dib, &dibSize)) + return false; + const bool result = dibToBMP(dib, dibSize, &data, &size); + free(dib); + if (!result) + return false; + } + else if (!copyGlobalData(handle, &data, &size)) + return false; + + *output = data; + *outputSize = size; + return true; +} + +static void updateLocalClipboard(void) +{ + const HWND owner = GetClipboardOwner(); + const DWORD sequence = GetClipboardSequenceNumber(); + if (owner == windowsCB.window) + return; + if (owner == windowsCB.localOwner && + sequence == windowsCB.localSequence) + return; + + LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; + size_t count = 0; + uint32_t mask = 0; + for (LG_ClipboardData type = LG_CLIPBOARD_DATA_TEXT; + type < LG_CLIPBOARD_DATA_FILES; ++type) + { + if (!localTypeAvailable(type)) + continue; + + types[count++] = type; + mask |= UINT32_C(1) << type; + } + + windowsCB.localOwner = owner; + windowsCB.localSequence = sequence; + windowsCB.localTypes = mask; + atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel); + if (count) + clipboard_notifyTypes(types, count); + else + clipboard_release(); +} + +static void handleLocalRequest( + const struct WindowsClipboardOperation * operation) +{ + const LG_ClipboardData type = operation->dataType; + const uint32_t typeMask = type < LG_CLIPBOARD_DATA_NONE ? + UINT32_C(1) << type : 0; + if (requestCanceled(operation->request)) + return; + if (!typeMask || !(windowsCB.localTypes & typeMask) || + windowsCB.localSequence != GetClipboardSequenceNumber() || + windowsCB.localOwner != GetClipboardOwner() || !openClipboard()) + { + clipboard_abort(operation->request); + return; + } + + const bool current = windowsCB.localSequence == + GetClipboardSequenceNumber() && + windowsCB.localOwner == GetClipboardOwner(); + uint8_t * data = NULL; + size_t size = 0; + const bool read = current && readLocalClipboard(type, + &data, &size); + const HWND owner = GetClipboardOwner(); + const DWORD sequence = GetClipboardSequenceNumber(); + const bool sequenceChanged = sequence != windowsCB.localSequence; + CloseClipboard(); + const bool unchanged = owner == GetClipboardOwner() && + sequence == GetClipboardSequenceNumber(); + if (requestCanceled(operation->request)) + { + free(data); + return; + } + if (!read || (sequenceChanged && !owner) || !unchanged || + owner != windowsCB.localOwner) + { + free(data); + clipboard_abort(operation->request); + return; + } + + windowsCB.localSequence = sequence; + clipboard_data(operation->request, type, data, size); + free(data); +} + +static bool prepareRemoteData(LG_ClipboardData type, + const void * source, size_t sourceSize, UINT formats[2], + HGLOBAL handles[2], size_t * count) +{ + if (sourceSize && !source) + return false; + + uint8_t * converted = NULL; + size_t convertedSize = 0; + const void * data = source; + size_t size = sourceSize; + + switch (type) + { + case LG_CLIPBOARD_DATA_TEXT: + if (!utf8ToClipboardText( + source, sourceSize, &converted, &convertedSize)) + return false; + data = converted; + size = convertedSize; + formats[0] = CF_UNICODETEXT; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_PNG: + if (!windowsCB.pngFormat) + return false; + formats[0] = windowsCB.pngFormat; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_BMP: + if (!bmpToDIB(source, sourceSize, &converted, &convertedSize)) + return false; + data = converted; + size = convertedSize; + formats[0] = CF_DIB; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_TIFF: + formats[0] = CF_TIFF; + *count = 1; + break; + + case LG_CLIPBOARD_DATA_JPEG: + if (!windowsCB.jfifFormat && !windowsCB.jpegFormat) + return false; + if (windowsCB.jfifFormat) + formats[(*count)++] = windowsCB.jfifFormat; + if (windowsCB.jpegFormat) + formats[(*count)++] = windowsCB.jpegFormat; + break; + + case LG_CLIPBOARD_DATA_FILES: + case LG_CLIPBOARD_DATA_NONE: + return false; + } + + bool success = true; + for (size_t i = 0; i < *count; ++i) + { + handles[i] = makeGlobalData(data, size); + if (!handles[i]) + { + success = false; + break; + } + } + free(converted); + if (success) + return true; + + for (size_t i = 0; i < *count; ++i) + if (handles[i]) + GlobalFree(handles[i]); + return false; +} + +static void handleRemoteData( + const struct WindowsClipboardOperation * operation) +{ + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + return; + + UINT formats[2] = { 0 }; + HGLOBAL handles[2] = { NULL }; + size_t count = 0; + if (!prepareRemoteData(operation->dataType, + operation->data, operation->size, formats, handles, &count)) + { + DEBUG_WARN("Ignoring invalid guest clipboard data"); + return; + } + + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) || !openClipboard()) + goto fail; + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) || !EmptyClipboard()) + { + CloseClipboard(); + goto fail; + } + + bool published = false; + for (size_t i = 0; i < count; ++i) + { + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + break; + + if (SetClipboardData(formats[i], handles[i])) + { + handles[i] = NULL; + published = true; + } + else + DEBUG_WINERROR("Failed to publish Windows clipboard data", + GetLastError()); + } + CloseClipboard(); + windowsCB.localOwner = GetClipboardOwner(); + windowsCB.localSequence = GetClipboardSequenceNumber(); + windowsCB.localTypes = 0; + if (!published) + DEBUG_WARN("Failed to publish any Windows clipboard format"); + +fail: + for (size_t i = 0; i < count; ++i) + if (handles[i]) + GlobalFree(handles[i]); +} + +static void handleRemoteRelease( + const struct WindowsClipboardOperation * operation) +{ + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) || + GetClipboardOwner() != windowsCB.window || !openClipboard()) + return; + + bool released = false; + if (operation->generation == atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire) && + GetClipboardOwner() == windowsCB.window) + released = EmptyClipboard(); + CloseClipboard(); + if (released) + { + windowsCB.localOwner = GetClipboardOwner(); + windowsCB.localSequence = GetClipboardSequenceNumber(); + windowsCB.localTypes = 0; + } +} + +static bool growTransfer(struct WindowsClipboardTransfer * transfer, + size_t wanted) +{ + if (wanted <= transfer->capacity) + return true; + + size_t capacity = transfer->capacity ? transfer->capacity : 4096; + while (capacity < wanted) + { + if (capacity > SIZE_MAX / 2) + { + capacity = wanted; + break; + } + capacity *= 2; + } + + uint8_t * data = realloc(transfer->data, capacity); + if (!data) + return false; + transfer->data = data; + transfer->capacity = capacity; + return true; +} + +static void freeTransfer(struct WindowsClipboardTransfer * transfer) +{ + if (!transfer) + return; + + free(transfer->data); + free(transfer); +} + +static LG_ClipboardResult remoteDataBegin(void * opaque, + LG_ClipboardData type, uint64_t sizeHint) +{ + struct WindowsClipboardTransfer * transfer = opaque; + size_t size = 0; + if (transfer->started || type != transfer->type || + (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + !uint64ToSize(sizeHint, &size))) + return LG_CLIPBOARD_RESULT_FAILED; + + transfer->started = true; + if (sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && size && + !growTransfer(transfer, size)) + return LG_CLIPBOARD_RESULT_FAILED; + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult remoteDataChunk(void * opaque, uint64_t offset, + const void * data, size_t size) +{ + struct WindowsClipboardTransfer * transfer = opaque; + if (!transfer->started || offset != transfer->size || !data || !size || + size > SIZE_MAX - transfer->size || + !growTransfer(transfer, transfer->size + size)) + return LG_CLIPBOARD_RESULT_FAILED; + + memcpy(transfer->data + transfer->size, data, size); + transfer->size += size; + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult remoteDataEnd(void * opaque, uint64_t finalSize) +{ + struct WindowsClipboardTransfer * transfer = opaque; + if (!transfer->started || finalSize != transfer->size) + return LG_CLIPBOARD_RESULT_FAILED; + + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_DATA); + if (!operation) + return LG_CLIPBOARD_RESULT_FAILED; + + operation->generation = transfer->generation; + operation->dataType = transfer->type; + operation->data = transfer->data; + operation->size = transfer->size; + transfer->data = NULL; + if (!queueOperation(operation)) + return LG_CLIPBOARD_RESULT_FAILED; + freeTransfer(transfer); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static void remoteDataCancel(void * opaque, + LG_ClipboardCancelReason reason) +{ + (void)reason; + freeTransfer(opaque); +} + +static const LG_ClipboardStreamOps remoteStream = +{ + .begin = remoteDataBegin, + .chunk = remoteDataChunk, + .end = remoteDataEnd, + .cancel = remoteDataCancel, +}; + +static void handleRemoteNotice( + const struct WindowsClipboardOperation * operation) +{ + if (operation->generation != atomic_load_explicit( + &windowsCBGeneration, memory_order_acquire)) + return; + if (operation->dataType == LG_CLIPBOARD_DATA_FILES) + { + handleRemoteRelease(operation); + return; + } + if (operation->dataType >= LG_CLIPBOARD_DATA_NONE) + return; + + struct WindowsClipboardTransfer * transfer = + calloc(1, sizeof(*transfer)); + if (!transfer) + { + DEBUG_ERROR("Out of memory while requesting guest clipboard data"); + return; + } + + LG_ClipboardRequest request; + transfer->generation = operation->generation; + transfer->type = operation->dataType; + if (!clipboard_requestStream( + transfer->type, &remoteStream, transfer, &request)) + freeTransfer(transfer); +} + +static void dispatchOperations(void) +{ + struct WindowsClipboardOperation * operation; + while ((operation = takeOperation())) + { + switch (operation->type) + { + case WINDOWS_CB_OPERATION_NOTICE: + handleRemoteNotice(operation); + break; + + case WINDOWS_CB_OPERATION_RELEASE: + handleRemoteRelease(operation); + break; + + case WINDOWS_CB_OPERATION_REQUEST: + handleLocalRequest(operation); + break; + + case WINDOWS_CB_OPERATION_DATA: + handleRemoteData(operation); + break; + } + + freeOperation(operation); + } +} + +static bool eventInit(HWND window) +{ + if (!window) + return false; + + windowsCB.pngFormat = RegisterClipboardFormatW(L"PNG"); + windowsCB.jfifFormat = RegisterClipboardFormatW(L"JFIF"); + windowsCB.jpegFormat = RegisterClipboardFormatW(L"JPEG"); + if (!windowsCB.pngFormat || !windowsCB.jfifFormat || + !windowsCB.jpegFormat) + { + DEBUG_WINERROR("Failed to register Windows clipboard formats", + GetLastError()); + return false; + } + + if (!AddClipboardFormatListener(window)) + { + DEBUG_WINERROR("Failed to register the Windows clipboard listener", + GetLastError()); + return false; + } + + AcquireSRWLockExclusive(&windowsCBQueueLock); + windowsCB.window = window; + windowsCB.listener = true; + windowsCB.accepting = true; + ReleaseSRWLockExclusive(&windowsCBQueueLock); + atomic_store_explicit(&windowsCBGeneration, 0, memory_order_release); + updateLocalClipboard(); + return true; +} + +bool windowsCBHandleMessage(HWND window, UINT message, + WPARAM wParam, LPARAM lParam, LRESULT * result) +{ + (void)wParam; + (void)lParam; + + switch (message) + { + case WM_LG_CLIPBOARD_INIT: + *result = eventInit(window); + return true; + + case WM_LG_CLIPBOARD_DISPATCH: + dispatchOperations(); + *result = 0; + return true; + + case WM_CLIPBOARDUPDATE: + if (windowsCB.accepting) + updateLocalClipboard(); + *result = 0; + return true; + } + + return false; +} + +void windowsCBEventShutdown(void) +{ + HWND window; + bool listener; + struct WindowsClipboardOperation * operations; + + AcquireSRWLockExclusive(&windowsCBQueueLock); + window = windowsCB.window; + listener = windowsCB.listener; + operations = windowsCB.queueHead; + windowsCB.accepting = false; + windowsCB.listener = false; + windowsCB.window = NULL; + windowsCB.queueHead = NULL; + windowsCB.queueTail = NULL; + ReleaseSRWLockExclusive(&windowsCBQueueLock); + + atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel); + if (listener && window && !RemoveClipboardFormatListener(window)) + DEBUG_WINERROR("Failed to remove the Windows clipboard listener", + GetLastError()); + freeOperations(operations); +} + +bool windowsCBInit(void) +{ + const HWND window = windowsGetWindow(); + return window && SendMessageW( + window, WM_LG_CLIPBOARD_INIT, 0, 0) != 0; +} + +void windowsCBNotice(LG_ClipboardData type) +{ + const uint64_t generation = atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel) + 1; + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_NOTICE); + if (!operation) + return; + + operation->generation = generation; + operation->dataType = type; + queueOperation(operation); +} + +void windowsCBRelease(void) +{ + const uint64_t generation = atomic_fetch_add_explicit( + &windowsCBGeneration, 1, memory_order_acq_rel) + 1; + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_RELEASE); + if (!operation) + return; + + operation->generation = generation; + queueOperation(operation); +} + +void windowsCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) +{ + struct WindowsClipboardOperation * operation = + newOperation(WINDOWS_CB_OPERATION_REQUEST); + if (!operation) + { + clipboard_abort(request); + return; + } + + operation->request = request; + operation->dataType = type; + if (!queueOperation(operation)) + clipboard_abort(request); +} + +void windowsCBRequestReady(LG_ClipboardRequest request) +{ + (void)request; +} + +void windowsCBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason) +{ + (void)reason; + + AcquireSRWLockExclusive(&windowsCBQueueLock); + windowsCB.canceledRequest = request; + ReleaseSRWLockExclusive(&windowsCBQueueLock); +} diff --git a/client/displayservers/Windows/clipboard.h b/client/displayservers/Windows/clipboard.h new file mode 100644 index 00000000..3342e82c --- /dev/null +++ b/client/displayservers/Windows/clipboard.h @@ -0,0 +1,42 @@ +/** + * 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 + */ + +#ifndef _H_WINDOWSDS_CLIPBOARD_ +#define _H_WINDOWSDS_CLIPBOARD_ + +#include + +#include + +#include "interface/displayserver.h" + +bool windowsCBHandleMessage(HWND window, UINT message, + WPARAM wParam, LPARAM lParam, LRESULT * result); +void windowsCBEventShutdown(void); + +bool windowsCBInit(void); +void windowsCBNotice(LG_ClipboardData type); +void windowsCBRelease(void); +void windowsCBRequest(LG_ClipboardRequest request, LG_ClipboardData type); +void windowsCBRequestReady(LG_ClipboardRequest request); +void windowsCBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); + +#endif diff --git a/client/displayservers/Windows/windows.c b/client/displayservers/Windows/windows.c index 4ce064f5..79612cc3 100644 --- a/client/displayservers/Windows/windows.c +++ b/client/displayservers/Windows/windows.c @@ -54,6 +54,8 @@ #include #include +#include "clipboard.h" + #define LG_WINDOW_CLASS L"LookingGlassClientWindow" enum WindowsMessage @@ -849,6 +851,7 @@ static LRESULT handleCustomMessage(UINT message, WPARAM wParam, switch (message) { case WM_LG_SHUTDOWN: + windowsCBEventShutdown(); eventSourceFree(); setKeyboardGrab(false); setPointerGrab(false, false); @@ -930,6 +933,11 @@ static LRESULT handleCustomMessage(UINT message, WPARAM wParam, static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) { + LRESULT clipboardResult; + if (windowsCBHandleMessage( + window, message, wParam, lParam, &clipboardResult)) + return clipboardResult; + if (message >= WM_LG_SHUTDOWN && message <= WM_LG_EVENT_SOURCE) return handleCustomMessage(message, wParam, lParam); @@ -1887,4 +1895,10 @@ struct LG_DisplayServerOps LGDS_Windows = .getFullscreen = windowsGetFullscreen, .setFullscreen = windowsSetFullscreen, .minimize = windowsMinimize, + .cbInit = windowsCBInit, + .cbNotice = windowsCBNotice, + .cbRelease = windowsCBRelease, + .cbRequest = windowsCBRequest, + .cbRequestReady = windowsCBRequestReady, + .cbRequestCancel = windowsCBRequestCancel, };