mirror of
https://github.com/gnif/LookingGlass.git
synced 2024-12-23 22:31:46 +00:00
75a9e38e3a
Since we are using epoll, there is no longer any need to remove O_NONBLOCK with fcntl, and the comment is also no longer relevant.
515 lines
12 KiB
C
515 lines
12 KiB
C
/*
|
|
Looking Glass - KVM FrameRelay (KVMFR) Client
|
|
Copyright (C) 2021 Guanzhong Chen (quantum2048@gmail.com)
|
|
Copyright (C) 2021 Tudor Brindus (contact@tbrindus.ca)
|
|
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 "wayland.h"
|
|
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <sys/epoll.h>
|
|
#include <unistd.h>
|
|
#include <wayland-client.h>
|
|
|
|
#include "app.h"
|
|
#include "common/debug.h"
|
|
|
|
static const char * textMimetypes[] =
|
|
{
|
|
"text/plain",
|
|
"text/plain;charset=utf-8",
|
|
"TEXT",
|
|
"STRING",
|
|
"UTF8_STRING",
|
|
NULL,
|
|
};
|
|
|
|
static const char * pngMimetypes[] =
|
|
{
|
|
"image/png",
|
|
NULL,
|
|
};
|
|
|
|
static const char * bmpMimetypes[] =
|
|
{
|
|
"image/bmp",
|
|
"image/x-bmp",
|
|
"image/x-MS-bmp",
|
|
"image/x-win-bitmap",
|
|
NULL,
|
|
};
|
|
|
|
static const char * tiffMimetypes[] =
|
|
{
|
|
"image/tiff",
|
|
NULL,
|
|
};
|
|
|
|
static const char * jpegMimetypes[] =
|
|
{
|
|
"image/jpeg",
|
|
NULL,
|
|
};
|
|
|
|
static const char ** cbTypeToMimetypes(enum LG_ClipboardData type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case LG_CLIPBOARD_DATA_TEXT:
|
|
return textMimetypes;
|
|
case LG_CLIPBOARD_DATA_PNG:
|
|
return pngMimetypes;
|
|
case LG_CLIPBOARD_DATA_BMP:
|
|
return bmpMimetypes;
|
|
case LG_CLIPBOARD_DATA_TIFF:
|
|
return tiffMimetypes;
|
|
case LG_CLIPBOARD_DATA_JPEG:
|
|
return jpegMimetypes;
|
|
default:
|
|
DEBUG_ERROR("invalid clipboard type");
|
|
abort();
|
|
}
|
|
}
|
|
|
|
static bool containsMimetype(const char ** mimetypes,
|
|
const char * needle)
|
|
{
|
|
for (const char ** mimetype = mimetypes; *mimetype; mimetype++)
|
|
if (!strcmp(needle, *mimetype))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool mimetypeEndswith(const char * mimetype, const char * what)
|
|
{
|
|
size_t mimetypeLen = strlen(mimetype);
|
|
size_t whatLen = strlen(what);
|
|
|
|
if (mimetypeLen < whatLen)
|
|
return false;
|
|
|
|
return !strcmp(mimetype + mimetypeLen - whatLen, what);
|
|
}
|
|
|
|
static bool isTextMimetype(const char * mimetype)
|
|
{
|
|
if (containsMimetype(textMimetypes, mimetype))
|
|
return true;
|
|
|
|
char * text = "text/";
|
|
if (!strncmp(mimetype, text, strlen(text)))
|
|
return true;
|
|
|
|
if (mimetypeEndswith(mimetype, "script") ||
|
|
mimetypeEndswith(mimetype, "xml") ||
|
|
mimetypeEndswith(mimetype, "yaml"))
|
|
return true;
|
|
|
|
if (strstr(mimetype, "json"))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static enum LG_ClipboardData mimetypeToCbType(const char * mimetype)
|
|
{
|
|
if (isTextMimetype(mimetype))
|
|
return LG_CLIPBOARD_DATA_TEXT;
|
|
|
|
if (containsMimetype(pngMimetypes, mimetype))
|
|
return LG_CLIPBOARD_DATA_PNG;
|
|
|
|
if (containsMimetype(bmpMimetypes, mimetype))
|
|
return LG_CLIPBOARD_DATA_BMP;
|
|
|
|
if (containsMimetype(tiffMimetypes, mimetype))
|
|
return LG_CLIPBOARD_DATA_TIFF;
|
|
|
|
if (containsMimetype(jpegMimetypes, mimetype))
|
|
return LG_CLIPBOARD_DATA_JPEG;
|
|
|
|
return LG_CLIPBOARD_DATA_NONE;
|
|
}
|
|
|
|
static bool isOptionalImageCbtype(enum LG_ClipboardData type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case LG_CLIPBOARD_DATA_TEXT:
|
|
case LG_CLIPBOARD_DATA_PNG:
|
|
return false;
|
|
case LG_CLIPBOARD_DATA_BMP:
|
|
case LG_CLIPBOARD_DATA_TIFF:
|
|
case LG_CLIPBOARD_DATA_JPEG:
|
|
return true;
|
|
default:
|
|
DEBUG_ERROR("invalid clipboard type");
|
|
abort();
|
|
}
|
|
}
|
|
|
|
// Destination client handlers.
|
|
|
|
static void dataOfferHandleOffer(void * data, struct wl_data_offer * offer,
|
|
const char * mimetype)
|
|
{
|
|
enum LG_ClipboardData type = mimetypeToCbType(mimetype);
|
|
// We almost never prefer text/html, as that's used to represent rich text.
|
|
// Since we can't copy or paste rich text, we should instead prefer actual
|
|
// images or plain text.
|
|
// We also prefer PNG over other image types as it's the only type guaranteed
|
|
// to be supported by the spice agent.
|
|
if (type != LG_CLIPBOARD_DATA_NONE &&
|
|
(wlCb.pendingType == LG_CLIPBOARD_DATA_NONE ||
|
|
strstr(wlCb.pendingMimetype, "html") ||
|
|
(type == LG_CLIPBOARD_DATA_PNG &&
|
|
isOptionalImageCbtype(wlCb.pendingType))))
|
|
{
|
|
wlCb.pendingType = type;
|
|
if (wlCb.pendingMimetype)
|
|
free(wlCb.pendingMimetype);
|
|
wlCb.pendingMimetype = strdup(mimetype);
|
|
}
|
|
|
|
if (!strcmp(mimetype, wlCb.lgMimetype))
|
|
wlCb.isSelfCopy = true;
|
|
}
|
|
|
|
static void dataOfferHandleSourceActions(void * data,
|
|
struct wl_data_offer * offer, uint32_t sourceActions)
|
|
{
|
|
// Do nothing.
|
|
}
|
|
|
|
static void dataOfferHandleAction(void * data, struct wl_data_offer * offer,
|
|
uint32_t dndAction)
|
|
{
|
|
// Do nothing.
|
|
}
|
|
|
|
static const struct wl_data_offer_listener dataOfferListener = {
|
|
.offer = dataOfferHandleOffer,
|
|
.source_actions = dataOfferHandleSourceActions,
|
|
.action = dataOfferHandleAction,
|
|
};
|
|
|
|
static void dataDeviceHandleDataOffer(void * data,
|
|
struct wl_data_device * dataDevice, struct wl_data_offer * offer)
|
|
{
|
|
wlCb.pendingType = LG_CLIPBOARD_DATA_NONE;
|
|
wlCb.isSelfCopy = false;
|
|
wl_data_offer_add_listener(offer, &dataOfferListener, NULL);
|
|
}
|
|
|
|
static void clipboardReadCancel(struct ClipboardRead * data, bool freeBuf)
|
|
{
|
|
waylandPollUnregister(data->fd);
|
|
close(data->fd);
|
|
wl_data_offer_destroy(data->offer);
|
|
if (freeBuf)
|
|
free(data->buf);
|
|
free(data);
|
|
wlCb.currentRead = NULL;
|
|
}
|
|
|
|
static void clipboardReadCallback(uint32_t events, void * opaque)
|
|
{
|
|
struct ClipboardRead * data = opaque;
|
|
if (events & EPOLLERR)
|
|
{
|
|
clipboardReadCancel(data, true);
|
|
return;
|
|
}
|
|
|
|
ssize_t result = read(data->fd, data->buf + data->numRead, data->size - data->numRead);
|
|
if (result < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to read from clipboard: %s", strerror(errno));
|
|
clipboardReadCancel(data, true);
|
|
return;
|
|
}
|
|
|
|
if (result == 0)
|
|
{
|
|
data->buf[data->numRead] = 0;
|
|
wlCb.stashedType = data->type;
|
|
wlCb.stashedSize = data->numRead;
|
|
wlCb.stashedContents = data->buf;
|
|
|
|
clipboardReadCancel(data, false);
|
|
app_clipboardNotifyTypes(&wlCb.stashedType, 1);
|
|
app_clipboardNotifySize(wlCb.stashedType, 0);
|
|
return;
|
|
}
|
|
|
|
data->numRead += result;
|
|
if (data->numRead >= data->size)
|
|
{
|
|
data->size *= 2;
|
|
void * nbuf = realloc(data->buf, data->size);
|
|
if (!nbuf) {
|
|
DEBUG_ERROR("Failed to realloc clipboard buffer: %s", strerror(errno));
|
|
clipboardReadCancel(data, true);
|
|
return;
|
|
}
|
|
|
|
data->buf = nbuf;
|
|
}
|
|
}
|
|
|
|
static void dataDeviceHandleSelection(void * opaque,
|
|
struct wl_data_device * dataDevice, struct wl_data_offer * offer)
|
|
{
|
|
if (wlCb.pendingType == LG_CLIPBOARD_DATA_NONE || wlCb.isSelfCopy || !offer)
|
|
return;
|
|
|
|
if (wlCb.currentRead)
|
|
clipboardReadCancel(wlCb.currentRead, true);
|
|
|
|
int fds[2];
|
|
if (pipe(fds) < 0)
|
|
{
|
|
DEBUG_ERROR("Failed to get a clipboard pipe: %s", strerror(errno));
|
|
abort();
|
|
}
|
|
|
|
wl_data_offer_receive(offer, wlCb.pendingMimetype, fds[1]);
|
|
close(fds[1]);
|
|
free(wlCb.pendingMimetype);
|
|
wlCb.pendingMimetype = NULL;
|
|
|
|
wl_display_roundtrip(wlWm.display);
|
|
|
|
if (wlCb.stashedContents)
|
|
{
|
|
free(wlCb.stashedContents);
|
|
wlCb.stashedContents = NULL;
|
|
}
|
|
|
|
struct ClipboardRead * data = malloc(sizeof(struct ClipboardRead));
|
|
if (!data)
|
|
{
|
|
DEBUG_ERROR("Failed to allocate memory to read clipboard");
|
|
close(fds[0]);
|
|
return;
|
|
}
|
|
|
|
data->fd = fds[0];
|
|
data->size = 4096;
|
|
data->numRead = 0;
|
|
data->buf = malloc(data->size);
|
|
data->offer = offer;
|
|
data->type = wlCb.pendingType;
|
|
|
|
if (!data->buf)
|
|
{
|
|
DEBUG_ERROR("Failed to allocate memory to receive clipboard data");
|
|
close(data->fd);
|
|
free(data);
|
|
return;
|
|
}
|
|
|
|
if (!waylandPollRegister(data->fd, clipboardReadCallback, data, EPOLLIN))
|
|
{
|
|
DEBUG_ERROR("Failed to register clipboard read into epoll: %s", strerror(errno));
|
|
close(data->fd);
|
|
free(data->buf);
|
|
free(data);
|
|
}
|
|
|
|
wlCb.currentRead = data;
|
|
}
|
|
|
|
static void dataDeviceHandleEnter(void * data, struct wl_data_device * device,
|
|
uint32_t serial, struct wl_surface * surface, wl_fixed_t sxW, wl_fixed_t syW,
|
|
struct wl_data_offer * offer)
|
|
{
|
|
// Do nothing.
|
|
}
|
|
|
|
static void dataDeviceHandleMotion(void * data, struct wl_data_device * device,
|
|
uint32_t time, wl_fixed_t sxW, wl_fixed_t syW)
|
|
{
|
|
// Do nothing.
|
|
}
|
|
|
|
static void dataDeviceHandleLeave(void * data, struct wl_data_device * device)
|
|
{
|
|
// Do nothing.
|
|
}
|
|
|
|
static const struct wl_data_device_listener dataDeviceListener = {
|
|
.data_offer = dataDeviceHandleDataOffer,
|
|
.selection = dataDeviceHandleSelection,
|
|
.enter = dataDeviceHandleEnter,
|
|
.motion = dataDeviceHandleMotion,
|
|
.leave = dataDeviceHandleLeave,
|
|
};
|
|
|
|
bool waylandCBInit(void)
|
|
{
|
|
memset(&wlCb, 0, sizeof(wlCb));
|
|
|
|
if (!wlWm.dataDeviceManager)
|
|
{
|
|
DEBUG_ERROR("Missing wl_data_device_manager interface (version 3+)");
|
|
return false;
|
|
}
|
|
|
|
wlCb.dataDevice = wl_data_device_manager_get_data_device(
|
|
wlWm.dataDeviceManager, wlWm.seat);
|
|
if (!wlCb.dataDevice)
|
|
{
|
|
DEBUG_ERROR("Failed to get data device");
|
|
return false;
|
|
}
|
|
|
|
wlCb.stashedType = LG_CLIPBOARD_DATA_NONE;
|
|
wl_data_device_add_listener(wlCb.dataDevice, &dataDeviceListener, NULL);
|
|
|
|
snprintf(wlCb.lgMimetype, sizeof(wlCb.lgMimetype),
|
|
"application/x-looking-glass-copy;pid=%d", getpid());
|
|
|
|
return true;
|
|
}
|
|
|
|
void waylandCBRequest(LG_ClipboardData type)
|
|
{
|
|
// We only notified once, so it must be this.
|
|
assert(type == wlCb.stashedType);
|
|
app_clipboardData(wlCb.stashedType, wlCb.stashedContents, wlCb.stashedSize);
|
|
}
|
|
|
|
struct ClipboardWrite
|
|
{
|
|
int fd;
|
|
size_t pos;
|
|
struct CountedBuffer * buffer;
|
|
};
|
|
|
|
static void clipboardWriteCallback(uint32_t events, void * opaque)
|
|
{
|
|
struct ClipboardWrite * data = opaque;
|
|
if (events & EPOLLERR)
|
|
goto error;
|
|
|
|
ssize_t written = write(data->fd, data->buffer->data + data->pos, data->buffer->size - data->pos);
|
|
if (written < 0)
|
|
{
|
|
if (errno != EPIPE)
|
|
DEBUG_ERROR("Failed to write clipboard data: %s", strerror(errno));
|
|
goto error;
|
|
}
|
|
|
|
data->pos += written;
|
|
if (data->pos < data->buffer->size)
|
|
return;
|
|
|
|
error:
|
|
waylandPollUnregister(data->fd);
|
|
close(data->fd);
|
|
countedBufferRelease(&data->buffer);
|
|
free(data);
|
|
}
|
|
|
|
static void dataSourceHandleSend(void * data, struct wl_data_source * source,
|
|
const char * mimetype, int fd)
|
|
{
|
|
struct WCBTransfer * transfer = (struct WCBTransfer *) data;
|
|
if (containsMimetype(transfer->mimetypes, mimetype))
|
|
{
|
|
struct ClipboardWrite * data = malloc(sizeof(struct ClipboardWrite));
|
|
if (!data)
|
|
{
|
|
DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite");
|
|
goto error;
|
|
}
|
|
|
|
data->fd = fd;
|
|
data->pos = 0;
|
|
data->buffer = transfer->data;
|
|
countedBufferAddRef(transfer->data);
|
|
waylandPollRegister(fd, clipboardWriteCallback, data, EPOLLOUT);
|
|
return;
|
|
}
|
|
|
|
error:
|
|
close(fd);
|
|
}
|
|
|
|
static void dataSourceHandleCancelled(void * data,
|
|
struct wl_data_source * source)
|
|
{
|
|
struct WCBTransfer * transfer = (struct WCBTransfer *) data;
|
|
countedBufferRelease(&transfer->data);
|
|
free(transfer);
|
|
wl_data_source_destroy(source);
|
|
}
|
|
|
|
static const struct wl_data_source_listener dataSourceListener = {
|
|
.send = dataSourceHandleSend,
|
|
.cancelled = dataSourceHandleCancelled,
|
|
};
|
|
|
|
static void waylandCBReplyFn(void * opaque, LG_ClipboardData type,
|
|
uint8_t * data, uint32_t size)
|
|
{
|
|
struct WCBTransfer * transfer = malloc(sizeof(struct WCBTransfer));
|
|
if (!transfer)
|
|
{
|
|
DEBUG_ERROR("Out of memory when allocating WCBTransfer");
|
|
return;
|
|
}
|
|
|
|
transfer->mimetypes = cbTypeToMimetypes(type);
|
|
transfer->data = countedBufferNew(size);
|
|
if (!transfer->data)
|
|
{
|
|
DEBUG_ERROR("Out of memory when allocating clipboard buffer");
|
|
free(transfer);
|
|
return;
|
|
}
|
|
memcpy(transfer->data->data, data, size);
|
|
|
|
struct wl_data_source * source =
|
|
wl_data_device_manager_create_data_source(wlWm.dataDeviceManager);
|
|
wl_data_source_add_listener(source, &dataSourceListener, transfer);
|
|
for (const char ** mimetype = transfer->mimetypes; *mimetype; mimetype++)
|
|
wl_data_source_offer(source, *mimetype);
|
|
wl_data_source_offer(source, wlCb.lgMimetype);
|
|
|
|
wl_data_device_set_selection(wlCb.dataDevice, source,
|
|
wlWm.keyboardEnterSerial);
|
|
}
|
|
|
|
void waylandCBNotice(LG_ClipboardData type)
|
|
{
|
|
wlCb.haveRequest = true;
|
|
wlCb.type = type;
|
|
app_clipboardRequest(waylandCBReplyFn, NULL);
|
|
}
|
|
|
|
void waylandCBRelease(void)
|
|
{
|
|
wlCb.haveRequest = false;
|
|
}
|