diff options
Diffstat (limited to 'src/VBox/Additions/x11/VBoxClient')
16 files changed, 9312 insertions, 0 deletions
diff --git a/src/VBox/Additions/x11/VBoxClient/Makefile.kmk b/src/VBox/Additions/x11/VBoxClient/Makefile.kmk new file mode 100644 index 00000000..52c9ca1e --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/Makefile.kmk @@ -0,0 +1,184 @@ +# $Id: Makefile.kmk $ +## @file +# Sub-Makefile for the VirtualBox Guest Addition X11 Client. +# + +# +# Copyright (C) 2006-2020 Oracle Corporation +# +# This file is part of VirtualBox Open Source Edition (OSE), as +# available from http://www.virtualbox.org. This file is free software; +# you can redistribute it and/or modify it under the terms of the GNU +# General Public License (GPL) as published by the Free Software +# Foundation, in version 2 as it comes in the "COPYING" file of the +# VirtualBox OSE distribution. VirtualBox OSE is distributed in the +# hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# Use header files from our tree for randr and xinerama. and don't link but rather dlopen libXrand +VBOX_WITH_DISTRO_XRAND_XINERAMA= + +# +# VBoxClient - clipboard and seamless. +# +PROGRAMS += VBoxClient +# +# Please make sure that you grep the source tree and modify all occurences accordingly +# if you rename the following program name. +# +PROGRAMS += VBoxDRMClient + +VBoxClient_TEMPLATE = NewVBoxGuestR3Exe +VBoxClient_DEFS += VBOX_X11_CLIPBOARD VBOX_WITH_HGCM +VBoxClient_DEFS += VBOX_BUILD_TARGET=\"$(KBUILD_TARGET).$(KBUILD_TARGET_ARCH)\" +ifdef VBOX_WITH_DBUS + VBoxClient_DEFS += VBOX_WITH_DBUS +endif + +VBoxClient_DEFS.linux += _GNU_SOURCE +VBoxClient_INCS = $(VBOX_GRAPHICS_INCS) +VBoxClient_INCS += ../x11include/panoramiXproto-1.1 +VBoxClient_INCS += ../x11include/libXrandr-1.5.2 +VBoxClient_INCS += ../x11include/randrproto-1.6.0 +VBoxClient_SOURCES = \ + main.cpp \ + display-svga-x11.cpp \ + seamless.cpp \ + seamless-x11.cpp \ + logging.cpp \ + hostversion.cpp + +VBoxDRMClient_TEMPLATE = NewVBoxGuestR3Exe +VBoxDRMClient_DEFS += VBOX_BUILD_TARGET=\"$(KBUILD_TARGET).$(KBUILD_TARGET_ARCH)\" +VBoxDRMClient_SOURCES = \ + display-drm.cpp \ + logging.cpp + + +VBoxClient_SOURCES.linux = \ + chk_stubs.c +VBoxClient_LIBPATH = \ + $(VBOX_LIBPATH32_X11) +VBoxClient_LIBS.freebsd = \ + iconv +VBoxClient_LIBS.linux = \ + dl +VBoxClient_LIBS.netbsd = \ + crypt +VBoxClient_LIBS.solaris = \ + dl +VBoxClient_LIBS = \ + X11 Xt Xext Xmu +ifdef VBOX_WITH_DISTRO_XRAND_XINERAMA + VBoxClient_DEFS += WITH_DISTRO_XRAND_XINERAMA + VBoxClient_LIBS += Xrandr +endif + +# XXX: -L comes from the template, but not rpath +VBoxClient_LDFLAGS.netbsd = \ + -Wl,-rpath /usr/X11R7/lib + +ifdef VBOX_WITH_DRAG_AND_DROP + ifdef VBOX_DND_WITH_XTEST + VBoxClient_DEFS += VBOX_DND_WITH_XTEST + VBoxClient_LIBS += \ + Xtst + endif +endif + +# This forces the memcpy references in the static libraries to go to +# __wrap_memcpy, which we can wrap around memcpy@GLIBC_2.2.5. I do not know +# how else to do that without recompiling or implementing our own memcpy. +ifeq ($(KBUILD_TARGET),linux) +VBoxClient_LDFLAGS.amd64 += \ + -Wl,--wrap=memcpy +endif + +ifdef VBOX_WITH_GUEST_PROPS +VBoxClient_DEFS += VBOX_WITH_GUEST_PROPS +endif + +ifdef VBOX_WITH_DRAG_AND_DROP +VBoxClient_DEFS += \ + VBOX_WITH_DRAG_AND_DROP \ + $(if $(VBOX_WITH_DRAG_AND_DROP_GH),VBOX_WITH_DRAG_AND_DROP_GH,) +VBoxClient_SOURCES += \ + draganddrop.cpp +VBoxClient_LIBS += \ + $(VBOX_LIB_VBGL_R3) \ + $(PATH_STAGE_LIB)/additions/VBoxDnDGuestR3Lib$(VBOX_SUFF_LIB) +endif + +ifdef VBOX_WITH_SHARED_CLIPBOARD + VBoxClient_DEFS += \ + VBOX_WITH_SHARED_CLIPBOARD \ + $(if $(VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS),VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS,) + VBoxClient_SOURCES += \ + $(PATH_ROOT)/src/VBox/GuestHost/SharedClipboard/clipboard-common.cpp \ + $(PATH_ROOT)/src/VBox/GuestHost/SharedClipboard/clipboard-x11.cpp \ + clipboard.cpp + ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + VBoxClient_DEFS += VBOX_WITH_SHARED_CLIPBOARD_GUEST + VBoxClient_SOURCES += \ + $(PATH_ROOT)/src/VBox/GuestHost/SharedClipboard/clipboard-transfers.cpp \ + $(PATH_ROOT)/src/VBox/GuestHost/SharedClipboard/ClipboardPath.cpp + ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE + VBoxClient_DEFS += VBOX_WITH_SHARED_CLIPBOARD_FUSE + # @todo Make this dynamic loading more generic. + VBoxClient_SOURCES += \ + $(PATH_ROOT)/src/VBox/ImageMounter/vboximg-mount/fuse.cpp \ + clipboard-fuse.cpp + # @todo Ditto. + VBoxClient_INCS += \ + $(PATH_ROOT)/src/VBox/ImageMounter/vboximg-mount + endif + endif +endif + +if defined(VBOX_WITH_TESTCASES) && !defined(VBOX_ONLY_ADDITIONS) && !defined(VBOX_ONLY_SDK) + if1of ($(KBUILD_TARGET), freebsd linux netbsd openbsd solaris) + +# Set this in LocalConfig.kmk if you are working on the X11 clipboard service +# to automatically run the unit test at build time. +# OTHERS += $(tstSeamlessX11-auto_0_OUTDIR)/tstSeamlessX11-auto.run + + PROGRAMS += tstSeamlessX11-auto + tstSeamlessX11-auto_TEMPLATE = VBOXR3TSTEXE + tstSeamlessX11-auto_SOURCES = \ + testcase/tstSeamlessX11-auto.cpp \ + seamless-x11.cpp + tstSeamlessX11-auto_DEFS = TESTCASE + tstSeamlessX11-auto_LIBS = \ + $(LIB_RUNTIME) + + TESTING += $(tstSeamlessX11-auto_0_OUTDIR)/tstSeamlessX11-auto +$$(tstSeamlessX11-auto_0_OUTDIR)/tstSeamlessX11-auto.run: \ + $$(tstSeamlessX11-auto_1_STAGE_TARGET) + export VBOX_LOG_DEST=nofile; $(tstSeamlessX11-auto_1_STAGE_TARGET) quiet + $(QUIET)$(APPEND) -t "$@" "done" + + # + # Additional testcase designed to be run manually, which initiates and starts the Linux + # guest client part of the seamless additions in the host, faking seamless events from + # the host and writing information about guest events to standard output. + # + PROGRAMS += tstSeamlessX11 + tstSeamlessX11_TEMPLATE = VBOXR3TSTEXE + tstSeamlessX11_SOURCES = \ + testcase/tstSeamlessX11.cpp \ + seamless.cpp \ + seamless-x11.cpp + tstSeamlessX11_LIBPATH = \ + $(VBOX_LIBPATH_X11) + tstSeamlessX11_LIBS = \ + $(LIB_RUNTIME) \ + Xext \ + Xmu \ + X11 + endif +endif + +include $(FILE_KBUILD_SUB_FOOTER) diff --git a/src/VBox/Additions/x11/VBoxClient/VBoxClient.h b/src/VBox/Additions/x11/VBoxClient/VBoxClient.h new file mode 100644 index 00000000..83b9072c --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/VBoxClient.h @@ -0,0 +1,84 @@ +/* $Id: VBoxClient.h $ */ +/** @file + * + * VirtualBox additions user session daemon. + */ + +/* + * Copyright (C) 2006-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +#ifndef GA_INCLUDED_SRC_x11_VBoxClient_VBoxClient_h +#define GA_INCLUDED_SRC_x11_VBoxClient_VBoxClient_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <VBox/log.h> +#include <iprt/cpp/utils.h> +#include <iprt/string.h> + +void VBClLogInfo(const char *pszFormat, ...); +void VBClLogError(const char *pszFormat, ...); +void VBClLogFatalError(const char *pszFormat, ...); +void VBClLogDestroy(void); +int VBClLogCreate(const char *pszLogFile); + +/** Call clean-up for the current service and exit. */ +extern void VBClCleanUp(bool fExit = true); + +/** A simple interface describing a service. VBoxClient will run exactly one + * service per invocation. */ +struct VBCLSERVICE +{ + /** Returns the (friendly) name of the service. */ + const char *(*getName)(void); + /** Get the services default path to pidfile, relative to $HOME */ + /** @todo Should this also have a component relative to the X server number? + */ + const char *(*getPidFilePath)(void); + /** Special initialisation, if needed. @a pause and @a resume are + * guaranteed not to be called until after this returns. */ + int (*init)(struct VBCLSERVICE **ppInterface); + /** Run the service main loop */ + int (*run)(struct VBCLSERVICE **ppInterface, bool fDaemonised); + /** Clean up any global resources before we shut down hard. The last calls + * to @a pause and @a resume are guaranteed to finish before this is called. + */ + void (*cleanup)(struct VBCLSERVICE **ppInterface); +}; + +/** Default handler for various struct VBCLSERVICE member functions. */ +DECLINLINE(int) VBClServiceDefaultHandler(struct VBCLSERVICE **pSelf) +{ + RT_NOREF1(pSelf); + return VINF_SUCCESS; +} + +/** Default handler for the struct VBCLSERVICE clean-up member function. + * Usually used because the service is cleaned up automatically when the user + * process/X11 exits. */ +DECLINLINE(void) VBClServiceDefaultCleanup(struct VBCLSERVICE **ppInterface) +{ + RT_NOREF(ppInterface); +} + +extern struct VBCLSERVICE **VBClGetClipboardService(); +extern struct VBCLSERVICE **VBClGetSeamlessService(); +extern struct VBCLSERVICE **VBClGetHostVersionService(); +#ifdef VBOX_WITH_DRAG_AND_DROP +extern struct VBCLSERVICE **VBClGetDragAndDropService(); +#endif /* VBOX_WITH_DRAG_AND_DROP */ +extern struct VBCLSERVICE **VBClCheck3DService(); +extern struct VBCLSERVICE **VBClDisplaySVGAService(); +extern struct VBCLSERVICE **VBClDisplaySVGAX11Service(); + +#endif /* !GA_INCLUDED_SRC_x11_VBoxClient_VBoxClient_h */ diff --git a/src/VBox/Additions/x11/VBoxClient/chk_stubs.c b/src/VBox/Additions/x11/VBoxClient/chk_stubs.c new file mode 100644 index 00000000..5139c0bd --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/chk_stubs.c @@ -0,0 +1,59 @@ +/* $Id: chk_stubs.c $ */ +/** @file + * glibc stubs for the VirtualBox Guest Addition X11 Client. + */ + +/* + * Copyright (C) 2018-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +/* If we want the binary to be usable with glibc 2.3, we have to prevent + VBoxClient from containing later symbols. This includes resolution of + symbols from supc++ and gcc_eh. */ + +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <unistd.h> + +extern int __sprintf_chk(char *psz, int fFlags, size_t cb, const char *pszFormat, ...); +int __sprintf_chk(char *psz, int fFlags, size_t cb, const char *pszFormat, ...) +{ + int rc; + va_list va; + + (void)fFlags; + va_start(va, pszFormat); + rc = vsnprintf(psz, cb, pszFormat, va); + va_end(va); + return rc; +} + +extern void __stack_chk_fail(void); +void __stack_chk_fail(void) +{ + fprintf(stderr, "Stack check failed!\n"); + _exit(1); +} + +#ifdef __x86_64 +/* Furthermore, wrap references to memcpy to force them to go to the right + * version. We are forced to do it this way because the shared libraries + * supc++ and gcc_eh contain references which we cannot change. */ + +extern void *__wrap_memcpy(void *dest, const void *src, size_t n); + +asm (".symver memcpy, memcpy@GLIBC_2.2.5"); +void *__wrap_memcpy(void *dest, const void *src, size_t n) +{ + return memcpy(dest, src, n); +} +#endif diff --git a/src/VBox/Additions/x11/VBoxClient/clipboard.cpp b/src/VBox/Additions/x11/VBoxClient/clipboard.cpp new file mode 100644 index 00000000..578c0b0b --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/clipboard.cpp @@ -0,0 +1,536 @@ +/** $Id: clipboard.cpp $ */ +/** @file + * Guest Additions - X11 Shared Clipboard. + */ + +/* + * Copyright (C) 2007-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/alloc.h> +#include <iprt/asm.h> +#include <iprt/assert.h> +#ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE +# include <iprt/dir.h> +#endif +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/string.h> +#include <iprt/path.h> +#include <iprt/process.h> +#include <iprt/semaphore.h> + +#include <VBox/log.h> +#include <VBox/VBoxGuestLib.h> +#include <VBox/HostServices/VBoxClipboardSvc.h> +#include <VBox/GuestHost/SharedClipboard.h> +#include <VBox/GuestHost/SharedClipboard-x11.h> + +#include "VBoxClient.h" + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE +# include "clipboard-fuse.h" +#endif + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE +typedef struct _SHCLCTXFUSE +{ + RTTHREAD Thread; +} SHCLCTXFUSE; +#endif /* VBOX_WITH_SHARED_CLIPBOARD_FUSE */ + +/** + * Global clipboard context information. + */ +struct SHCLCONTEXT +{ + /** Client command context */ + VBGLR3SHCLCMDCTX CmdCtx; +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + /** Associated transfer data. */ + SHCLTRANSFERCTX TransferCtx; +# ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE + SHCLCTXFUSE FUSE; +# endif +#endif + /** X11 clipboard context. */ + SHCLX11CTX X11; +}; + +/** Only one client is supported. There seems to be no need for more clients. */ +static SHCLCONTEXT g_Ctx; + + +/** + * Callback implementation for getting clipboard data from the host. + * + * @returns VBox status code. VERR_NO_DATA if no data available. + * @param pCtx Our context information. + * @param Format The format of the data being requested. + * @param ppv On success and if pcb > 0, this will point to a buffer + * to be freed with RTMemFree containing the data read. + * @param pcb On success, this contains the number of bytes of data returned. + */ +DECLCALLBACK(int) ShClX11RequestDataForX11Callback(PSHCLCONTEXT pCtx, SHCLFORMAT Format, void **ppv, uint32_t *pcb) +{ + RT_NOREF(pCtx); + + LogFlowFunc(("Format=0x%x\n", Format)); + + int rc = VINF_SUCCESS; + + uint32_t cbRead = 0; + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + if (Format == VBOX_SHCL_FMT_URI_LIST) + { + //rc = VbglR3ClipboardRootListRead() + } + else +#endif + { + uint32_t cbData = _4K; /** @todo Make this dynamic. */ + void *pvData = RTMemAlloc(cbData); + if (pvData) + { + rc = VbglR3ClipboardReadDataEx(&pCtx->CmdCtx, Format, pvData, cbData, &cbRead); + } + else + rc = VERR_NO_MEMORY; + + /* + * A return value of VINF_BUFFER_OVERFLOW tells us to try again with a + * larger buffer. The size of the buffer needed is placed in *pcb. + * So we start all over again. + */ + if (rc == VINF_BUFFER_OVERFLOW) + { + /* cbRead contains the size required. */ + + cbData = cbRead; + pvData = RTMemRealloc(pvData, cbRead); + if (pvData) + { + rc = VbglR3ClipboardReadDataEx(&pCtx->CmdCtx, Format, pvData, cbData, &cbRead); + if (rc == VINF_BUFFER_OVERFLOW) + rc = VERR_BUFFER_OVERFLOW; + } + else + rc = VERR_NO_MEMORY; + } + + if (!cbRead) + rc = VERR_NO_DATA; + + if (RT_SUCCESS(rc)) + { + *pcb = cbRead; /* Actual bytes read. */ + *ppv = pvData; + } + else + { + /* + * Catch other errors. This also catches the case in which the buffer was + * too small a second time, possibly because the clipboard contents + * changed half-way through the operation. Since we can't say whether or + * not this is actually an error, we just return size 0. + */ + RTMemFree(pvData); + } + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Opaque data structure describing a request from the host for clipboard + * data, passed in when the request is forwarded to the X11 backend so that + * it can be completed correctly. + */ +struct CLIPREADCBREQ +{ + /** The data format that was requested. */ + SHCLFORMAT Format; +}; + +/** + * Tell the host that new clipboard formats are available. + * + * @param pCtx Our context information. + * @param fFormats The formats to report. + */ +DECLCALLBACK(void) ShClX11ReportFormatsCallback(PSHCLCONTEXT pCtx, SHCLFORMATS fFormats) +{ + RT_NOREF(pCtx); + + LogFlowFunc(("Formats=0x%x\n", fFormats)); + + int rc2 = VbglR3ClipboardReportFormats(pCtx->CmdCtx.idClient, fFormats); + RT_NOREF(rc2); + LogFlowFuncLeaveRC(rc2); +} + +/** + * This is called by the backend to tell us that a request for data from + * X11 has completed. + * + * @param pCtx Our context information. + * @param rcCompletion The completion status of the request. + * @param pReq The request structure that we passed in when we started + * the request. We RTMemFree() this in this function. + * @param pv The clipboard data returned from X11 if the request succeeded (see @a rc). + * @param cb The size of the data in @a pv. + */ +DECLCALLBACK(void) ShClX11RequestFromX11CompleteCallback(PSHCLCONTEXT pCtx, + int rcCompletion, CLIPREADCBREQ *pReq, void *pv, uint32_t cb) +{ + LogFlowFunc(("rcCompletion=%Rrc, Format=0x%x, pv=%p, cb=%RU32\n", rcCompletion, pReq->Format, pv, cb)); + + if (RT_SUCCESS(rcCompletion)) /* Only write data if the request succeeded. */ + { + AssertPtrReturnVoid(pv); + AssertReturnVoid(pv); + + rcCompletion = VbglR3ClipboardWriteDataEx(&pCtx->CmdCtx, pReq->Format, pv, cb); + } + + RTMemFree(pReq); + + LogFlowFuncLeaveRC(rcCompletion); +} + +/** + * Connect the guest clipboard to the host. + * + * @returns VBox status code. + */ +static int vboxClipboardConnect(void) +{ + LogFlowFuncEnter(); + + int rc = ShClX11Init(&g_Ctx.X11, &g_Ctx, false /* fHeadless */); + if (RT_SUCCESS(rc)) + { + rc = ShClX11ThreadStart(&g_Ctx.X11, false /* grab */); + if (RT_SUCCESS(rc)) + { + rc = VbglR3ClipboardConnectEx(&g_Ctx.CmdCtx, VBOX_SHCL_GF_0_CONTEXT_ID); + if (RT_FAILURE(rc)) + ShClX11ThreadStop(&g_Ctx.X11); + } + } + else + rc = VERR_NO_MEMORY; + + if (RT_FAILURE(rc)) + { + VBClLogError("Error connecting to host service, rc=%Rrc\n", rc); + + VbglR3ClipboardDisconnectEx(&g_Ctx.CmdCtx); + ShClX11Destroy(&g_Ctx.X11); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * The main loop of our clipboard reader. + */ +int vboxClipboardMain(void) +{ + LogRel(("Worker loop running\n")); + + int rc; + + PSHCLCONTEXT pCtx = &g_Ctx; + + bool fShutdown = false; + + /* The thread waits for incoming messages from the host. */ + for (;;) + { + PVBGLR3CLIPBOARDEVENT pEvent = (PVBGLR3CLIPBOARDEVENT)RTMemAllocZ(sizeof(VBGLR3CLIPBOARDEVENT)); + AssertPtrBreakStmt(pEvent, rc = VERR_NO_MEMORY); + + LogFlowFunc(("Waiting for host message (fUseLegacyProtocol=%RTbool, fHostFeatures=%#RX64) ...\n", + pCtx->CmdCtx.fUseLegacyProtocol, pCtx->CmdCtx.fHostFeatures)); + + uint32_t idMsg = 0; + uint32_t cParms = 0; + rc = VbglR3ClipboardMsgPeekWait(&pCtx->CmdCtx, &idMsg, &cParms, NULL /* pidRestoreCheck */); + if (RT_SUCCESS(rc)) + { +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + rc = VbglR3ClipboardEventGetNextEx(idMsg, cParms, &pCtx->CmdCtx, &pCtx->TransferCtx, pEvent); +#else + rc = VbglR3ClipboardEventGetNext(idMsg, cParms, &pCtx->CmdCtx, pEvent); +#endif + } + + if (RT_FAILURE(rc)) + { + LogFlowFunc(("Getting next event failed with %Rrc\n", rc)); + + VbglR3ClipboardEventFree(pEvent); + pEvent = NULL; + + if (fShutdown) + break; + + /* Wait a bit before retrying. */ + RTThreadSleep(1000); + continue; + } + else + { + AssertPtr(pEvent); + LogFlowFunc(("Event uType=%RU32\n", pEvent->enmType)); + + switch (pEvent->enmType) + { + case VBGLR3CLIPBOARDEVENTTYPE_REPORT_FORMATS: + { + ShClX11ReportFormatsToX11(&g_Ctx.X11, pEvent->u.fReportedFormats); + break; + } + + case VBGLR3CLIPBOARDEVENTTYPE_READ_DATA: + { + /* The host needs data in the specified format. */ + CLIPREADCBREQ *pReq; + pReq = (CLIPREADCBREQ *)RTMemAllocZ(sizeof(CLIPREADCBREQ)); + if (pReq) + { + pReq->Format = pEvent->u.fReadData; + ShClX11ReadDataFromX11(&g_Ctx.X11, pReq->Format, pReq); + } + else + rc = VERR_NO_MEMORY; + break; + } + + case VBGLR3CLIPBOARDEVENTTYPE_QUIT: + { + LogRel2(("Host requested termination\n")); + fShutdown = true; + break; + } + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + case VBGLR3CLIPBOARDEVENTTYPE_TRANSFER_STATUS: + { + /* Nothing to do here. */ + rc = VINF_SUCCESS; + break; + } +#endif + case VBGLR3CLIPBOARDEVENTTYPE_NONE: + { + /* Nothing to do here. */ + rc = VINF_SUCCESS; + break; + } + + default: + { + AssertMsgFailedBreakStmt(("Event type %RU32 not implemented\n", pEvent->enmType), rc = VERR_NOT_SUPPORTED); + } + } + + if (pEvent) + { + VbglR3ClipboardEventFree(pEvent); + pEvent = NULL; + } + } + + if (fShutdown) + break; + } + + LogRel(("Worker loop ended\n")); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE +static DECLCALLBACK(int) vboxClipoardFUSEThread(RTTHREAD hThreadSelf, void *pvUser) +{ + RT_NOREF(hThreadSelf, pvUser); + + VbglR3Init(); + + LogFlowFuncEnter(); + + RTThreadUserSignal(hThreadSelf); + + SHCL_FUSE_OPTS Opts; + RT_ZERO(Opts); + + Opts.fForeground = true; + Opts.fSingleThreaded = false; /** @todo Do we want multithread here? */ + + int rc = RTPathTemp(Opts.szMountPoint, sizeof(Opts.szMountPoint)); + if (RT_SUCCESS(rc)) + { + rc = RTPathAppend(Opts.szMountPoint, sizeof(Opts.szMountPoint), "VBoxSharedClipboard"); + if (RT_SUCCESS(rc)) + { + rc = RTDirCreate(Opts.szMountPoint, 0700, + RTDIRCREATE_FLAGS_NO_SYMLINKS); + if (rc == VERR_ALREADY_EXISTS) + rc = VINF_SUCCESS; + } + } + + if (RT_SUCCESS(rc)) + { + rc = ShClFuseMain(&Opts); + } + else + LogRel(("Error creating FUSE mount directory, rc=%Rrc\n", rc)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +static int vboxClipboardFUSEStart() +{ + LogFlowFuncEnter(); + + PSHCLCONTEXT pCtx = &g_Ctx; + + int rc = RTThreadCreate(&pCtx->FUSE.Thread, vboxClipoardFUSEThread, &pCtx->FUSE, 0, + RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, "SHCLFUSE"); + if (RT_SUCCESS(rc)) + rc = RTThreadUserWait(pCtx->FUSE.Thread, 30 * 1000); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +static int vboxClipboardFUSEStop() +{ + LogFlowFuncEnter(); + + PSHCLCONTEXT pCtx = &g_Ctx; + + int rcThread; + int rc = RTThreadWait(pCtx->FUSE.Thread, 1000, &rcThread); + + LogFlowFuncLeaveRC(rc); + return rc; +} +#endif /* VBOX_WITH_SHARED_CLIPBOARD_FUSE */ + +static const char *getName() +{ + return "Shared Clipboard"; +} + +static const char *getPidFilePath() +{ + return ".vboxclient-clipboard.pid"; +} + +static int init(struct VBCLSERVICE **pSelf) +{ + RT_NOREF(pSelf); + + int rc; + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + rc = ShClTransferCtxInit(&g_Ctx.TransferCtx); +#else + rc = VINF_SUCCESS; +#endif + + LogFlowFuncLeaveRC(rc); + return rc; +} + +static int run(struct VBCLSERVICE **ppInterface, bool fDaemonised) +{ + RT_NOREF(ppInterface, fDaemonised); + + /* Initialise the guest library. */ + int rc = vboxClipboardConnect(); + if (RT_SUCCESS(rc)) + { +#ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE + rc = vboxClipboardFUSEStart(); + if (RT_SUCCESS(rc)) + { +#endif + rc = vboxClipboardMain(); + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_FUSE + int rc2 = vboxClipboardFUSEStop(); + if (RT_SUCCESS(rc)) + rc = rc2; + } +#endif + } + + if (RT_FAILURE(rc)) + VBClLogError("Service terminated abnormally with %Rrc\n", rc); + + if (rc == VERR_HGCM_SERVICE_NOT_FOUND) + rc = VINF_SUCCESS; /* Prevent automatic restart by daemon script if host service not available. */ + + return rc; +} + +static void cleanup(struct VBCLSERVICE **ppInterface) +{ + RT_NOREF(ppInterface); + +#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS + ShClTransferCtxDestroy(&g_Ctx.TransferCtx); +#endif +} + +struct VBCLSERVICE vbclClipboardInterface = +{ + getName, + getPidFilePath, + init, + run, + cleanup +}; + +struct CLIPBOARDSERVICE +{ + struct VBCLSERVICE *pInterface; +}; + +struct VBCLSERVICE **VBClGetClipboardService(void) +{ + struct CLIPBOARDSERVICE *pService = + (struct CLIPBOARDSERVICE *)RTMemAlloc(sizeof(*pService)); + + if (!pService) + VBClLogFatalError("Out of memory\n"); + pService->pInterface = &vbclClipboardInterface; + return &pService->pInterface; +} diff --git a/src/VBox/Additions/x11/VBoxClient/display-drm.cpp b/src/VBox/Additions/x11/VBoxClient/display-drm.cpp new file mode 100644 index 00000000..40ff41ff --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/display-drm.cpp @@ -0,0 +1,281 @@ +/* $Id: display-drm.cpp $ */ +/** @file + * X11 guest client - VMSVGA emulation resize event pass-through to drm guest + * driver. + */ + +/* + * Copyright (C) 2016-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +/* + * Known things to test when changing this code. All assume a guest with VMSVGA + * active and controlled by X11 or Wayland, and Guest Additions installed and + * running, unless otherwise stated. + * - On Linux 4.6 and later guests, VBoxClient --vmsvga should be running as + * root and not as the logged-in user. Dynamic resizing should work for all + * screens in any environment which handles kernel resize notifications, + * including at log-in screens. Test GNOME Shell Wayland and GNOME Shell + * under X.Org or Unity or KDE at the log-in screen and after log-in. + * - Linux 4.10 changed the user-kernel-ABI introduced in 4.6: test both. + * - On other guests (than Linux 4.6 or later) running X.Org Server 1.3 or + * later, VBoxClient --vmsvga should never be running as root, and should run + * (and dynamic resizing and screen enable/disable should work for all + * screens) whenever a user is logged in to a supported desktop environment. + * - On guests running X.Org Server 1.2 or older, VBoxClient --vmsvga should + * never run as root and should run whenever a user is logged in to a + * supported desktop environment. Dynamic resizing should work for the first + * screen, and enabling others should not be possible. + * - When VMSVGA is not enabled, VBoxClient --vmsvga should never stay running. + */ + +#include "VBoxClient.h" + +#include <VBox/VBoxGuestLib.h> + +#include <iprt/assert.h> +#include <iprt/file.h> +#include <iprt/err.h> +#include <iprt/string.h> +#include <iprt/initterm.h> +#include <iprt/message.h> +#include <unistd.h> +#include <stdio.h> + + +/** Maximum number of supported screens. DRM and X11 both limit this to 32. */ +/** @todo if this ever changes, dynamically allocate resizeable arrays in the + * context structure. */ +#define VMW_MAX_HEADS 32 + +/* VMWare kernel driver control parts definitions. */ + +#ifdef RT_OS_LINUX +# include <sys/ioctl.h> +#else /* Solaris and BSDs, in case they ever adopt the DRM driver. */ +# include <sys/ioccom.h> +#endif + +#define DRM_DRIVER_NAME "vmwgfx" + +/** Counter of how often our daemon has been respawned. */ +unsigned g_cRespawn = 0; +/** Logging verbosity level. */ +unsigned g_cVerbosity = 0; + +/** DRM version structure. */ +struct DRMVERSION +{ + int cMajor; + int cMinor; + int cPatchLevel; + size_t cbName; + char *pszName; + size_t cbDate; + char *pszDate; + size_t cbDescription; + char *pszDescription; +}; +AssertCompileSize(struct DRMVERSION, 8 + 7 * sizeof(void *)); + +/** Rectangle structure for geometry of a single screen. */ +struct DRMVMWRECT +{ + int32_t x; + int32_t y; + uint32_t w; + uint32_t h; +}; +AssertCompileSize(struct DRMVMWRECT, 16); + +#define DRM_IOCTL_VERSION _IOWR('d', 0x00, struct DRMVERSION) + +struct DRMCONTEXT +{ + RTFILE hDevice; +}; + +static void drmConnect(struct DRMCONTEXT *pContext) +{ + unsigned i; + RTFILE hDevice; + + if (pContext->hDevice != NIL_RTFILE) + VBClLogFatalError("%s called with bad argument\n", __func__); + /* Try to open the SVGA DRM device. */ + for (i = 0; i < 128; ++i) + { + char szPath[64]; + struct DRMVERSION version; + char szName[sizeof(DRM_DRIVER_NAME)]; + int rc; + + /* Control devices for drm graphics driver control devices go from + * controlD64 to controlD127. Render node devices go from renderD128 + * to renderD192. The driver takes resize hints via the control device + * on pre-4.10 kernels and on the render device on newer ones. Try + * both types. */ + if (i % 2 == 0) + rc = RTStrPrintf(szPath, sizeof(szPath), "/dev/dri/renderD%u", i / 2 + 128); + else + rc = RTStrPrintf(szPath, sizeof(szPath), "/dev/dri/controlD%u", i / 2 + 64); + if (RT_FAILURE(rc)) + VBClLogFatalError("RTStrPrintf of device path failed, rc=%Rrc\n", rc); + rc = RTFileOpen(&hDevice, szPath, RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE); + if (RT_FAILURE(rc)) + continue; + RT_ZERO(version); + version.cbName = sizeof(szName); + version.pszName = szName; + rc = RTFileIoCtl(hDevice, DRM_IOCTL_VERSION, &version, sizeof(version), NULL); + if ( RT_SUCCESS(rc) + && !strncmp(szName, DRM_DRIVER_NAME, sizeof(DRM_DRIVER_NAME) - 1) + && ( version.cMajor > 2 + || (version.cMajor == 2 && version.cMinor > 9))) + break; + hDevice = NIL_RTFILE; + } + pContext->hDevice = hDevice; +} + +/** Preferred screen layout information for DRM_VMW_UPDATE_LAYOUT IoCtl. The + * rects argument is a cast pointer to an array of drm_vmw_rect. */ +struct DRMVMWUPDATELAYOUT { + uint32_t cOutputs; + uint32_t u32Pad; + uint64_t ptrRects; +}; +AssertCompileSize(struct DRMVMWUPDATELAYOUT, 16); + +#define DRM_IOCTL_VMW_UPDATE_LAYOUT \ + _IOW('d', 0x40 + 20, struct DRMVMWUPDATELAYOUT) + +static void drmSendHints(struct DRMCONTEXT *pContext, struct DRMVMWRECT *paRects, + unsigned cHeads) +{ + uid_t guid = getuid(); + if (setreuid(0, 0) == -1) + perror("setreuid failed during drm ioctl."); + int rc; + struct DRMVMWUPDATELAYOUT ioctlLayout; + + if (pContext->hDevice == NIL_RTFILE) + VBClLogFatalError("%s bad device argument\n", __func__); + ioctlLayout.cOutputs = cHeads; + ioctlLayout.ptrRects = (uint64_t)paRects; + rc = RTFileIoCtl(pContext->hDevice, DRM_IOCTL_VMW_UPDATE_LAYOUT, + &ioctlLayout, sizeof(ioctlLayout), NULL); + if (RT_FAILURE(rc) && rc != VERR_INVALID_PARAMETER) + VBClLogFatalError("Failure updating layout, rc=%Rrc\n", rc); + setreuid(guid, 0); +} + +int main(int argc, char *argv[]) +{ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + rc = VbglR3InitUser(); + if (RT_FAILURE(rc)) + VBClLogFatalError("VbglR3InitUser failed: %Rrc", rc); + + struct DRMCONTEXT drmContext = { NIL_RTFILE }; + static struct VMMDevDisplayDef aMonitors[VMW_MAX_HEADS]; + + unsigned cEnabledMonitors; + /* Do not acknowledge the first event we query for to pick up old events, + * e.g. from before a guest reboot. */ + bool fAck = false; + drmConnect(&drmContext); + if (drmContext.hDevice == NIL_RTFILE) + return VERR_OPEN_FAILED; + rc = VbglR3CtlFilterMask(VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST, 0); + if (RT_FAILURE(rc)) + { + VBClLogFatalError("Failed to request display change events, rc=%Rrc\n", rc); + return VERR_INVALID_HANDLE; + } + rc = VbglR3AcquireGuestCaps(VMMDEV_GUEST_SUPPORTS_GRAPHICS, 0, false); + if (rc == VERR_RESOURCE_BUSY) /* Someone else has already acquired it. */ + { + return VERR_RESOURCE_BUSY; + } + if (RT_FAILURE(rc)) + { + VBClLogFatalError("Failed to register resizing support, rc=%Rrc\n", rc); + return VERR_INVALID_HANDLE; + } + + for (;;) + { + uint32_t events; + struct VMMDevDisplayDef aDisplays[VMW_MAX_HEADS]; + uint32_t cDisplaysOut; + /* Query the first size without waiting. This lets us e.g. pick up + * the last event before a guest reboot when we start again after. */ + rc = VbglR3GetDisplayChangeRequestMulti(VMW_MAX_HEADS, &cDisplaysOut, aDisplays, fAck); + fAck = true; + if (RT_FAILURE(rc)) + VBClLogFatalError("Failed to get display change request, rc=%Rrc\n", rc); + if (cDisplaysOut > VMW_MAX_HEADS) + VBClLogFatalError("Display change request contained, rc=%Rrc\n", rc); + if (cDisplaysOut > 0) + { + for (unsigned i = 0; i < cDisplaysOut && i < VMW_MAX_HEADS; ++i) + { + uint32_t idDisplay = aDisplays[i].idDisplay; + if (idDisplay >= VMW_MAX_HEADS) + continue; + aMonitors[idDisplay].fDisplayFlags = aDisplays[i].fDisplayFlags; + if (!(aDisplays[i].fDisplayFlags & VMMDEV_DISPLAY_DISABLED)) + { + if ((idDisplay == 0) || (aDisplays[i].fDisplayFlags & VMMDEV_DISPLAY_ORIGIN)) + { + aMonitors[idDisplay].xOrigin = aDisplays[i].xOrigin; + aMonitors[idDisplay].yOrigin = aDisplays[i].yOrigin; + } else { + aMonitors[idDisplay].xOrigin = aMonitors[idDisplay - 1].xOrigin + aMonitors[idDisplay - 1].cx; + aMonitors[idDisplay].yOrigin = aMonitors[idDisplay - 1].yOrigin; + } + aMonitors[idDisplay].cx = aDisplays[i].cx; + aMonitors[idDisplay].cy = aDisplays[i].cy; + } + } + /* Create an dense (consisting of enabled monitors only) array to pass to DRM. */ + cEnabledMonitors = 0; + struct DRMVMWRECT aEnabledMonitors[VMW_MAX_HEADS]; + for (int j = 0; j < VMW_MAX_HEADS; ++j) + { + if (!(aMonitors[j].fDisplayFlags & VMMDEV_DISPLAY_DISABLED)) + { + aEnabledMonitors[cEnabledMonitors].x = aMonitors[j].xOrigin; + aEnabledMonitors[cEnabledMonitors].y = aMonitors[j].yOrigin; + aEnabledMonitors[cEnabledMonitors].w = aMonitors[j].cx; + aEnabledMonitors[cEnabledMonitors].h = aMonitors[j].cy; + if (cEnabledMonitors > 0) + aEnabledMonitors[cEnabledMonitors].x = aEnabledMonitors[cEnabledMonitors - 1].x + aEnabledMonitors[cEnabledMonitors - 1].w; + ++cEnabledMonitors; + } + } + for (unsigned i = 0; i < cEnabledMonitors; ++i) + printf("Monitor %u: %dx%d, (%d, %d)\n", i, (int)aEnabledMonitors[i].w, (int)aEnabledMonitors[i].h, + (int)aEnabledMonitors[i].x, (int)aEnabledMonitors[i].y); + drmSendHints(&drmContext, aEnabledMonitors, cEnabledMonitors); + } + do + { + rc = VbglR3WaitEvent(VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST, RT_INDEFINITE_WAIT, &events); + } while (rc == VERR_INTERRUPTED); + if (RT_FAILURE(rc)) + VBClLogFatalError("Failure waiting for event, rc=%Rrc\n", rc); + } + return 0; +} diff --git a/src/VBox/Additions/x11/VBoxClient/display-svga-x11.cpp b/src/VBox/Additions/x11/VBoxClient/display-svga-x11.cpp new file mode 100644 index 00000000..af441af0 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/display-svga-x11.cpp @@ -0,0 +1,1458 @@ +/* $Id: display-svga-x11.cpp $ */ +/** @file + * X11 guest client - VMSVGA emulation resize event pass-through to X.Org + * guest driver. + */ + +/* + * Copyright (C) 2017-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +/* + * Known things to test when changing this code. All assume a guest with VMSVGA + * active and controlled by X11 or Wayland, and Guest Additions installed and + * running, unless otherwise stated. + * - On Linux 4.6 and later guests, VBoxClient --vmsvga should be running as + * root and not as the logged-in user. Dynamic resizing should work for all + * screens in any environment which handles kernel resize notifications, + * including at log-in screens. Test GNOME Shell Wayland and GNOME Shell + * under X.Org or Unity or KDE at the log-in screen and after log-in. + * - Linux 4.10 changed the user-kernel-ABI introduced in 4.6: test both. + * - On other guests (than Linux 4.6 or later) running X.Org Server 1.3 or + * later, VBoxClient --vmsvga should never be running as root, and should run + * (and dynamic resizing and screen enable/disable should work for all + * screens) whenever a user is logged in to a supported desktop environment. + * - On guests running X.Org Server 1.2 or older, VBoxClient --vmsvga should + * never run as root and should run whenever a user is logged in to a + * supported desktop environment. Dynamic resizing should work for the first + * screen, and enabling others should not be possible. + * - When VMSVGA is not enabled, VBoxClient --vmsvga should never stay running. + * - The following assumptions are done and should be taken into account when reading/chaning the code: + * # The order of the outputs (monitors) is assumed to be the same in RANDROUTPUT array and + XRRScreenResources.outputs array. + * - This code does 2 related but separate things: 1- It resizes and enables/disables monitors upon host's + requests (see the infinite loop in run()). 2- it listens to RandR events (caused by this or any other X11 client) + on a different thread and notifies host about the new monitor positions. See sendMonitorPositions(...). This is + mainly a work around since we have realized that vmsvga does not convey correct monitor positions thru FIFO. + */ +#include <stdio.h> +#include <dlfcn.h> +/** For sleep(..) */ +#include <unistd.h> +#include "VBoxClient.h" + +#include <VBox/VBoxGuestLib.h> + +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/err.h> +#include <iprt/file.h> +#include <iprt/mem.h> +#include <iprt/path.h> +#include <iprt/string.h> +#include <iprt/thread.h> + +#include <X11/Xlibint.h> +#include <X11/extensions/Xrandr.h> +#include <X11/extensions/panoramiXproto.h> + +#define MILLIS_PER_INCH (25.4) +#define DEFAULT_DPI (96.0) + +/** Maximum number of supported screens. DRM and X11 both limit this to 32. */ +/** @todo if this ever changes, dynamically allocate resizeable arrays in the + * context structure. */ +#define VMW_MAX_HEADS 32 +/** Monitor positions array. Allocated here and deallocated in the class descructor. */ +RTPOINT *mpMonitorPositions; +/** Thread to listen to some of the X server events. */ +RTTHREAD mX11MonitorThread = NIL_RTTHREAD; +/** Shutdown indicator for the monitor thread. */ +static bool g_fMonitorThreadShutdown = false; + + +typedef struct { + CARD8 reqType; /* always X_VMwareCtrlReqCode */ + CARD8 VMwareCtrlReqType; /* always X_VMwareCtrlSetTopology */ + CARD16 length B16; + CARD32 screen B32; + CARD32 number B32; + CARD32 pad1 B32; +} xVMwareCtrlSetTopologyReq; +#define sz_xVMwareCtrlSetTopologyReq 16 + +#define X_VMwareCtrlSetTopology 2 + +typedef struct { + BYTE type; /* X_Reply */ + BYTE pad1; + CARD16 sequenceNumber B16; + CARD32 length B32; + CARD32 screen B32; + CARD32 pad2 B32; + CARD32 pad3 B32; + CARD32 pad4 B32; + CARD32 pad5 B32; + CARD32 pad6 B32; +} xVMwareCtrlSetTopologyReply; +#define sz_xVMwareCtrlSetTopologyReply 32 + +struct X11VMWRECT +{ + int16_t x; + int16_t y; + uint16_t w; + uint16_t h; +}; +AssertCompileSize(struct X11VMWRECT, 8); + +struct X11CONTEXT +{ + Display *pDisplay; + /* We use a separate connection for randr event listening since sharing a + single display object with resizing (main) and event listening threads ends up having a deadlock.*/ + Display *pDisplayRandRMonitoring; + Window rootWindow; + int iDefaultScreen; + XRRScreenResources *pScreenResources; + int hRandRMajor; + int hRandRMinor; + int hRandREventBase; + int hRandRErrorBase; + int hEventMask; + bool fMonitorInfoAvailable; + /** The number of outputs (monitors, including disconnect ones) xrandr reports. */ + int hOutputCount; + void *pRandLibraryHandle; + bool fWmwareCtrlExtention; + int hVMWCtrlMajorOpCode; + /** Function pointers we used if we dlopen libXrandr instead of linking. */ + void (*pXRRSelectInput) (Display *, Window, int); + Bool (*pXRRQueryExtension) (Display *, int *, int *); + Status (*pXRRQueryVersion) (Display *, int *, int*); + XRRMonitorInfo* (*pXRRGetMonitors)(Display *, Window, Bool, int *); + XRRScreenResources* (*pXRRGetScreenResources)(Display *, Window); + Status (*pXRRSetCrtcConfig)(Display *, XRRScreenResources *, RRCrtc, + Time, int, int, RRMode, Rotation, RROutput *, int); + void (*pXRRFreeMonitors)(XRRMonitorInfo *); + void (*pXRRFreeScreenResources)(XRRScreenResources *); + void (*pXRRFreeModeInfo)(XRRModeInfo *); + void (*pXRRFreeOutputInfo)(XRROutputInfo *); + void (*pXRRSetScreenSize)(Display *, Window, int, int, int, int); + int (*pXRRUpdateConfiguration)(XEvent *event); + XRRModeInfo* (*pXRRAllocModeInfo)(_Xconst char *, int); + RRMode (*pXRRCreateMode) (Display *, Window, XRRModeInfo *); + XRROutputInfo* (*pXRRGetOutputInfo) (Display *, XRRScreenResources *, RROutput); + XRRCrtcInfo* (*pXRRGetCrtcInfo) (Display *, XRRScreenResources *, RRCrtc crtc); + void (*pXRRAddOutputMode)(Display *, RROutput, RRMode); +}; + +static X11CONTEXT x11Context; + +struct RANDROUTPUT +{ + int32_t x; + int32_t y; + uint32_t width; + uint32_t height; + bool fEnabled; +}; + +struct DisplayModeR { + + /* These are the values that the user sees/provides */ + int Clock; /* pixel clock freq (kHz) */ + int HDisplay; /* horizontal timing */ + int HSyncStart; + int HSyncEnd; + int HTotal; + int HSkew; + int VDisplay; /* vertical timing */ + int VSyncStart; + int VSyncEnd; + int VTotal; + int VScan; + float HSync; + float VRefresh; +}; + +/** Forward declarations. */ +static void x11Connect(); +static int determineOutputCount(); + +#define checkFunctionPtrReturn(pFunction) \ + do { \ + if (!pFunction) \ + { \ + VBClLogFatalError("Could not find symbol address (%s)\n", #pFunction); \ + dlclose(x11Context.pRandLibraryHandle); \ + x11Context.pRandLibraryHandle = NULL; \ + return VERR_NOT_FOUND; \ + } \ + } while (0) + +#define checkFunctionPtr(pFunction) \ + do { \ + if (!pFunction) \ + VBClLogFatalError("Could not find symbol address (%s)\n", #pFunction);\ + } while (0) + + +/** A slightly modified version of the xf86CVTMode function from xf86cvt.c + * from the xserver source code. Computes several parameters of a display mode + * out of horizontal and vertical resolutions. Replicated here to avoid further + * dependencies. */ +DisplayModeR f86CVTMode(int HDisplay, int VDisplay, float VRefresh /* Herz */, Bool Reduced, + Bool Interlaced) +{ + DisplayModeR Mode; + + /* 1) top/bottom margin size (% of height) - default: 1.8 */ +#define CVT_MARGIN_PERCENTAGE 1.8 + + /* 2) character cell horizontal granularity (pixels) - default 8 */ +#define CVT_H_GRANULARITY 8 + + /* 4) Minimum vertical porch (lines) - default 3 */ +#define CVT_MIN_V_PORCH 3 + + /* 4) Minimum number of vertical back porch lines - default 6 */ +#define CVT_MIN_V_BPORCH 6 + + /* Pixel Clock step (kHz) */ +#define CVT_CLOCK_STEP 250 + + Bool Margins = false; + float VFieldRate, HPeriod; + int HDisplayRnd, HMargin; + int VDisplayRnd, VMargin, VSync; + float Interlace; /* Please rename this */ + + /* CVT default is 60.0Hz */ + if (!VRefresh) + VRefresh = 60.0; + + /* 1. Required field rate */ + if (Interlaced) + VFieldRate = VRefresh * 2; + else + VFieldRate = VRefresh; + + /* 2. Horizontal pixels */ + HDisplayRnd = HDisplay - (HDisplay % CVT_H_GRANULARITY); + + /* 3. Determine left and right borders */ + if (Margins) { + /* right margin is actually exactly the same as left */ + HMargin = (((float) HDisplayRnd) * CVT_MARGIN_PERCENTAGE / 100.0); + HMargin -= HMargin % CVT_H_GRANULARITY; + } + else + HMargin = 0; + + /* 4. Find total active pixels */ + Mode.HDisplay = HDisplayRnd + 2 * HMargin; + + /* 5. Find number of lines per field */ + if (Interlaced) + VDisplayRnd = VDisplay / 2; + else + VDisplayRnd = VDisplay; + + /* 6. Find top and bottom margins */ + /* nope. */ + if (Margins) + /* top and bottom margins are equal again. */ + VMargin = (((float) VDisplayRnd) * CVT_MARGIN_PERCENTAGE / 100.0); + else + VMargin = 0; + + Mode.VDisplay = VDisplay + 2 * VMargin; + + /* 7. Interlace */ + if (Interlaced) + Interlace = 0.5; + else + Interlace = 0.0; + + /* Determine VSync Width from aspect ratio */ + if (!(VDisplay % 3) && ((VDisplay * 4 / 3) == HDisplay)) + VSync = 4; + else if (!(VDisplay % 9) && ((VDisplay * 16 / 9) == HDisplay)) + VSync = 5; + else if (!(VDisplay % 10) && ((VDisplay * 16 / 10) == HDisplay)) + VSync = 6; + else if (!(VDisplay % 4) && ((VDisplay * 5 / 4) == HDisplay)) + VSync = 7; + else if (!(VDisplay % 9) && ((VDisplay * 15 / 9) == HDisplay)) + VSync = 7; + else /* Custom */ + VSync = 10; + + if (!Reduced) { /* simplified GTF calculation */ + + /* 4) Minimum time of vertical sync + back porch interval (µs) + * default 550.0 */ +#define CVT_MIN_VSYNC_BP 550.0 + + /* 3) Nominal HSync width (% of line period) - default 8 */ +#define CVT_HSYNC_PERCENTAGE 8 + + float HBlankPercentage; + int VSyncAndBackPorch, VBackPorch; + int HBlank; + + /* 8. Estimated Horizontal period */ + HPeriod = ((float) (1000000.0 / VFieldRate - CVT_MIN_VSYNC_BP)) / + (VDisplayRnd + 2 * VMargin + CVT_MIN_V_PORCH + Interlace); + + /* 9. Find number of lines in sync + backporch */ + if (((int) (CVT_MIN_VSYNC_BP / HPeriod) + 1) < + (VSync + CVT_MIN_V_PORCH)) + VSyncAndBackPorch = VSync + CVT_MIN_V_PORCH; + else + VSyncAndBackPorch = (int) (CVT_MIN_VSYNC_BP / HPeriod) + 1; + + /* 10. Find number of lines in back porch */ + VBackPorch = VSyncAndBackPorch - VSync; + (void) VBackPorch; + + /* 11. Find total number of lines in vertical field */ + Mode.VTotal = VDisplayRnd + 2 * VMargin + VSyncAndBackPorch + Interlace + + CVT_MIN_V_PORCH; + + /* 5) Definition of Horizontal blanking time limitation */ + /* Gradient (%/kHz) - default 600 */ +#define CVT_M_FACTOR 600 + + /* Offset (%) - default 40 */ +#define CVT_C_FACTOR 40 + + /* Blanking time scaling factor - default 128 */ +#define CVT_K_FACTOR 128 + + /* Scaling factor weighting - default 20 */ +#define CVT_J_FACTOR 20 + +#define CVT_M_PRIME CVT_M_FACTOR * CVT_K_FACTOR / 256 +#define CVT_C_PRIME (CVT_C_FACTOR - CVT_J_FACTOR) * CVT_K_FACTOR / 256 + \ + CVT_J_FACTOR + + /* 12. Find ideal blanking duty cycle from formula */ + HBlankPercentage = CVT_C_PRIME - CVT_M_PRIME * HPeriod / 1000.0; + + /* 13. Blanking time */ + if (HBlankPercentage < 20) + HBlankPercentage = 20; + + HBlank = Mode.HDisplay * HBlankPercentage / (100.0 - HBlankPercentage); + HBlank -= HBlank % (2 * CVT_H_GRANULARITY); + + /* 14. Find total number of pixels in a line. */ + Mode.HTotal = Mode.HDisplay + HBlank; + + /* Fill in HSync values */ + Mode.HSyncEnd = Mode.HDisplay + HBlank / 2; + + Mode.HSyncStart = Mode.HSyncEnd - + (Mode.HTotal * CVT_HSYNC_PERCENTAGE) / 100; + Mode.HSyncStart += CVT_H_GRANULARITY - + Mode.HSyncStart % CVT_H_GRANULARITY; + + /* Fill in VSync values */ + Mode.VSyncStart = Mode.VDisplay + CVT_MIN_V_PORCH; + Mode.VSyncEnd = Mode.VSyncStart + VSync; + + } + else { /* Reduced blanking */ + /* Minimum vertical blanking interval time (µs) - default 460 */ +#define CVT_RB_MIN_VBLANK 460.0 + + /* Fixed number of clocks for horizontal sync */ +#define CVT_RB_H_SYNC 32.0 + + /* Fixed number of clocks for horizontal blanking */ +#define CVT_RB_H_BLANK 160.0 + + /* Fixed number of lines for vertical front porch - default 3 */ +#define CVT_RB_VFPORCH 3 + + int VBILines; + + /* 8. Estimate Horizontal period. */ + HPeriod = ((float) (1000000.0 / VFieldRate - CVT_RB_MIN_VBLANK)) / + (VDisplayRnd + 2 * VMargin); + + /* 9. Find number of lines in vertical blanking */ + VBILines = ((float) CVT_RB_MIN_VBLANK) / HPeriod + 1; + + /* 10. Check if vertical blanking is sufficient */ + if (VBILines < (CVT_RB_VFPORCH + VSync + CVT_MIN_V_BPORCH)) + VBILines = CVT_RB_VFPORCH + VSync + CVT_MIN_V_BPORCH; + + /* 11. Find total number of lines in vertical field */ + Mode.VTotal = VDisplayRnd + 2 * VMargin + Interlace + VBILines; + + /* 12. Find total number of pixels in a line */ + Mode.HTotal = Mode.HDisplay + CVT_RB_H_BLANK; + + /* Fill in HSync values */ + Mode.HSyncEnd = Mode.HDisplay + CVT_RB_H_BLANK / 2; + Mode.HSyncStart = Mode.HSyncEnd - CVT_RB_H_SYNC; + + /* Fill in VSync values */ + Mode.VSyncStart = Mode.VDisplay + CVT_RB_VFPORCH; + Mode.VSyncEnd = Mode.VSyncStart + VSync; + } + /* 15/13. Find pixel clock frequency (kHz for xf86) */ + Mode.Clock = Mode.HTotal * 1000.0 / HPeriod; + Mode.Clock -= Mode.Clock % CVT_CLOCK_STEP; + + /* 16/14. Find actual Horizontal Frequency (kHz) */ + Mode.HSync = ((float) Mode.Clock) / ((float) Mode.HTotal); + + /* 17/15. Find actual Field rate */ + Mode.VRefresh = (1000.0 * ((float) Mode.Clock)) / + ((float) (Mode.HTotal * Mode.VTotal)); + + /* 18/16. Find actual vertical frame frequency */ + /* ignore - just set the mode flag for interlaced */ + if (Interlaced) + Mode.VTotal *= 2; + return Mode; +} + +/** Makes a call to vmwarectrl extension. This updates the + * connection information and possible resolutions (modes) + * of each monitor on the driver. Also sets the preferred mode + * of each output (monitor) to currently selected one. */ +bool VMwareCtrlSetTopology(Display *dpy, int hExtensionMajorOpcode, + int screen, xXineramaScreenInfo extents[], int number) +{ + xVMwareCtrlSetTopologyReply rep; + xVMwareCtrlSetTopologyReq *req; + + long len; + + LockDisplay(dpy); + + GetReq(VMwareCtrlSetTopology, req); + req->reqType = hExtensionMajorOpcode; + req->VMwareCtrlReqType = X_VMwareCtrlSetTopology; + req->screen = screen; + req->number = number; + + len = ((long) number) << 1; + SetReqLen(req, len, len); + len <<= 2; + _XSend(dpy, (char *)extents, len); + + if (!_XReply(dpy, (xReply *)&rep, + (SIZEOF(xVMwareCtrlSetTopologyReply) - SIZEOF(xReply)) >> 2, + xFalse)) + { + UnlockDisplay(dpy); + SyncHandle(); + return false; + } + UnlockDisplay(dpy); + SyncHandle(); + return true; +} + +/** This function assumes monitors are named as from Virtual1 to VirtualX. */ +static int getMonitorIdFromName(const char *sMonitorName) +{ + if (!sMonitorName) + return -1; + int iLen = strlen(sMonitorName); + if (iLen <= 0) + return -1; + int iBase = 10; + int iResult = 0; + for (int i = iLen - 1; i >= 0; --i) + { + /* Stop upon seeing the first non-numeric char. */ + if (sMonitorName[i] < 48 || sMonitorName[i] > 57) + break; + iResult += (sMonitorName[i] - 48) * iBase / 10; + iBase *= 10; + } + return iResult; +} + +static void sendMonitorPositions(RTPOINT *pPositions, size_t cPositions) +{ + if (cPositions && !pPositions) + { + VBClLogError(("Monitor position update called with NULL pointer!\n")); + return; + } + int rc = VbglR3SeamlessSendMonitorPositions(cPositions, pPositions); + if (RT_SUCCESS(rc)) + VBClLogError("Sending monitor positions (%u of them) to the host: %Rrc\n", cPositions, rc); + else + VBClLogError("Error during sending monitor positions (%u of them) to the host: %Rrc\n", cPositions, rc); +} + +static void queryMonitorPositions() +{ + static const int iSentinelPosition = -1; + if (mpMonitorPositions) + { + free(mpMonitorPositions); + mpMonitorPositions = NULL; + } + + int iMonitorCount = 0; + XRRMonitorInfo *pMonitorInfo = NULL; +#ifdef WITH_DISTRO_XRAND_XINERAMA + pMonitorInfo = XRRGetMonitors(x11Context.pDisplayRandRMonitoring, + DefaultRootWindow(x11Context.pDisplayRandRMonitoring), true, &iMonitorCount); +#else + if (x11Context.pXRRGetMonitors) + pMonitorInfo = x11Context.pXRRGetMonitors(x11Context.pDisplayRandRMonitoring, + DefaultRootWindow(x11Context.pDisplayRandRMonitoring), true, &iMonitorCount); +#endif + if (!pMonitorInfo) + return; + if (iMonitorCount == -1) + VBClLogError("Could not get monitor info\n"); + else + { + mpMonitorPositions = (RTPOINT*)malloc(x11Context.hOutputCount * sizeof(RTPOINT)); + /** @todo memset? */ + for (int i = 0; i < x11Context.hOutputCount; ++i) + { + mpMonitorPositions[i].x = iSentinelPosition; + mpMonitorPositions[i].y = iSentinelPosition; + } + for (int i = 0; i < iMonitorCount; ++i) + { + int iMonitorID = getMonitorIdFromName(XGetAtomName(x11Context.pDisplayRandRMonitoring, pMonitorInfo[i].name)) - 1; + if (iMonitorID >= x11Context.hOutputCount || iMonitorID == -1) + continue; + VBClLogInfo("Monitor %d (w,h)=(%d,%d) (x,y)=(%d,%d)\n", + i, + pMonitorInfo[i].width, pMonitorInfo[i].height, + pMonitorInfo[i].x, pMonitorInfo[i].y); + mpMonitorPositions[iMonitorID].x = pMonitorInfo[i].x; + mpMonitorPositions[iMonitorID].y = pMonitorInfo[i].y; + } + if (iMonitorCount > 0) + sendMonitorPositions(mpMonitorPositions, x11Context.hOutputCount); + } +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeMonitors(pMonitorInfo); +#else + if (x11Context.pXRRFreeMonitors) + x11Context.pXRRFreeMonitors(pMonitorInfo); +#endif +} + +static void monitorRandREvents() +{ + XEvent event; + XNextEvent(x11Context.pDisplayRandRMonitoring, &event); + int eventTypeOffset = event.type - x11Context.hRandREventBase; + switch (eventTypeOffset) + { + case RRScreenChangeNotify: + VBClLogInfo("RRScreenChangeNotify event received\n"); + queryMonitorPositions(); + break; + default: + break; + } +} + +/** + * @callback_method_impl{FNRTTHREAD} + */ +static DECLCALLBACK(int) x11MonitorThreadFunction(RTTHREAD ThreadSelf, void *pvUser) +{ + RT_NOREF(ThreadSelf, pvUser); + while (!ASMAtomicReadBool(&g_fMonitorThreadShutdown)) + { + monitorRandREvents(); + } + return 0; +} + +static int startX11MonitorThread() +{ + int rc; + Assert(g_fMonitorThreadShutdown == false); + if (mX11MonitorThread == NIL_RTTHREAD) + { + rc = RTThreadCreate(&mX11MonitorThread, x11MonitorThreadFunction, NULL /*pvUser*/, 0 /*cbStack*/, + RTTHREADTYPE_MSG_PUMP, RTTHREADFLAGS_WAITABLE, "X11 events"); + if (RT_FAILURE(rc)) + VBClLogFatalError("Warning: failed to start X11 monitor thread (VBoxClient) rc=%Rrc!\n", rc); + } + else + rc = VINF_ALREADY_INITIALIZED; + return rc; +} + +static int stopX11MonitorThread(void) +{ + int rc; + if (mX11MonitorThread != NIL_RTTHREAD) + { + ASMAtomicWriteBool(&g_fMonitorThreadShutdown, true); + /** @todo Send event to thread to get it out of XNextEvent. */ + //???????? + //mX11Monitor.interruptEventWait(); + rc = RTThreadWait(mX11MonitorThread, RT_MS_1SEC, NULL /*prc*/); + if (RT_SUCCESS(rc)) + { + mX11MonitorThread = NIL_RTTHREAD; + g_fMonitorThreadShutdown = false; + } + else + VBClLogError("Failed to stop X11 monitor thread, rc=%Rrc!\n", rc); + } + return rc; +} + +static bool callVMWCTRL(struct RANDROUTPUT *paOutputs) +{ + int hHeight = 600; + int hWidth = 800; + + xXineramaScreenInfo *extents = (xXineramaScreenInfo *)malloc(x11Context.hOutputCount * sizeof(xXineramaScreenInfo)); + if (!extents) + return false; + int hRunningOffset = 0; + for (int i = 0; i < x11Context.hOutputCount; ++i) + { + if (paOutputs[i].fEnabled) + { + hHeight = paOutputs[i].height; + hWidth = paOutputs[i].width; + } + else + { + hHeight = 0; + hWidth = 0; + } + extents[i].x_org = hRunningOffset; + extents[i].y_org = 0; + extents[i].width = hWidth; + extents[i].height = hHeight; + hRunningOffset += hWidth; + } + return VMwareCtrlSetTopology(x11Context.pDisplay, x11Context.hVMWCtrlMajorOpCode, + DefaultScreen(x11Context.pDisplay), + extents, x11Context.hOutputCount); + free(extents); +} + +/** + * Tries to determine if the session parenting this process is of Xwayland. + * NB: XDG_SESSION_TYPE is a systemd(1) environment variable and is unlikely + * set in non-systemd environments or remote logins. + * Therefore we check the Wayland specific display environment variable first. + */ +static bool isXwayland(void) +{ + const char *const pDisplayType = getenv("WAYLAND_DISPLAY"); + const char *pSessionType; + + if (pDisplayType != NULL) { + return true; + } + pSessionType = getenv("XDG_SESSION_TYPE"); + if ((pSessionType != NULL) && (RTStrIStartsWith(pSessionType, "wayland"))) { + return true; + } + return false; +} + +/** + * An abbreviated copy of the VGSvcReadProp from VBoxServiceUtils.cpp + */ +static int readGuestProperty(uint32_t u32ClientId, const char *pszPropName) +{ + AssertPtrReturn(pszPropName, VERR_INVALID_POINTER); + + uint32_t cbBuf = _1K; + void *pvBuf = NULL; + int rc = VINF_SUCCESS; /* MSC can't figure out the loop */ + + for (unsigned cTries = 0; cTries < 10; cTries++) + { + /* + * (Re-)Allocate the buffer and try read the property. + */ + RTMemFree(pvBuf); + pvBuf = RTMemAlloc(cbBuf); + if (!pvBuf) + { + VBClLogError("Guest Property: Failed to allocate %zu bytes\n", cbBuf); + rc = VERR_NO_MEMORY; + break; + } + char *pszValue; + char *pszFlags; + uint64_t uTimestamp; + rc = VbglR3GuestPropRead(u32ClientId, pszPropName, pvBuf, cbBuf, &pszValue, &uTimestamp, &pszFlags, NULL); + if (RT_FAILURE(rc)) + { + if (rc == VERR_BUFFER_OVERFLOW) + { + /* try again with a bigger buffer. */ + cbBuf *= 2; + continue; + } + else + break; + } + else + break; + } + + if (pvBuf) + RTMemFree(pvBuf); + return rc; +} + +/** + * We start VBoxDRMClient from VBoxService in case some guest property is set. + * We check the same guest property here and dont start this service in case + * it (guest property) is set. + */ +static bool checkDRMClient() +{ + uint32_t uGuestPropSvcClientID; + int rc = VbglR3GuestPropConnect(&uGuestPropSvcClientID); + if (RT_FAILURE(rc)) + return false; + rc = readGuestProperty(uGuestPropSvcClientID, "/VirtualBox/GuestAdd/DRMResize" /*pszPropName*/); + if (RT_FAILURE(rc)) + return false; + return true; +} + +static bool startDRMClient() +{ + char* argv[] = {NULL}; + char* env[] = {NULL}; + char szDRMClientPath[RTPATH_MAX]; + RTPathExecDir(szDRMClientPath, RTPATH_MAX); + RTPathAppend(szDRMClientPath, RTPATH_MAX, "VBoxDRMClient"); + VBClLogInfo("Starting DRM client.\n"); + int rc = execve(szDRMClientPath, argv, env); + if (rc == -1) + VBClLogFatalError("execve for % returns the following error %d %s\n", szDRMClientPath, errno, strerror(errno)); + /* This is reached only when execve fails. */ + return false; +} + +static bool init() +{ + /* If DRM client is already running don't start this service. */ + if (checkDRMClient()) + { + VBClLogFatalError("DRM resizing is already running. Exiting this service\n"); + return false; + } + if (isXwayland()) + return startDRMClient(); + + x11Connect(); + if (x11Context.pDisplay == NULL) + return false; + /* don't start the monitoring thread if related randr functionality is not available. */ + if (x11Context.fMonitorInfoAvailable) + if (RT_FAILURE(startX11MonitorThread())) + return false; + return true; +} + +static void cleanup() +{ + if (mpMonitorPositions) + { + free(mpMonitorPositions); + mpMonitorPositions = NULL; + } + stopX11MonitorThread(); + if (x11Context.pRandLibraryHandle) + { + dlclose(x11Context.pRandLibraryHandle); + x11Context.pRandLibraryHandle = NULL; + } +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRSelectInput(x11Context.pDisplayRandRMonitoring, x11Context.rootWindow, 0); + XRRFreeScreenResources(x11Context.pScreenResources); +#else + if (x11Context.pXRRSelectInput) + x11Context.pXRRSelectInput(x11Context.pDisplayRandRMonitoring, x11Context.rootWindow, 0); + if (x11Context.pXRRFreeScreenResources) + x11Context.pXRRFreeScreenResources(x11Context.pScreenResources); +#endif + XCloseDisplay(x11Context.pDisplay); + XCloseDisplay(x11Context.pDisplayRandRMonitoring); +} + +#ifndef WITH_DISTRO_XRAND_XINERAMA +static int openLibRandR() +{ + x11Context.pRandLibraryHandle = dlopen("libXrandr.so", RTLD_LAZY /*| RTLD_LOCAL */); + if (!x11Context.pRandLibraryHandle) + x11Context.pRandLibraryHandle = dlopen("libXrandr.so.2", RTLD_LAZY /*| RTLD_LOCAL */); + if (!x11Context.pRandLibraryHandle) + x11Context.pRandLibraryHandle = dlopen("libXrandr.so.2.2.0", RTLD_LAZY /*| RTLD_LOCAL */); + + if (!x11Context.pRandLibraryHandle) + { + VBClLogFatalError("Could not locate libXrandr for dlopen\n"); + return VERR_NOT_FOUND; + } + + *(void **)(&x11Context.pXRRSelectInput) = dlsym(x11Context.pRandLibraryHandle, "XRRSelectInput"); + checkFunctionPtrReturn(x11Context.pXRRSelectInput); + + *(void **)(&x11Context.pXRRQueryExtension) = dlsym(x11Context.pRandLibraryHandle, "XRRQueryExtension"); + checkFunctionPtrReturn(x11Context.pXRRQueryExtension); + + *(void **)(&x11Context.pXRRQueryVersion) = dlsym(x11Context.pRandLibraryHandle, "XRRQueryVersion"); + checkFunctionPtrReturn(x11Context.pXRRQueryVersion); + + /* Don't bail out when XRRGetMonitors XRRFreeMonitors are missing as in Oracle Solaris 10. It is not crucial esp. for single monitor. */ + *(void **)(&x11Context.pXRRGetMonitors) = dlsym(x11Context.pRandLibraryHandle, "XRRGetMonitors"); + checkFunctionPtr(x11Context.pXRRGetMonitors); + + *(void **)(&x11Context.pXRRFreeMonitors) = dlsym(x11Context.pRandLibraryHandle, "XRRFreeMonitors"); + checkFunctionPtr(x11Context.pXRRFreeMonitors); + + x11Context.fMonitorInfoAvailable = x11Context.pXRRGetMonitors && x11Context.pXRRFreeMonitors; + + *(void **)(&x11Context.pXRRGetScreenResources) = dlsym(x11Context.pRandLibraryHandle, "XRRGetScreenResources"); + checkFunctionPtrReturn(x11Context.pXRRGetScreenResources); + + *(void **)(&x11Context.pXRRSetCrtcConfig) = dlsym(x11Context.pRandLibraryHandle, "XRRSetCrtcConfig"); + checkFunctionPtrReturn(x11Context.pXRRSetCrtcConfig); + + *(void **)(&x11Context.pXRRFreeScreenResources) = dlsym(x11Context.pRandLibraryHandle, "XRRFreeScreenResources"); + checkFunctionPtrReturn(x11Context.pXRRFreeScreenResources); + + *(void **)(&x11Context.pXRRFreeModeInfo) = dlsym(x11Context.pRandLibraryHandle, "XRRFreeModeInfo"); + checkFunctionPtrReturn(x11Context.pXRRFreeModeInfo); + + *(void **)(&x11Context.pXRRFreeOutputInfo) = dlsym(x11Context.pRandLibraryHandle, "XRRFreeOutputInfo"); + checkFunctionPtrReturn(x11Context.pXRRFreeOutputInfo); + + *(void **)(&x11Context.pXRRSetScreenSize) = dlsym(x11Context.pRandLibraryHandle, "XRRSetScreenSize"); + checkFunctionPtrReturn(x11Context.pXRRSetScreenSize); + + *(void **)(&x11Context.pXRRUpdateConfiguration) = dlsym(x11Context.pRandLibraryHandle, "XRRUpdateConfiguration"); + checkFunctionPtrReturn(x11Context.pXRRUpdateConfiguration); + + *(void **)(&x11Context.pXRRAllocModeInfo) = dlsym(x11Context.pRandLibraryHandle, "XRRAllocModeInfo"); + checkFunctionPtrReturn(x11Context.pXRRAllocModeInfo); + + *(void **)(&x11Context.pXRRCreateMode) = dlsym(x11Context.pRandLibraryHandle, "XRRCreateMode"); + checkFunctionPtrReturn(x11Context.pXRRCreateMode); + + *(void **)(&x11Context.pXRRGetOutputInfo) = dlsym(x11Context.pRandLibraryHandle, "XRRGetOutputInfo"); + checkFunctionPtrReturn(x11Context.pXRRGetOutputInfo); + + *(void **)(&x11Context.pXRRGetCrtcInfo) = dlsym(x11Context.pRandLibraryHandle, "XRRGetCrtcInfo"); + checkFunctionPtrReturn(x11Context.pXRRGetCrtcInfo); + + *(void **)(&x11Context.pXRRAddOutputMode) = dlsym(x11Context.pRandLibraryHandle, "XRRAddOutputMode"); + checkFunctionPtrReturn(x11Context.pXRRAddOutputMode); + + return VINF_SUCCESS; +} +#endif + +static void x11Connect() +{ + x11Context.pScreenResources = NULL; + x11Context.pXRRSelectInput = NULL; + x11Context.pRandLibraryHandle = NULL; + x11Context.pXRRQueryExtension = NULL; + x11Context.pXRRQueryVersion = NULL; + x11Context.pXRRGetMonitors = NULL; + x11Context.pXRRGetScreenResources = NULL; + x11Context.pXRRSetCrtcConfig = NULL; + x11Context.pXRRFreeMonitors = NULL; + x11Context.pXRRFreeScreenResources = NULL; + x11Context.pXRRFreeOutputInfo = NULL; + x11Context.pXRRFreeModeInfo = NULL; + x11Context.pXRRSetScreenSize = NULL; + x11Context.pXRRUpdateConfiguration = NULL; + x11Context.pXRRAllocModeInfo = NULL; + x11Context.pXRRCreateMode = NULL; + x11Context.pXRRGetOutputInfo = NULL; + x11Context.pXRRGetCrtcInfo = NULL; + x11Context.pXRRAddOutputMode = NULL; + x11Context.fWmwareCtrlExtention = false; + x11Context.fMonitorInfoAvailable = false; + x11Context.hRandRMajor = 0; + x11Context.hRandRMinor = 0; + + int dummy; + if (x11Context.pDisplay != NULL) + VBClLogFatalError("%s called with bad argument\n", __func__); + x11Context.pDisplay = XOpenDisplay(NULL); + x11Context.pDisplayRandRMonitoring = XOpenDisplay(NULL); + if (x11Context.pDisplay == NULL) + return; +#ifndef WITH_DISTRO_XRAND_XINERAMA + if (openLibRandR() != VINF_SUCCESS) + { + XCloseDisplay(x11Context.pDisplay); + XCloseDisplay(x11Context.pDisplayRandRMonitoring); + x11Context.pDisplay = NULL; + x11Context.pDisplayRandRMonitoring = NULL; + return; + } +#endif + + x11Context.fWmwareCtrlExtention = XQueryExtension(x11Context.pDisplay, "VMWARE_CTRL", + &x11Context.hVMWCtrlMajorOpCode, &dummy, &dummy); + if (!x11Context.fWmwareCtrlExtention) + VBClLogError("VMWARE's ctrl extension is not available! Multi monitor management is not possible\n"); + else + VBClLogInfo("VMWARE's ctrl extension is available. Major Opcode is %d.\n", x11Context.hVMWCtrlMajorOpCode); + + /* Check Xrandr stuff. */ + bool fSuccess = false; +#ifdef WITH_DISTRO_XRAND_XINERAMA + fSuccess = XRRQueryExtension(x11Context.pDisplay, &x11Context.hRandREventBase, &x11Context.hRandRErrorBase); +#else + if (x11Context.pXRRQueryExtension) + fSuccess = x11Context.pXRRQueryExtension(x11Context.pDisplay, &x11Context.hRandREventBase, &x11Context.hRandRErrorBase); +#endif + if (fSuccess) + { + fSuccess = false; +#ifdef WITH_DISTRO_XRAND_XINERAMA + fSuccess = XRRQueryVersion(x11Context.pDisplay, &x11Context.hRandRMajor, &x11Context.hRandRMinor); +#else + if (x11Context.pXRRQueryVersion) + fSuccess = x11Context.pXRRQueryVersion(x11Context.pDisplay, &x11Context.hRandRMajor, &x11Context.hRandRMinor); +#endif + if (!fSuccess) + { + XCloseDisplay(x11Context.pDisplay); + x11Context.pDisplay = NULL; + return; + } + if (x11Context.hRandRMajor < 1 || x11Context.hRandRMinor <= 3) + { + VBClLogFatalError("Resizing service requires libXrandr Version >= 1.4. Detected version is %d.%d\n", x11Context.hRandRMajor, x11Context.hRandRMinor); + XCloseDisplay(x11Context.pDisplay); + x11Context.pDisplay = NULL; + return; + } + } + x11Context.rootWindow = DefaultRootWindow(x11Context.pDisplay); + x11Context.hEventMask = RRScreenChangeNotifyMask; + + /* Select the XEvent types we want to listen to. */ +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRSelectInput(x11Context.pDisplayRandRMonitoring, x11Context.rootWindow, x11Context.hEventMask); +#else + if (x11Context.pXRRSelectInput) + x11Context.pXRRSelectInput(x11Context.pDisplayRandRMonitoring, x11Context.rootWindow, x11Context.hEventMask); +#endif + x11Context.iDefaultScreen = DefaultScreen(x11Context.pDisplay); + +#ifdef WITH_DISTRO_XRAND_XINERAMA + x11Context.pScreenResources = XRRGetScreenResources(x11Context.pDisplay, x11Context.rootWindow); +#else + if (x11Context.pXRRGetScreenResources) + x11Context.pScreenResources = x11Context.pXRRGetScreenResources(x11Context.pDisplay, x11Context.rootWindow); +#endif + /* Currently without the VMWARE_CTRL extension we cannot connect outputs and set outputs' preferred mode. + * So we set the output count to 1 to get the 1st output position correct. */ + x11Context.hOutputCount = x11Context.fWmwareCtrlExtention ? determineOutputCount() : 1; +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeScreenResources(x11Context.pScreenResources); +#else + if (x11Context.pXRRFreeScreenResources) + x11Context.pXRRFreeScreenResources(x11Context.pScreenResources); +#endif +} + +static int determineOutputCount() +{ + if (!x11Context.pScreenResources) + return 0; + return x11Context.pScreenResources->noutput; +} + +static int findExistingModeIndex(unsigned iXRes, unsigned iYRes) +{ + if (!x11Context.pScreenResources) + return -1; + for (int i = 0; i < x11Context.pScreenResources->nmode; ++i) + { + if (x11Context.pScreenResources->modes[i].width == iXRes && x11Context.pScreenResources->modes[i].height == iYRes) + return i; + } + return -1; +} + +static bool disableCRTC(RRCrtc crtcID) +{ + XRRCrtcInfo *pCrctInfo = NULL; + +#ifdef WITH_DISTRO_XRAND_XINERAMA + pCrctInfo = XRRGetCrtcInfo(x11Context.pDisplay, x11Context.pScreenResources, crtcID); +#else + if (x11Context.pXRRGetCrtcInfo) + pCrctInfo = x11Context.pXRRGetCrtcInfo(x11Context.pDisplay, x11Context.pScreenResources, crtcID); +#endif + + if (!pCrctInfo) + return false; + + Status ret = Success; +#ifdef WITH_DISTRO_XRAND_XINERAMA + ret = XRRSetCrtcConfig(x11Context.pDisplay, x11Context.pScreenResources, crtcID, + CurrentTime, 0, 0, None, RR_Rotate_0, NULL, 0); +#else + if (x11Context.pXRRSetCrtcConfig) + ret = x11Context.pXRRSetCrtcConfig(x11Context.pDisplay, x11Context.pScreenResources, crtcID, + CurrentTime, 0, 0, None, RR_Rotate_0, NULL, 0); +#endif + /** @todo In case of unsuccesful crtc config set we have to revert frame buffer size and crtc sizes. */ + if (ret == Success) + return true; + else + return false; +} + +static XRRScreenSize currentSize() +{ + XRRScreenSize cSize; + cSize.width = DisplayWidth(x11Context.pDisplay, x11Context.iDefaultScreen); + cSize.mwidth = DisplayWidthMM(x11Context.pDisplay, x11Context.iDefaultScreen); + cSize.height = DisplayHeight(x11Context.pDisplay, x11Context.iDefaultScreen); + cSize.mheight = DisplayHeightMM(x11Context.pDisplay, x11Context.iDefaultScreen); + return cSize; +} + +static unsigned int computeDpi(unsigned int pixels, unsigned int mm) +{ + unsigned int dpi = 0; + if (mm > 0) { + dpi = (unsigned int)((double)pixels * MILLIS_PER_INCH / + (double)mm + 0.5); + } + return (dpi > 0) ? dpi : DEFAULT_DPI; +} + +static bool resizeFrameBuffer(struct RANDROUTPUT *paOutputs) +{ + unsigned int iXRes = 0; + unsigned int iYRes = 0; + /* Don't care about the output positions for now. */ + for (int i = 0; i < x11Context.hOutputCount; ++i) + { + if (!paOutputs[i].fEnabled) + continue; + iXRes += paOutputs[i].width; + iYRes = iYRes < paOutputs[i].height ? paOutputs[i].height : iYRes; + } + XRRScreenSize cSize= currentSize(); + unsigned int xdpi = computeDpi(cSize.width, cSize.mwidth); + unsigned int ydpi = computeDpi(cSize.height, cSize.mheight); + unsigned int xmm; + unsigned int ymm; + xmm = (int)(MILLIS_PER_INCH * iXRes / ((double)xdpi) + 0.5); + ymm = (int)(MILLIS_PER_INCH * iYRes / ((double)ydpi) + 0.5); +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRSelectInput(x11Context.pDisplay, x11Context.rootWindow, RRScreenChangeNotifyMask); + XRRSetScreenSize(x11Context.pDisplay, x11Context.rootWindow, iXRes, iYRes, xmm, ymm); +#else + if (x11Context.pXRRSelectInput) + x11Context.pXRRSelectInput(x11Context.pDisplay, x11Context.rootWindow, RRScreenChangeNotifyMask); + if (x11Context.pXRRSetScreenSize) + x11Context.pXRRSetScreenSize(x11Context.pDisplay, x11Context.rootWindow, iXRes, iYRes, xmm, ymm); +#endif + XSync(x11Context.pDisplay, False); + XEvent configEvent; + bool event = false; + while (XCheckTypedEvent(x11Context.pDisplay, RRScreenChangeNotify + x11Context.hRandREventBase, &configEvent)) + { +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRUpdateConfiguration(&configEvent); +#else + if (x11Context.pXRRUpdateConfiguration) + x11Context.pXRRUpdateConfiguration(&configEvent); +#endif + event = true; + } +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRSelectInput(x11Context.pDisplay, x11Context.rootWindow, 0); +#else + if (x11Context.pXRRSelectInput) + x11Context.pXRRSelectInput(x11Context.pDisplay, x11Context.rootWindow, 0); +#endif + XRRScreenSize newSize = currentSize(); + + if (!event || newSize.width != (int)iXRes || newSize.height != (int)iYRes) + { + VBClLogError("Resizing frame buffer to %d %d has failed\n", iXRes, iYRes); + return false; + } + return true; +} + +static XRRModeInfo *createMode(int iXRes, int iYRes) +{ + XRRModeInfo *pModeInfo = NULL; + char sModeName[126]; + sprintf(sModeName, "%dx%d_vbox", iXRes, iYRes); +#ifdef WITH_DISTRO_XRAND_XINERAMA + pModeInfo = XRRAllocModeInfo(sModeName, strlen(sModeName)); +#else + if (x11Context.pXRRAllocModeInfo) + pModeInfo = x11Context.pXRRAllocModeInfo(sModeName, strlen(sModeName)); +#endif + pModeInfo->width = iXRes; + pModeInfo->height = iYRes; + + DisplayModeR mode = f86CVTMode(iXRes, iYRes, 60 /*VRefresh */, true /*Reduced */, false /* Interlaced */); + + pModeInfo->dotClock = mode.Clock; + pModeInfo->hSyncStart = mode.HSyncStart; + pModeInfo->hSyncEnd = mode.HSyncEnd; + pModeInfo->hTotal = mode.HTotal; + pModeInfo->hSkew = mode.HSkew; + pModeInfo->vSyncStart = mode.VSyncStart; + pModeInfo->vSyncEnd = mode.VSyncEnd; + pModeInfo->vTotal = mode.VTotal; + + RRMode newMode = None; +#ifdef WITH_DISTRO_XRAND_XINERAMA + newMode = XRRCreateMode(x11Context.pDisplay, x11Context.rootWindow, pModeInfo); +#else + if (x11Context.pXRRCreateMode) + newMode = x11Context.pXRRCreateMode(x11Context.pDisplay, x11Context.rootWindow, pModeInfo); +#endif + if (newMode == None) + { +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeModeInfo(pModeInfo); +#else + if (x11Context.pXRRFreeModeInfo) + x11Context.pXRRFreeModeInfo(pModeInfo); +#endif + return NULL; + } + pModeInfo->id = newMode; + return pModeInfo; +} + +static bool configureOutput(int iOutputIndex, struct RANDROUTPUT *paOutputs) +{ + if (iOutputIndex >= x11Context.hOutputCount) + { + VBClLogError("Output index %d is greater than # of oputputs %d\n", iOutputIndex, x11Context.hOutputCount); + return false; + } + RROutput outputId = x11Context.pScreenResources->outputs[iOutputIndex]; + XRROutputInfo *pOutputInfo = NULL; +#ifdef WITH_DISTRO_XRAND_XINERAMA + pOutputInfo = XRRGetOutputInfo(x11Context.pDisplay, x11Context.pScreenResources, outputId); +#else + if (x11Context.pXRRGetOutputInfo) + pOutputInfo = x11Context.pXRRGetOutputInfo(x11Context.pDisplay, x11Context.pScreenResources, outputId); +#endif + if (!pOutputInfo) + return false; + XRRModeInfo *pModeInfo = NULL; + bool fNewMode = false; + /* Index of the mode within the XRRScreenResources.modes array. -1 if such a mode with required resolution does not exists*/ + int iModeIndex = findExistingModeIndex(paOutputs[iOutputIndex].width, paOutputs[iOutputIndex].height); + if (iModeIndex != -1 && iModeIndex < x11Context.pScreenResources->nmode) + pModeInfo = &(x11Context.pScreenResources->modes[iModeIndex]); + else + { + /* A mode with required size was not found. Create a new one. */ + pModeInfo = createMode(paOutputs[iOutputIndex].width, paOutputs[iOutputIndex].height); + fNewMode = true; + } + if (!pModeInfo) + { + VBClLogError("Could not create mode for the resolution (%d, %d)\n", + paOutputs[iOutputIndex].width, paOutputs[iOutputIndex].height); + return false; + } + +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRAddOutputMode(x11Context.pDisplay, outputId, pModeInfo->id); +#else + if (x11Context.pXRRAddOutputMode) + x11Context.pXRRAddOutputMode(x11Context.pDisplay, outputId, pModeInfo->id); +#endif + /* Make sure outputs crtc is set. */ + pOutputInfo->crtc = pOutputInfo->crtcs[0]; + + RRCrtc crtcId = pOutputInfo->crtcs[0]; + Status ret = Success; +#ifdef WITH_DISTRO_XRAND_XINERAMA + ret = XRRSetCrtcConfig(x11Context.pDisplay, x11Context.pScreenResources, crtcId, CurrentTime, + paOutputs[iOutputIndex].x, paOutputs[iOutputIndex].y, + pModeInfo->id, RR_Rotate_0, &(outputId), 1 /*int noutputs*/); +#else + if (x11Context.pXRRSetCrtcConfig) + ret = x11Context.pXRRSetCrtcConfig(x11Context.pDisplay, x11Context.pScreenResources, crtcId, CurrentTime, + paOutputs[iOutputIndex].x, paOutputs[iOutputIndex].y, + pModeInfo->id, RR_Rotate_0, &(outputId), 1 /*int noutputs*/); +#endif + if (ret != Success) + VBClLogError("crtc set config failed for output %d\n", iOutputIndex); + +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeOutputInfo(pOutputInfo); +#else + if (x11Context.pXRRFreeOutputInfo) + x11Context.pXRRFreeOutputInfo(pOutputInfo); +#endif + + if (fNewMode) + { +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeModeInfo(pModeInfo); +#else + if (x11Context.pXRRFreeModeInfo) + x11Context.pXRRFreeModeInfo(pModeInfo); +#endif + } + return true; +} + +/** Construct the xrandr command which sets the whole monitor topology each time. */ +static void setXrandrTopology(struct RANDROUTPUT *paOutputs) +{ + XGrabServer(x11Context.pDisplay); + if (x11Context.fWmwareCtrlExtention) + callVMWCTRL(paOutputs); + +#ifdef WITH_DISTRO_XRAND_XINERAMA + x11Context.pScreenResources = XRRGetScreenResources(x11Context.pDisplay, x11Context.rootWindow); +#else + if (x11Context.pXRRGetScreenResources) + x11Context.pScreenResources = x11Context.pXRRGetScreenResources(x11Context.pDisplay, x11Context.rootWindow); +#endif + x11Context.hOutputCount = x11Context.fWmwareCtrlExtention ? determineOutputCount() : 1; + + if (!x11Context.pScreenResources) + { + XUngrabServer(x11Context.pDisplay); + return; + } + + /* Disable crtcs. */ + for (int i = 0; i < x11Context.pScreenResources->noutput; ++i) + { + XRROutputInfo *pOutputInfo = NULL; +#ifdef WITH_DISTRO_XRAND_XINERAMA + pOutputInfo = XRRGetOutputInfo(x11Context.pDisplay, x11Context.pScreenResources, x11Context.pScreenResources->outputs[i]); +#else + if (x11Context.pXRRGetOutputInfo) + pOutputInfo = x11Context.pXRRGetOutputInfo(x11Context.pDisplay, x11Context.pScreenResources, x11Context.pScreenResources->outputs[i]); +#endif + if (!pOutputInfo) + continue; + if (pOutputInfo->crtc == None) + continue; + + if (!disableCRTC(pOutputInfo->crtc)) + { + VBClLogFatalError("Crtc disable failed %lu\n", pOutputInfo->crtc); + XUngrabServer(x11Context.pDisplay); +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeScreenResources(x11Context.pScreenResources); +#else + if (x11Context.pXRRFreeScreenResources) + x11Context.pXRRFreeScreenResources(x11Context.pScreenResources); +#endif + return; + } +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeOutputInfo(pOutputInfo); +#else + if (x11Context.pXRRFreeOutputInfo) + x11Context.pXRRFreeOutputInfo(pOutputInfo); +#endif + } + /* Resize the frame buffer. */ + if (!resizeFrameBuffer(paOutputs)) + { + XUngrabServer(x11Context.pDisplay); +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeScreenResources(x11Context.pScreenResources); +#else + if (x11Context.pXRRFreeScreenResources) + x11Context.pXRRFreeScreenResources(x11Context.pScreenResources); +#endif + return; + } + + /* Configure the outputs. */ + for (int i = 0; i < x11Context.hOutputCount; ++i) + { + /* be paranoid. */ + if (i >= x11Context.pScreenResources->noutput) + break; + if (!paOutputs[i].fEnabled) + continue; + configureOutput(i, paOutputs); + } + XSync(x11Context.pDisplay, False); +#ifdef WITH_DISTRO_XRAND_XINERAMA + XRRFreeScreenResources(x11Context.pScreenResources); +#else + if (x11Context.pXRRFreeScreenResources) + x11Context.pXRRFreeScreenResources(x11Context.pScreenResources); +#endif + XUngrabServer(x11Context.pDisplay); + XFlush(x11Context.pDisplay); +} + +static const char *getName() +{ + return "Display SVGA X11"; +} + +static const char *getPidFilePath() +{ + return ".vboxclient-display-svga-x11.pid"; +} + +static int run(struct VBCLSERVICE **ppInterface, bool fDaemonised) +{ + RT_NOREF(ppInterface, fDaemonised); + + /* In 32-bit guests GAs build on our release machines causes an xserver hang. + * So for 32-bit GAs we use our DRM client. */ +#if ARCH_BITS == 32 + startDRMClient(); + return VERR_NOT_AVAILABLE; +#endif + + if (!init()) + return VERR_NOT_AVAILABLE; + + /* Do not acknowledge the first event we query for to pick up old events, + * e.g. from before a guest reboot. */ + bool fAck = false; + bool fFirstRun = true; + static struct VMMDevDisplayDef aMonitors[VMW_MAX_HEADS]; + + int rc = VbglR3CtlFilterMask(VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST, 0); + if (RT_FAILURE(rc)) + VBClLogFatalError("Failed to request display change events, rc=%Rrc\n", rc); + rc = VbglR3AcquireGuestCaps(VMMDEV_GUEST_SUPPORTS_GRAPHICS, 0, false); + if (RT_FAILURE(rc)) + VBClLogFatalError("Failed to register resizing support, rc=%Rrc\n", rc); + if (rc == VERR_RESOURCE_BUSY) /* Someone else has already acquired it. */ + return VERR_RESOURCE_BUSY; + for (;;) + { + struct VMMDevDisplayDef aDisplays[VMW_MAX_HEADS]; + uint32_t cDisplaysOut; + /* Query the first size without waiting. This lets us e.g. pick up + * the last event before a guest reboot when we start again after. */ + rc = VbglR3GetDisplayChangeRequestMulti(VMW_MAX_HEADS, &cDisplaysOut, aDisplays, fAck); + fAck = true; + if (RT_FAILURE(rc)) + VBClLogFatalError("Failed to get display change request, rc=%Rrc\n", rc); + if (cDisplaysOut > VMW_MAX_HEADS) + VBClLogFatalError("Display change request contained, rc=%Rrc\n", rc); + if (cDisplaysOut > 0) + { + for (unsigned i = 0; i < cDisplaysOut && i < VMW_MAX_HEADS; ++i) + { + uint32_t idDisplay = aDisplays[i].idDisplay; + if (idDisplay >= VMW_MAX_HEADS) + continue; + aMonitors[idDisplay].fDisplayFlags = aDisplays[i].fDisplayFlags; + if (!(aDisplays[i].fDisplayFlags & VMMDEV_DISPLAY_DISABLED)) + { + if (idDisplay == 0 || (aDisplays[i].fDisplayFlags & VMMDEV_DISPLAY_ORIGIN)) + { + aMonitors[idDisplay].xOrigin = aDisplays[i].xOrigin; + aMonitors[idDisplay].yOrigin = aDisplays[i].yOrigin; + } else { + aMonitors[idDisplay].xOrigin = aMonitors[idDisplay - 1].xOrigin + aMonitors[idDisplay - 1].cx; + aMonitors[idDisplay].yOrigin = aMonitors[idDisplay - 1].yOrigin; + } + aMonitors[idDisplay].cx = aDisplays[i].cx; + aMonitors[idDisplay].cy = aDisplays[i].cy; + } + } + /* Create a whole topology and send it to xrandr. */ + struct RANDROUTPUT aOutputs[VMW_MAX_HEADS]; + int iRunningX = 0; + for (int j = 0; j < x11Context.hOutputCount; ++j) + { + aOutputs[j].x = iRunningX; + aOutputs[j].y = aMonitors[j].yOrigin; + aOutputs[j].width = aMonitors[j].cx; + aOutputs[j].height = aMonitors[j].cy; + aOutputs[j].fEnabled = !(aMonitors[j].fDisplayFlags & VMMDEV_DISPLAY_DISABLED); + if (aOutputs[j].fEnabled) + iRunningX += aOutputs[j].width; + } + /* In 32-bit guests GAs build on our release machines causes an xserver lock during vmware_ctrl extention + if we do the call withing XGrab. We make the call the said extension only once (to connect the outputs) + rather than at each resize iteration. */ +#if ARCH_BITS == 32 + if (fFirstRun) + callVMWCTRL(aOutputs); +#endif + setXrandrTopology(aOutputs); + /* Wait for some seconds and set toplogy again after the boot. In some desktop environments (cinnamon) where + DE get into our resizing our first resize is reverted by the DE. Sleeping for some secs. helps. Setting + topology a 2nd time resolves the black screen I get after resizing.*/ + if (fFirstRun) + { + sleep(4); + setXrandrTopology(aOutputs); + fFirstRun = false; + } + } + uint32_t events; + do + { + rc = VbglR3WaitEvent(VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST, RT_INDEFINITE_WAIT, &events); + } while (rc == VERR_INTERRUPTED); + if (RT_FAILURE(rc)) + VBClLogFatalError("Failure waiting for event, rc=%Rrc\n", rc); + } + cleanup(); +} + +static struct VBCLSERVICE interface = +{ + getName, + getPidFilePath, + VBClServiceDefaultHandler, /* Init */ + run, + VBClServiceDefaultCleanup +}, *pInterface = &interface; + +struct VBCLSERVICE **VBClDisplaySVGAX11Service() +{ + return &pInterface; +} diff --git a/src/VBox/Additions/x11/VBoxClient/draganddrop.cpp b/src/VBox/Additions/x11/VBoxClient/draganddrop.cpp new file mode 100644 index 00000000..9d750584 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/draganddrop.cpp @@ -0,0 +1,3583 @@ +/* $Id: draganddrop.cpp $ */ +/** @file + * X11 guest client - Drag and drop implementation. + */ + +/* + * Copyright (C) 2011-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/Xatom.h> +#ifdef VBOX_DND_WITH_XTEST +# include <X11/extensions/XTest.h> +#endif + +#include <iprt/asm.h> +#include <iprt/buildconfig.h> +#include <iprt/critsect.h> +#include <iprt/thread.h> +#include <iprt/time.h> + +#include <iprt/cpp/mtlist.h> +#include <iprt/cpp/ministring.h> + +#include <limits.h> + +#ifdef LOG_GROUP +# undef LOG_GROUP +#endif +#define LOG_GROUP LOG_GROUP_GUEST_DND +#include <VBox/log.h> +#include <VBox/VBoxGuestLib.h> + +#include "VBox/HostServices/DragAndDropSvc.h" +#include "VBoxClient.h" + +/* Enable this define to see the proxy window(s) when debugging + * their behavior. Don't have this enabled in release builds! */ +#ifdef DEBUG +//# define VBOX_DND_DEBUG_WND +#endif + +/* Enable this to handle drag'n drop "promises". + * This is needed for supporting certain applications (i.e. PcManFM on LXDE), + * which require the drag'n drop meta data a lot earlier than actually needed. + * That behavior is similar to macOS' drag'n drop promises, hence the name. + * + * Those applications query the data right while dragging over them (see GtkWidget::drag-motion), + * instead of when the source dropped the data (GtkWidget::drag-drop). + * + * This might be entirely implementation-specific, so not being a bug in GTK/GDK. Also see #9820. + */ +#ifdef VBOX_WITH_DRAG_AND_DROP_PROMISES +# undef VBOX_WITH_DRAG_AND_DROP_PROMISES +#endif + +/** + * For X11 guest Xdnd is used. See http://www.acc.umu.se/~vatten/XDND.html for + * a walk trough. + * + * Also useful pages: + * - https://www.freedesktop.org/wiki/Draganddropwarts/ + * - https://www.freedesktop.org/wiki/Specifications/XDNDRevision/ + * + * Host -> Guest: + * For X11 this means mainly forwarding all the events from HGCM to the + * appropriate X11 events. There exists a proxy window, which is invisible and + * used for all the X11 communication. On a HGCM Enter event, we set our proxy + * window as XdndSelection owner with the given mime-types. On every HGCM move + * event, we move the X11 mouse cursor to the new position and query for the + * window below that position. Depending on if it is XdndAware, a new window or + * a known window, we send the appropriate X11 messages to it. On HGCM drop, we + * send a XdndDrop message to the current window and wait for a X11 + * SelectionMessage from the target window. Because we didn't have the data in + * the requested mime-type, yet, we save that message and ask the host for the + * data. When the data is successfully received from the host, we put the data + * as a property to the window and send a X11 SelectionNotify event to the + * target window. + * + * Guest -> Host: + * This is a lot more trickery than H->G. When a pending event from HGCM + * arrives, we ask if there currently is an owner of the XdndSelection + * property. If so, our proxy window is shown (1x1, but without backing store) + * and some mouse event is triggered. This should be followed by an XdndEnter + * event send to the proxy window. From this event we can fetch the necessary + * info of the MIME types and allowed actions and send this back to the host. + * On a drop request from the host, we query for the selection and should get + * the data in the specified mime-type. This data is send back to the host. + * After that we send a XdndLeave event to the source window. + * + ** @todo Cancelling (e.g. with ESC key) doesn't work. + ** @todo INCR (incremental transfers) support. + ** @todo Really check for the Xdnd version and the supported features. + ** @todo Either get rid of the xHelpers class or properly unify the code with the drag instance class. + */ + +/********************************************************************************************************************************* + * Definitions * + ********************************************************************************************************************************/ + +/** The Xdnd protocol version we support. */ +#define VBOX_XDND_VERSION (5) + +/** Whether the target window accepts the data being dragged over or not. */ +#define VBOX_XDND_STATUS_FLAG_ACCEPT 0x1 +/** Whether the target window wants XdndPosition messages while dragging stuff over it. */ +#define VBOX_XDND_STATUS_FLAG_WANTS_POS 0x2 + +/** Whether the target window accepted the drop data or not. */ +#define VBOX_XDND_FINISHED_FLAG_SUCCEEDED 0x1 + +/** How many X properties our proxy window can hold. */ +#define VBOX_MAX_XPROPERTIES (LONG_MAX-1) + +/** + * Structure for storing new X11 events and HGCM messages + * into a single event queue. + */ +struct DnDEvent +{ + enum DnDEventType + { + /** Unknown event, do not use. */ + DnDEventType_Unknown = 0, + /** VBGLR3DNDEVENT event. */ + DnDEventType_HGCM, + /** X11 event. */ + DnDEventType_X11, + /** Blow the type up to 32-bit. */ + DnDEventType_32BIT_HACK = 0x7fffffff + }; + /** Event type. */ + DnDEventType enmType; + union + { + PVBGLR3DNDEVENT hgcm; + XEvent x11; + }; +#ifdef IN_GUEST + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif +}; + +enum XA_Type +{ + /* States */ + XA_WM_STATE = 0, + /* Properties */ + XA_TARGETS, + XA_MULTIPLE, + XA_INCR, + /* Mime Types */ + XA_image_bmp, + XA_image_jpg, + XA_image_tiff, + XA_image_png, + XA_text_uri_list, + XA_text_uri, + XA_text_plain, + XA_TEXT, + /* Xdnd */ + XA_XdndSelection, + XA_XdndAware, + XA_XdndEnter, + XA_XdndLeave, + XA_XdndTypeList, + XA_XdndActionList, + XA_XdndPosition, + XA_XdndActionCopy, + XA_XdndActionMove, + XA_XdndActionLink, + XA_XdndStatus, + XA_XdndDrop, + XA_XdndFinished, + /* Our own stop marker */ + XA_dndstop, + /* End marker */ + XA_End +}; + +/** + * Xdnd message value indexes, sorted by message type. + */ +typedef enum XdndMsg +{ + /** XdndEnter. */ + XdndEnterTypeCount = 3, /* Maximum number of types in XdndEnter message. */ + + XdndEnterWindow = 0, /* Source window (sender). */ + XdndEnterFlags, /* Version in high byte, bit 0 => more data types. */ + XdndEnterType1, /* First available data type. */ + XdndEnterType2, /* Second available data type. */ + XdndEnterType3, /* Third available data type. */ + + XdndEnterMoreTypesFlag = 1, /* Set if there are more than XdndEnterTypeCount. */ + XdndEnterVersionRShift = 24, /* Right shift to position version number. */ + XdndEnterVersionMask = 0xFF, /* Mask to get version after shifting. */ + + /** XdndHere. */ + XdndHereWindow = 0, /* Source window (sender). */ + XdndHereFlags, /* Reserved. */ + XdndHerePt, /* X + Y coordinates of mouse (root window coords). */ + XdndHereTimeStamp, /* Timestamp for requesting data. */ + XdndHereAction, /* Action requested by user. */ + + /** XdndPosition. */ + XdndPositionWindow = 0, /* Source window (sender). */ + XdndPositionFlags, /* Flags. */ + XdndPositionXY, /* X/Y coordinates of the mouse position relative to the root window. */ + XdndPositionTimeStamp, /* Time stamp for retrieving the data. */ + XdndPositionAction, /* Action requested by the user. */ + + /** XdndStatus. */ + XdndStatusWindow = 0, /* Target window (sender).*/ + XdndStatusFlags, /* Flags returned by target. */ + XdndStatusNoMsgXY, /* X + Y of "no msg" rectangle (root window coords). */ + XdndStatusNoMsgWH, /* Width + height of "no msg" rectangle. */ + XdndStatusAction, /* Action accepted by target. */ + + XdndStatusAcceptDropFlag = 1, /* Set if target will accept the drop. */ + XdndStatusSendHereFlag = 2, /* Set if target wants a stream of XdndPosition. */ + + /** XdndLeave. */ + XdndLeaveWindow = 0, /* Source window (sender). */ + XdndLeaveFlags, /* Reserved. */ + + /** XdndDrop. */ + XdndDropWindow = 0, /* Source window (sender). */ + XdndDropFlags, /* Reserved. */ + XdndDropTimeStamp, /* Timestamp for requesting data. */ + + /** XdndFinished. */ + XdndFinishedWindow = 0, /* Target window (sender). */ + XdndFinishedFlags, /* Version 5: Bit 0 is set if the current target accepted the drop. */ + XdndFinishedAction /* Version 5: Contains the action performed by the target. */ + +} XdndMsg; + +class DragAndDropService; + +/** List of Atoms. */ +#define VBoxDnDAtomList RTCList<Atom> + +class xHelpers +{ +public: + + static xHelpers *getInstance(Display *pDisplay = 0) + { + if (!m_pInstance) + { + AssertPtrReturn(pDisplay, NULL); + m_pInstance = new xHelpers(pDisplay); + } + + return m_pInstance; + } + + static void destroyInstance(void) + { + if (m_pInstance) + { + delete m_pInstance; + m_pInstance = NULL; + } + } + + inline Display *display() const { return m_pDisplay; } + inline Atom xAtom(XA_Type e) const { return m_xAtoms[e]; } + + inline Atom stringToxAtom(const char *pcszString) const + { + return XInternAtom(m_pDisplay, pcszString, False); + } + inline RTCString xAtomToString(Atom atom) const + { + if (atom == None) return "None"; + + char* pcsAtom = XGetAtomName(m_pDisplay, atom); + RTCString strAtom(pcsAtom); + XFree(pcsAtom); + + return strAtom; + } + + inline RTCString xAtomListToString(const VBoxDnDAtomList &formatList) + { + RTCString format; + for (size_t i = 0; i < formatList.size(); ++i) + format += xAtomToString(formatList.at(i)) + "\r\n"; + return format; + } + + RTCString xErrorToString(int xRc) const; + Window applicationWindowBelowCursor(Window parentWin) const; + +private: +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif + xHelpers(Display *pDisplay) + : m_pDisplay(pDisplay) + { + /* Not all x11 atoms we use are defined in the headers. Create the + * additional one we need here. */ + for (int i = 0; i < XA_End; ++i) + m_xAtoms[i] = XInternAtom(m_pDisplay, m_xAtomNames[i], False); + }; + + /* Private member vars */ + static xHelpers *m_pInstance; + Display *m_pDisplay; + Atom m_xAtoms[XA_End]; + static const char *m_xAtomNames[XA_End]; +}; + +/* Some xHelpers convenience defines. */ +#define gX11 xHelpers::getInstance() +#define xAtom(xa) xHelpers::getInstance()->xAtom((xa)) +#define xAtomToString(xa) xHelpers::getInstance()->xAtomToString((xa)) + +/********************************************************************************************************************************* + * xHelpers implementation. * + ********************************************************************************************************************************/ + +xHelpers *xHelpers::m_pInstance = NULL; + +/* Has to be in sync with the XA_Type enum. */ +const char *xHelpers::m_xAtomNames[] = +{ + /* States */ + "WM_STATE", + /* Properties */ + "TARGETS", + "MULTIPLE", + "INCR", + /* Mime Types */ + "image/bmp", + "image/jpg", + "image/tiff", + "image/png", + "text/uri-list", + "text/uri", + "text/plain", + "TEXT", + /* Xdnd */ + "XdndSelection", + "XdndAware", + "XdndEnter", + "XdndLeave", + "XdndTypeList", + "XdndActionList", + "XdndPosition", + "XdndActionCopy", + "XdndActionMove", + "XdndActionLink", + "XdndStatus", + "XdndDrop", + "XdndFinished", + /* Our own stop marker */ + "dndstop" +}; + +RTCString xHelpers::xErrorToString(int xRc) const +{ + switch (xRc) + { + case Success: return RTCStringFmt("%d (Success)", xRc); break; + case BadRequest: return RTCStringFmt("%d (BadRequest)", xRc); break; + case BadValue: return RTCStringFmt("%d (BadValue)", xRc); break; + case BadWindow: return RTCStringFmt("%d (BadWindow)", xRc); break; + case BadPixmap: return RTCStringFmt("%d (BadPixmap)", xRc); break; + case BadAtom: return RTCStringFmt("%d (BadAtom)", xRc); break; + case BadCursor: return RTCStringFmt("%d (BadCursor)", xRc); break; + case BadFont: return RTCStringFmt("%d (BadFont)", xRc); break; + case BadMatch: return RTCStringFmt("%d (BadMatch)", xRc); break; + case BadDrawable: return RTCStringFmt("%d (BadDrawable)", xRc); break; + case BadAccess: return RTCStringFmt("%d (BadAccess)", xRc); break; + case BadAlloc: return RTCStringFmt("%d (BadAlloc)", xRc); break; + case BadColor: return RTCStringFmt("%d (BadColor)", xRc); break; + case BadGC: return RTCStringFmt("%d (BadGC)", xRc); break; + case BadIDChoice: return RTCStringFmt("%d (BadIDChoice)", xRc); break; + case BadName: return RTCStringFmt("%d (BadName)", xRc); break; + case BadLength: return RTCStringFmt("%d (BadLength)", xRc); break; + case BadImplementation: return RTCStringFmt("%d (BadImplementation)", xRc); break; + } + return RTCStringFmt("%d (unknown)", xRc); +} + +/** @todo Make this iterative. */ +Window xHelpers::applicationWindowBelowCursor(Window wndParent) const +{ + /* No parent, nothing to do. */ + if(wndParent == 0) + return 0; + + Window wndApp = 0; + int cProps = -1; + + /* Fetch all x11 window properties of the parent window. */ + Atom *pProps = XListProperties(m_pDisplay, wndParent, &cProps); + if (cProps > 0) + { + /* We check the window for the WM_STATE property. */ + for (int i = 0; i < cProps; ++i) + { + if (pProps[i] == xAtom(XA_WM_STATE)) + { + /* Found it. */ + wndApp = wndParent; + break; + } + } + + /* Cleanup */ + XFree(pProps); + } + + if (!wndApp) + { + Window wndChild, wndTemp; + int tmp; + unsigned int utmp; + + /* Query the next child window of the parent window at the current + * mouse position. */ + XQueryPointer(m_pDisplay, wndParent, &wndTemp, &wndChild, &tmp, &tmp, &tmp, &tmp, &utmp); + + /* Recursive call our self to dive into the child tree. */ + wndApp = applicationWindowBelowCursor(wndChild); + } + + return wndApp; +} + +#ifdef DEBUG +# define VBOX_DND_FN_DECL_LOG(x) inline x /* For LogFlowXXX logging. */ +#else +# define VBOX_DND_FN_DECL_LOG(x) x +#endif + +/** + * Class which handles a single drag'n drop proxy window. + ** @todo Move all proxy window-related stuff into this class! Clean up this mess. + */ +class VBoxDnDProxyWnd +{ + +public: +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif + VBoxDnDProxyWnd(void); + virtual ~VBoxDnDProxyWnd(void); + +public: + + int init(Display *pDisplay); + void destroy(); + + int sendFinished(Window hWndSource, VBOXDNDACTION dndAction); + +public: + + Display *pDisp; + /** Proxy window handle. */ + Window hWnd; + int iX; + int iY; + int iWidth; + int iHeight; +}; + +/** This class only serve to avoid dragging in generic new() and delete(). */ +class WrappedXEvent +{ +public: + XEvent m_Event; + +public: +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif + WrappedXEvent(const XEvent &a_rSrcEvent) + { + m_Event = a_rSrcEvent; + } + + WrappedXEvent() + { + RT_ZERO(m_Event); + } + + WrappedXEvent &operator=(const XEvent &a_rSrcEvent) + { + m_Event = a_rSrcEvent; + return *this; + } +}; + +/** + * Class for handling a single drag and drop operation, that is, + * one source and one target at a time. + * + * For now only one DragInstance will exits when the app is running. + */ +class DragInstance +{ +public: + + enum State + { + Uninitialized = 0, + Initialized, + Dragging, + Dropped, + State_32BIT_Hack = 0x7fffffff + }; + + enum Mode + { + Unknown = 0, + HG, + GH, + Mode_32Bit_Hack = 0x7fffffff + }; + +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif + DragInstance(Display *pDisplay, DragAndDropService *pParent); + +public: + + int init(uint32_t uScreenID); + void uninit(void); + void reset(void); + + /* X11 message processing. */ + int onX11ClientMessage(const XEvent &e); + int onX11MotionNotify(const XEvent &e); + int onX11SelectionClear(const XEvent &e); + int onX11SelectionNotify(const XEvent &e); + int onX11SelectionRequest(const XEvent &evReq); + int onX11Event(const XEvent &e); + int waitForStatusChange(uint32_t enmState, RTMSINTERVAL uTimeoutMS = 30000); + bool waitForX11Msg(XEvent &evX, int iType, RTMSINTERVAL uTimeoutMS = 100); + bool waitForX11ClientMsg(XClientMessageEvent &evMsg, Atom aType, RTMSINTERVAL uTimeoutMS = 100); + + /* Session handling. */ + int checkForSessionChange(void); + +#ifdef VBOX_WITH_DRAG_AND_DROP_GH + /* Guest -> Host handling. */ + int ghIsDnDPending(void); + int ghDropped(const RTCString &strFormat, VBOXDNDACTION dndActionRequested); +#endif + + /* Host -> Guest handling. */ + int hgEnter(const RTCList<RTCString> &formats, VBOXDNDACTIONLIST dndListActionsAllowed); + int hgLeave(void); + int hgMove(uint32_t uPosX, uint32_t uPosY, VBOXDNDACTION dndActionDefault); + int hgDrop(uint32_t uPosX, uint32_t uPosY, VBOXDNDACTION dndActionDefault); + int hgDataReceive(PVBGLR3GUESTDNDMETADATA pMeta); + + /* X11 helpers. */ + int mouseCursorFakeMove(void) const; + int mouseCursorMove(int iPosX, int iPosY) const; + void mouseButtonSet(Window wndDest, int rx, int ry, int iButton, bool fPress); + int proxyWinShow(int *piRootX = NULL, int *piRootY = NULL) const; + int proxyWinHide(void); + + /* X11 window helpers. */ + char *wndX11GetNameA(Window wndThis) const; + + /* Xdnd protocol helpers. */ + void wndXDnDClearActionList(Window wndThis) const; + void wndXDnDClearFormatList(Window wndThis) const; + int wndXDnDGetActionList(Window wndThis, VBoxDnDAtomList &lstActions) const; + int wndXDnDGetFormatList(Window wndThis, VBoxDnDAtomList &lstTypes) const; + int wndXDnDSetActionList(Window wndThis, const VBoxDnDAtomList &lstActions) const; + int wndXDnDSetFormatList(Window wndThis, Atom atmProp, const VBoxDnDAtomList &lstFormats) const; + + /* Atom / HGCM formatting helpers. */ + int appendFormatsToList(const RTCList<RTCString> &lstFormats, VBoxDnDAtomList &lstAtoms) const; + int appendDataToList(const void *pvData, uint32_t cbData, VBoxDnDAtomList &lstAtoms) const; + static Atom toAtomAction(VBOXDNDACTION dndAction); + static int toAtomActions(VBOXDNDACTIONLIST dndActionList, VBoxDnDAtomList &lstAtoms); + static uint32_t toHGCMAction(Atom atom); + static uint32_t toHGCMActions(const VBoxDnDAtomList &actionsList); + +protected: + + /** The instance's own DnD context. */ + VBGLR3GUESTDNDCMDCTX m_dndCtx; + /** Pointer to service instance. */ + DragAndDropService *m_pParent; + /** Pointer to X display operating on. */ + Display *m_pDisplay; + /** X screen ID to operate on. */ + int m_screenID; + /** Pointer to X screen operating on. */ + Screen *m_pScreen; + /** Root window handle. */ + Window m_wndRoot; + /** Proxy window. */ + VBoxDnDProxyWnd m_wndProxy; + /** Current source/target window handle. */ + Window m_wndCur; + /** The XDnD protocol version the current + * source/target window is using. */ + long m_curVer; + /** List of (Atom) formats the source window supports. */ + VBoxDnDAtomList m_lstAtomFormats; + /** List of (Atom) actions the source window supports. */ + VBoxDnDAtomList m_lstAtomActions; + /** Buffer for answering the target window's selection request. */ + void *m_pvSelReqData; + /** Size (in bytes) of selection request data buffer. */ + uint32_t m_cbSelReqData; + /** Current operation mode. */ + volatile uint32_t m_enmMode; + /** Current state of operation mode. */ + volatile uint32_t m_enmState; + /** The instance's own X event queue. */ + RTCMTList<WrappedXEvent> m_eventQueueList; + /** Critical section for providing serialized access to list + * event queue's contents. */ + RTCRITSECT m_eventQueueCS; + /** Event for notifying this instance in case of a new + * event. */ + RTSEMEVENT m_eventQueueEvent; + /** Critical section for data access. */ + RTCRITSECT m_dataCS; + /** List of allowed formats. */ + RTCList<RTCString> m_lstAllowedFormats; + /** Number of failed attempts by the host + * to query for an active drag and drop operation on the guest. */ + uint16_t m_cFailedPendingAttempts; +}; + +/** + * Service class which implements drag'n drop. + */ +class DragAndDropService +{ +public: + DragAndDropService(void) + : m_pDisplay(NULL) + , m_hHGCMThread(NIL_RTTHREAD) + , m_hX11Thread(NIL_RTTHREAD) + , m_hEventSem(NIL_RTSEMEVENT) + , m_pCurDnD(NULL) + , m_fStop(false) + { + RT_ZERO(m_dndCtx); + } + + int init(void); + int run(bool fDaemonised = false); + void cleanup(void); + +private: + + static DECLCALLBACK(int) hgcmEventThread(RTTHREAD hThread, void *pvUser); + static DECLCALLBACK(int) x11EventThread(RTTHREAD hThread, void *pvUser); + + /* Private member vars */ + Display *m_pDisplay; + + /** Our (thread-safe) event queue with + * mixed events (DnD HGCM / X11). */ + RTCMTList<DnDEvent> m_eventQueue; + /** Critical section for providing serialized access to list + * event queue's contents. */ + RTCRITSECT m_eventQueueCS; + RTTHREAD m_hHGCMThread; + RTTHREAD m_hX11Thread; + /** This service' DnD command context. */ + VBGLR3GUESTDNDCMDCTX m_dndCtx; + RTSEMEVENT m_hEventSem; + DragInstance *m_pCurDnD; + /** Stop indicator flag to signal the thread that it should shut down. */ + bool m_fStop; + + friend class DragInstance; +}; + +/********************************************************************************************************************************* + * DragInstanc implementation. * + ********************************************************************************************************************************/ + +DragInstance::DragInstance(Display *pDisplay, DragAndDropService *pParent) + : m_pParent(pParent) + , m_pDisplay(pDisplay) + , m_pScreen(0) + , m_wndRoot(0) + , m_wndCur(0) + , m_curVer(-1) + , m_pvSelReqData(NULL) + , m_cbSelReqData(0) + , m_enmMode(Unknown) + , m_enmState(Uninitialized) +{ +} + +/** + * Unitializes (destroys) this drag instance. + */ +void DragInstance::uninit(void) +{ + LogFlowFuncEnter(); + + if (m_wndProxy.hWnd != 0) + XDestroyWindow(m_pDisplay, m_wndProxy.hWnd); + + int rc2 = VbglR3DnDDisconnect(&m_dndCtx); + + if (m_pvSelReqData) + RTMemFree(m_pvSelReqData); + + rc2 = RTSemEventDestroy(m_eventQueueEvent); + AssertRC(rc2); + + rc2 = RTCritSectDelete(&m_eventQueueCS); + AssertRC(rc2); + + rc2 = RTCritSectDelete(&m_dataCS); + AssertRC(rc2); +} + +/** + * Resets this drag instance. + */ +void DragInstance::reset(void) +{ + LogFlowFuncEnter(); + + /* Hide the proxy win. */ + proxyWinHide(); + + int rc2 = RTCritSectEnter(&m_dataCS); + if (RT_SUCCESS(rc2)) + { + /* If we are currently the Xdnd selection owner, clear that. */ + Window pWnd = XGetSelectionOwner(m_pDisplay, xAtom(XA_XdndSelection)); + if (pWnd == m_wndProxy.hWnd) + XSetSelectionOwner(m_pDisplay, xAtom(XA_XdndSelection), None, CurrentTime); + + /* Clear any other DnD specific data on the proxy window. */ + wndXDnDClearFormatList(m_wndProxy.hWnd); + wndXDnDClearActionList(m_wndProxy.hWnd); + + m_lstAtomActions.clear(); + + /* First, clear the formats list. */ + m_lstAtomFormats.clear(); + /* Append default targets we support. + * Note: The order is sorted by preference; be careful when changing this. */ + m_lstAtomFormats.append(xAtom(XA_TARGETS)); + m_lstAtomFormats.append(xAtom(XA_MULTIPLE)); + /** @todo Support INC (incremental transfers). */ + + m_wndCur = 0; + m_curVer = -1; + m_enmState = Initialized; + m_enmMode = Unknown; + m_eventQueueList.clear(); + m_cFailedPendingAttempts = 0; + + /* Reset the selection request buffer. */ + if (m_pvSelReqData) + { + RTMemFree(m_pvSelReqData); + m_pvSelReqData = NULL; + + Assert(m_cbSelReqData); + m_cbSelReqData = 0; + } + + RTCritSectLeave(&m_dataCS); + } +} + +/** + * Initializes this drag instance. + * + * @return IPRT status code. + * @param uScreenID X' screen ID to use. + */ +int DragInstance::init(uint32_t uScreenID) +{ + int rc = VbglR3DnDConnect(&m_dndCtx); + /* Note: Can return VINF_PERMISSION_DENIED if HGCM host service is not available. */ + if (rc != VINF_SUCCESS) + return rc; + + do + { + rc = RTSemEventCreate(&m_eventQueueEvent); + if (RT_FAILURE(rc)) + break; + + rc = RTCritSectInit(&m_eventQueueCS); + if (RT_FAILURE(rc)) + break; + + rc = RTCritSectInit(&m_dataCS); + if (RT_FAILURE(rc)) + break; + + /* + * Enough screens configured in the x11 server? + */ + if ((int)uScreenID > ScreenCount(m_pDisplay)) + { + rc = VERR_INVALID_PARAMETER; + break; + } +#if 0 + /* Get the screen number from the x11 server. */ + pDrag->screen = ScreenOfDisplay(m_pDisplay, uScreenID); + if (!pDrag->screen) + { + rc = VERR_GENERAL_FAILURE; + break; + } +#endif + m_screenID = uScreenID; + + /* Now query the corresponding root window of this screen. */ + m_wndRoot = RootWindow(m_pDisplay, m_screenID); + if (!m_wndRoot) + { + rc = VERR_GENERAL_FAILURE; + break; + } + + /* + * Create an invisible window which will act as proxy for the DnD + * operation. This window will be used for both the GH and HG + * direction. + */ + XSetWindowAttributes attr; + RT_ZERO(attr); + attr.event_mask = EnterWindowMask | LeaveWindowMask + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + attr.override_redirect = True; + attr.do_not_propagate_mask = NoEventMask; +#ifdef VBOX_DND_DEBUG_WND + attr.background_pixel = XWhitePixel(m_pDisplay, m_screenID); + attr.border_pixel = XBlackPixel(m_pDisplay, m_screenID); + m_wndProxy.hWnd = XCreateWindow(m_pDisplay, m_wndRoot /* Parent */, + 100, 100, /* Position */ + 100, 100, /* Width + height */ + 2, /* Border width */ + CopyFromParent, /* Depth */ + InputOutput, /* Class */ + CopyFromParent, /* Visual */ + CWBackPixel + | CWBorderPixel + | CWOverrideRedirect + | CWDontPropagate, /* Value mask */ + &attr); /* Attributes for value mask */ +#else + m_wndProxy.hWnd = XCreateWindow(m_pDisplay, m_wndRoot /* Parent */, + 0, 0, /* Position */ + 1, 1, /* Width + height */ + 0, /* Border width */ + CopyFromParent, /* Depth */ + InputOnly, /* Class */ + CopyFromParent, /* Visual */ + CWOverrideRedirect | CWDontPropagate, /* Value mask */ + &attr); /* Attributes for value mask */ +#endif + if (!m_wndProxy.hWnd) + { + VBClLogError("Error creating proxy window\n"); + rc = VERR_GENERAL_FAILURE; + break; + } + + rc = m_wndProxy.init(m_pDisplay); + if (RT_FAILURE(rc)) + { + VBClLogError("Error initializing proxy window, rc=%Rrc\n", rc); + break; + } + +#ifdef VBOX_DND_DEBUG_WND + XFlush(m_pDisplay); + XMapWindow(m_pDisplay, m_wndProxy.hWnd); + XRaiseWindow(m_pDisplay, m_wndProxy.hWnd); + XFlush(m_pDisplay); +#endif + VBClLogInfo("Proxy window=%RU32, root window=%RU32 ...\n", m_wndProxy.hWnd, m_wndRoot); + + /* Set the window's name for easier lookup. */ + XStoreName(m_pDisplay, m_wndProxy.hWnd, "VBoxClientWndDnD"); + + /* Make the new window Xdnd aware. */ + Atom atmVer = VBOX_XDND_VERSION; + XChangeProperty(m_pDisplay, m_wndProxy.hWnd, xAtom(XA_XdndAware), XA_ATOM, 32, PropModeReplace, + reinterpret_cast<unsigned char*>(&atmVer), 1); + } while (0); + + if (RT_SUCCESS(rc)) + { + reset(); + } + else + VBClLogError("Initializing drag instance for screen %RU32 failed with rc=%Rrc\n", uScreenID, rc); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Callback handler for a generic client message from a window. + * + * @return IPRT status code. + * @param e X11 event to handle. + */ +int DragInstance::onX11ClientMessage(const XEvent &e) +{ + AssertReturn(e.type == ClientMessage, VERR_INVALID_PARAMETER); + + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + LogFlowThisFunc(("Event wnd=%#x, msg=%s\n", e.xclient.window, xAtomToString(e.xclient.message_type).c_str())); + + int rc = VINF_SUCCESS; + + switch (m_enmMode) + { + case HG: + { + /* + * Client messages are used to inform us about the status of a XdndAware + * window, in response of some events we send to them. + */ + if (e.xclient.message_type == xAtom(XA_XdndStatus)) + { + Window wndTarget = static_cast<Window>(e.xclient.data.l[XdndStatusWindow]); + + /* Does the target accept the drop? */ + const bool fAcceptDrop = e.xclient.data.l[XdndStatusFlags] & VBOX_XDND_STATUS_FLAG_ACCEPT; + /* Does the target want XdndPosition messages? */ + const bool fWantsPosition = e.xclient.data.l[XdndStatusFlags] & VBOX_XDND_STATUS_FLAG_WANTS_POS; + RT_NOREF(fWantsPosition); + + char *pszWndName = wndX11GetNameA(m_wndCur); + AssertPtr(pszWndName); + + /* + * The XdndStatus message tell us if the window will accept the DnD + * event and with which action. We immediately send this info down to + * the host as a response of a previous DnD message. + */ + RTCString strActions = xAtomToString(e.xclient.data.l[XdndStatusAction]); + + VBClLogInfo("Target window %#x ('%s') %s accept data with actions '%s'\n", + wndTarget, pszWndName, fAcceptDrop ? "does" : "does not", strActions.c_str()); + + const uint16_t x = RT_HI_U16((uint32_t)e.xclient.data.l[XdndStatusNoMsgXY]); + const uint16_t y = RT_LO_U16((uint32_t)e.xclient.data.l[XdndStatusNoMsgXY]); + const uint16_t cx = RT_HI_U16((uint32_t)e.xclient.data.l[XdndStatusNoMsgWH]); + const uint16_t cy = RT_LO_U16((uint32_t)e.xclient.data.l[XdndStatusNoMsgWH]); + + if (cx && cy) + { + VBClLogInfo("Target window %#x ('%s') reported dead area at %RU16,%RU16 (%RU16 x %RU16)\n", + wndTarget, pszWndName, x, y, cx, cy); + /** @todo Save dead area and don't send XdndPosition messages anymore into it. */ + } + + if (m_wndCur == wndTarget) + { + VBOXDNDACTION dndAction = VBOX_DND_ACTION_IGNORE; /* Default is ignoring. */ + /** @todo Compare this with the allowed actions. */ + if (fAcceptDrop) + dndAction = toHGCMAction(static_cast<Atom>(e.xclient.data.l[XdndStatusAction])); + + rc = VbglR3DnDHGSendAckOp(&m_dndCtx, dndAction); + } + else + VBClLogInfo("Target window %#x ('%s') is not our current window, skipping\n", wndTarget, pszWndName); + + RTStrFree(pszWndName); + } + else if (e.xclient.message_type == xAtom(XA_XdndFinished)) + { + Window wndTarget = static_cast<Window>(e.xclient.data.l[XdndFinishedWindow]); + + const bool fSucceeded = e.xclient.data.l[XdndFinishedFlags] & VBOX_XDND_FINISHED_FLAG_SUCCEEDED; + + char *pszWndName = wndX11GetNameA(wndTarget); + AssertPtr(pszWndName); + + const char *pcszAction = xAtomToString(e.xclient.data.l[XdndFinishedAction]).c_str(); + + /* This message is sent on an un/successful DnD drop request. */ + LogFlowThisFunc(("XA_XdndFinished: wnd=%#x ('%s'), success=%RTbool, action=%s\n", + wndTarget, pszWndName, fSucceeded, pcszAction)); + + VBClLogInfo("Target window %#x ('%s') has %s the data with action '%s'\n", + wndTarget, pszWndName, fSucceeded ? "accepted" : "rejected", pcszAction ? "<None>" : pcszAction); + + RTStrFree(pszWndName); + + reset(); + } + else + { + LogFlowThisFunc(("Unhandled client message '%s'\n", xAtomToString(e.xclient.message_type).c_str())); + rc = VERR_NOT_SUPPORTED; + } + + break; + } + + case Unknown: /* Mode not set (yet). */ + case GH: + { + /* + * This message marks the beginning of a new drag and drop + * operation on the guest. + */ + if (e.xclient.message_type == xAtom(XA_XdndEnter)) + { + LogFlowFunc(("XA_XdndEnter\n")); + + /* + * Get the window which currently has the XA_XdndSelection + * bit set. + */ + Window wndSelection = XGetSelectionOwner(m_pDisplay, xAtom(XA_XdndSelection)); + + char *pszWndName = wndX11GetNameA(wndSelection); + AssertPtr(pszWndName); + LogFlowThisFunc(("wndSelection=%RU32 ('%s'), wndProxy=%RU32\n", wndSelection, pszWndName, m_wndProxy.hWnd)); + RTStrFree(pszWndName); + + mouseButtonSet(m_wndProxy.hWnd, -1, -1, 1, true /* fPress */); + + /* + * Update our state and the window handle to process. + */ + int rc2 = RTCritSectEnter(&m_dataCS); + if (RT_SUCCESS(rc2)) + { + m_wndCur = wndSelection; + m_curVer = e.xclient.data.l[XdndEnterFlags] >> XdndEnterVersionRShift; + Assert(m_wndCur == (Window)e.xclient.data.l[XdndEnterWindow]); /* Source window. */ +#ifdef DEBUG + XWindowAttributes xwa; + XGetWindowAttributes(m_pDisplay, m_wndCur, &xwa); + LogFlowThisFunc(("wndCur=%#x, x=%d, y=%d, width=%d, height=%d\n", m_wndCur, xwa.x, xwa.y, xwa.width, xwa.height)); +#endif + /* + * Retrieve supported formats. + */ + + /* Check if the MIME types are in the message itself or if we need + * to fetch the XdndTypeList property from the window. */ + bool fMoreTypes = e.xclient.data.l[XdndEnterFlags] & XdndEnterMoreTypesFlag; + LogFlowThisFunc(("XdndVer=%d, fMoreTypes=%RTbool\n", m_curVer, fMoreTypes)); + if (!fMoreTypes) + { + /* Only up to 3 format types supported. */ + /* Start with index 2 (first item). */ + for (int i = 2; i < 5; i++) + { + LogFlowThisFunc(("\t%s\n", gX11->xAtomToString(e.xclient.data.l[i]).c_str())); + m_lstAtomFormats.append(e.xclient.data.l[i]); + } + } + else + { + /* More than 3 format types supported. */ + rc = wndXDnDGetFormatList(wndSelection, m_lstAtomFormats); + } + + /* + * Retrieve supported actions. + */ + if (RT_SUCCESS(rc)) + { + if (m_curVer >= 2) /* More than one action allowed since protocol version 2. */ + { + rc = wndXDnDGetActionList(wndSelection, m_lstAtomActions); + } + else /* Only "copy" action allowed on legacy applications. */ + m_lstAtomActions.append(XA_XdndActionCopy); + } + + if (RT_SUCCESS(rc)) + { + m_enmMode = GH; + m_enmState = Dragging; + } + + RTCritSectLeave(&m_dataCS); + } + } + else if ( e.xclient.message_type == xAtom(XA_XdndPosition) + && m_wndCur == static_cast<Window>(e.xclient.data.l[XdndPositionWindow])) + { + if (m_enmState != Dragging) /* Wrong mode? Bail out. */ + { + reset(); + break; + } +#ifdef LOG_ENABLED + int32_t iPos = e.xclient.data.l[XdndPositionXY]; + Atom atmAction = m_curVer >= 2 /* Actions other than "copy" or only supported since protocol version 2. */ + ? e.xclient.data.l[XdndPositionAction] : xAtom(XA_XdndActionCopy); + LogFlowThisFunc(("XA_XdndPosition: wndProxy=%RU32, wndCur=%RU32, x=%RI32, y=%RI32, strAction=%s\n", + m_wndProxy.hWnd, m_wndCur, RT_HIWORD(iPos), RT_LOWORD(iPos), + xAtomToString(atmAction).c_str())); +#endif + + bool fAcceptDrop = true; + + /* Reply with a XdndStatus message to tell the source whether + * the data can be dropped or not. */ + XClientMessageEvent m; + RT_ZERO(m); + m.type = ClientMessage; + m.display = m_pDisplay; + m.window = e.xclient.data.l[XdndPositionWindow]; + m.message_type = xAtom(XA_XdndStatus); + m.format = 32; + m.data.l[XdndStatusWindow] = m_wndProxy.hWnd; + m.data.l[XdndStatusFlags] = fAcceptDrop ? RT_BIT(0) : 0; /* Whether to accept the drop or not. */ + + /* We don't want any new XA_XdndPosition messages while being + * in our proxy window. */ + m.data.l[XdndStatusNoMsgXY] = RT_MAKE_U32(m_wndProxy.iY, m_wndProxy.iX); + m.data.l[XdndStatusNoMsgWH] = RT_MAKE_U32(m_wndProxy.iHeight, m_wndProxy.iWidth); + + /** @todo Handle default action! */ + m.data.l[XdndStatusAction] = fAcceptDrop ? toAtomAction(VBOX_DND_ACTION_COPY) : None; + + int xRc = XSendEvent(m_pDisplay, e.xclient.data.l[XdndPositionWindow], + False /* Propagate */, NoEventMask, reinterpret_cast<XEvent *>(&m)); + if (xRc == 0) + VBClLogError("Error sending position XA_XdndStatus event to current window=%#x: %s\n", + m_wndCur, gX11->xErrorToString(xRc).c_str()); + } + else if ( e.xclient.message_type == xAtom(XA_XdndLeave) + && m_wndCur == static_cast<Window>(e.xclient.data.l[XdndLeaveWindow])) + { + LogFlowThisFunc(("XA_XdndLeave\n")); + VBClLogInfo("Guest to host transfer canceled by the guest source window\n"); + + /* Start over. */ + reset(); + } + else if ( e.xclient.message_type == xAtom(XA_XdndDrop) + && m_wndCur == static_cast<Window>(e.xclient.data.l[XdndDropWindow])) + { + LogFlowThisFunc(("XA_XdndDrop\n")); + + if (m_enmState != Dropped) /* Wrong mode? Bail out. */ + { + /* Can occur when dragging from guest->host, but then back in to the guest again. */ + VBClLogInfo("Could not drop on own proxy window\n"); /* Not fatal. */ + + /* Let the source know. */ + rc = m_wndProxy.sendFinished(m_wndCur, VBOX_DND_ACTION_IGNORE); + + /* Start over. */ + reset(); + break; + } + + m_eventQueueList.append(e); + rc = RTSemEventSignal(m_eventQueueEvent); + } + else /* Unhandled event, abort. */ + { + VBClLogInfo("Unhandled event from wnd=%#x, msg=%s\n", e.xclient.window, xAtomToString(e.xclient.message_type).c_str()); + + /* Let the source know. */ + rc = m_wndProxy.sendFinished(m_wndCur, VBOX_DND_ACTION_IGNORE); + + /* Start over. */ + reset(); + } + break; + } + + default: + { + AssertMsgFailed(("Drag and drop mode not implemented: %RU32\n", m_enmMode)); + rc = VERR_NOT_IMPLEMENTED; + break; + } + } + + LogFlowThisFunc(("Returning rc=%Rrc\n", rc)); + return rc; +} + +int DragInstance::onX11MotionNotify(const XEvent &e) +{ + RT_NOREF1(e); + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + + return VINF_SUCCESS; +} + +/** + * Callback handler for being notified if some other window now + * is the owner of the current selection. + * + * @return IPRT status code. + * @param e X11 event to handle. + * + * @remark + */ +int DragInstance::onX11SelectionClear(const XEvent &e) +{ + RT_NOREF1(e); + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + + return VINF_SUCCESS; +} + +/** + * Callback handler for a XDnD selection notify from a window. This is needed + * to let the us know if a certain window has drag'n drop data to share with us, + * e.g. our proxy window. + * + * @return IPRT status code. + * @param e X11 event to handle. + */ +int DragInstance::onX11SelectionNotify(const XEvent &e) +{ + AssertReturn(e.type == SelectionNotify, VERR_INVALID_PARAMETER); + + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + + int rc; + + switch (m_enmMode) + { + case GH: + { + if (m_enmState == Dropped) + { + m_eventQueueList.append(e); + rc = RTSemEventSignal(m_eventQueueEvent); + } + else + rc = VERR_WRONG_ORDER; + break; + } + + default: + { + LogFlowThisFunc(("Unhandled: wnd=%#x, msg=%s\n", + e.xclient.data.l[0], xAtomToString(e.xclient.message_type).c_str())); + rc = VERR_INVALID_STATE; + break; + } + } + + LogFlowThisFunc(("Returning rc=%Rrc\n", rc)); + return rc; +} + +/** + * Callback handler for a XDnD selection request from a window. This is needed + * to retrieve the data required to complete the actual drag'n drop operation. + * + * @returns IPRT status code. + * @param evReq X11 event to handle. + */ +int DragInstance::onX11SelectionRequest(const XEvent &evReq) +{ + AssertReturn(evReq.type == SelectionRequest, VERR_INVALID_PARAMETER); + + const XSelectionRequestEvent *pEvReq = &evReq.xselectionrequest; + + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + LogFlowThisFunc(("Event owner=%#x, requestor=%#x, selection=%s, target=%s, prop=%s, time=%u\n", + pEvReq->owner, + pEvReq->requestor, + xAtomToString(pEvReq->selection).c_str(), + xAtomToString(pEvReq->target).c_str(), + xAtomToString(pEvReq->property).c_str(), + pEvReq->time)); + int rc; + + switch (m_enmMode) + { + case HG: + { + rc = VINF_SUCCESS; + + char *pszWndName = wndX11GetNameA(pEvReq->requestor); + AssertPtr(pszWndName); + + /* + * Start by creating a refusal selection notify message. + * That way we only need to care for the success case. + */ + + XEvent evResp; + RT_ZERO(evResp); + + XSelectionEvent *pEvResp = &evResp.xselection; + + pEvResp->type = SelectionNotify; + pEvResp->display = pEvReq->display; + pEvResp->requestor = pEvReq->requestor; + pEvResp->selection = pEvReq->selection; + pEvResp->target = pEvReq->target; + pEvResp->property = None; /* "None" means refusal. */ + pEvResp->time = pEvReq->time; + +#ifdef DEBUG + LogFlowFunc(("Supported formats:\n")); + for (size_t i = 0; i < m_lstAtomFormats.size(); i++) + LogFlowFunc(("\t%s\n", xAtomToString(m_lstAtomFormats.at(i)).c_str())); +#endif + /* Is the requestor asking for the possible MIME types? */ + if (pEvReq->target == xAtom(XA_TARGETS)) + { + VBClLogInfo("Target window %#x ('%s') asking for target list\n", pEvReq->requestor, pszWndName); + + /* If so, set the window property with the formats on the requestor + * window. */ + rc = wndXDnDSetFormatList(pEvReq->requestor, pEvReq->property, m_lstAtomFormats); + if (RT_SUCCESS(rc)) + pEvResp->property = pEvReq->property; + } + /* Is the requestor asking for a specific MIME type (we support)? */ + else if (m_lstAtomFormats.contains(pEvReq->target)) + { + VBClLogInfo("Target window %#x ('%s') is asking for data as '%s'\n", + pEvReq->requestor, pszWndName, xAtomToString(pEvReq->target).c_str()); + +#ifdef VBOX_WITH_DRAG_AND_DROP_PROMISES +# error "Implement me!" +#else + /* Did we not drop our stuff to the guest yet? Bail out. */ + if (m_enmState != Dropped) + { + VBClLogError("Data not dropped by the host on the guest yet (client state %RU32, mode %RU32), refusing selection request by guest\n", + m_enmState, m_enmMode); + } + /* Did we not store the requestor's initial selection request yet? Then do so now. */ + else + { +#endif /* VBOX_WITH_DRAG_AND_DROP_PROMISES */ + /* Get the data format the requestor wants from us. */ + RTCString strFormat = xAtomToString(pEvReq->target); + Assert(strFormat.isNotEmpty()); + VBClLogInfo("Target window=%#x requested data from host as '%s', rc=%Rrc\n", + pEvReq->requestor, strFormat.c_str(), rc); + + /* Make a copy of the MIME data to be passed back. The X server will be become + * the new owner of that data, so no deletion needed. */ + /** @todo Do we need to do some more conversion here? XConvertSelection? */ + void *pvData = RTMemDup(m_pvSelReqData, m_cbSelReqData); + uint32_t cbData = m_cbSelReqData; + + /* Always return the requested property. */ + evResp.xselection.property = pEvReq->property; + + /* Note: Always seems to return BadRequest. Seems fine. */ + int xRc = XChangeProperty(pEvResp->display, pEvResp->requestor, pEvResp->property, + pEvResp->target, 8, PropModeReplace, + reinterpret_cast<const unsigned char*>(pvData), cbData); + + LogFlowFunc(("Changing property '%s' (target '%s') of window=%RU32: %s\n", + xAtomToString(pEvReq->property).c_str(), + xAtomToString(pEvReq->target).c_str(), + pEvReq->requestor, + gX11->xErrorToString(xRc).c_str())); + RT_NOREF(xRc); +#ifndef VBOX_WITH_DRAG_AND_DROP_PROMISES + } +#endif + } + /* Anything else. */ + else + { + VBClLogError("Refusing unknown command/format '%s' of wnd=%#x ('%s')\n", + xAtomToString(pEvReq->target).c_str(), pEvReq->requestor, pszWndName); + rc = VERR_NOT_SUPPORTED; + } + + LogFlowThisFunc(("Offering type '%s', property '%s' to wnd=%#x ...\n", + xAtomToString(pEvReq->target).c_str(), + xAtomToString(pEvReq->property).c_str(), pEvReq->requestor)); + + int xRc = XSendEvent(pEvReq->display, pEvReq->requestor, True /* Propagate */, 0, &evResp); + if (xRc == 0) + VBClLogError("Error sending SelectionNotify(1) event to wnd=%#x: %s\n", + pEvReq->requestor, gX11->xErrorToString(xRc).c_str()); + XFlush(pEvReq->display); + + if (pszWndName) + RTStrFree(pszWndName); + break; + } + + default: + rc = VERR_INVALID_STATE; + break; + } + + LogFlowThisFunc(("Returning rc=%Rrc\n", rc)); + return rc; +} + +/** + * Handles X11 events, called by x11EventThread. + * + * @returns IPRT status code. + * @param e X11 event to handle. + */ +int DragInstance::onX11Event(const XEvent &e) +{ + int rc; + + LogFlowThisFunc(("X11 event, type=%d\n", e.type)); + switch (e.type) + { + /* + * This can happen if a guest->host drag operation + * goes back from the host to the guest. This is not what + * we want and thus resetting everything. + */ + case ButtonPress: + RT_FALL_THROUGH(); + case ButtonRelease: + { + VBClLogInfo("Mouse button %s\n", e.type == ButtonPress ? "pressed" : "released"); + + reset(); + + rc = VINF_SUCCESS; + break; + } + + case ClientMessage: + rc = onX11ClientMessage(e); + break; + + case SelectionClear: + rc = onX11SelectionClear(e); + break; + + case SelectionNotify: + rc = onX11SelectionNotify(e); + break; + + case SelectionRequest: + rc = onX11SelectionRequest(e); + break; + + case MotionNotify: + rc = onX11MotionNotify(e); + break; + + default: + rc = VERR_NOT_IMPLEMENTED; + break; + } + + LogFlowThisFunc(("rc=%Rrc\n", rc)); + return rc; +} + +int DragInstance::waitForStatusChange(uint32_t enmState, RTMSINTERVAL uTimeoutMS /* = 30000 */) +{ + const uint64_t uiStart = RTTimeMilliTS(); + volatile uint32_t enmCurState; + + int rc = VERR_TIMEOUT; + + LogFlowFunc(("enmState=%RU32, uTimeoutMS=%RU32\n", enmState, uTimeoutMS)); + + do + { + enmCurState = ASMAtomicReadU32(&m_enmState); + if (enmCurState == enmState) + { + rc = VINF_SUCCESS; + break; + } + } + while (RTTimeMilliTS() - uiStart < uTimeoutMS); + + LogFlowThisFunc(("Returning %Rrc\n", rc)); + return rc; +} + +#ifdef VBOX_WITH_DRAG_AND_DROP_GH +/** + * Waits for an X11 event of a specific type. + * + * @returns IPRT status code. + * @param evX Reference where to store the event into. + * @param iType Event type to wait for. + * @param uTimeoutMS Timeout (in ms) to wait for the event. + */ +bool DragInstance::waitForX11Msg(XEvent &evX, int iType, RTMSINTERVAL uTimeoutMS /* = 100 */) +{ + LogFlowThisFunc(("iType=%d, uTimeoutMS=%RU32, cEventQueue=%zu\n", iType, uTimeoutMS, m_eventQueueList.size())); + + bool fFound = false; + const uint64_t uiStart = RTTimeMilliTS(); + + do + { + /* Check if there is a client message in the queue. */ + for (size_t i = 0; i < m_eventQueueList.size(); i++) + { + int rc2 = RTCritSectEnter(&m_eventQueueCS); + if (RT_SUCCESS(rc2)) + { + XEvent e = m_eventQueueList.at(i).m_Event; + + fFound = e.type == iType; + if (fFound) + { + m_eventQueueList.removeAt(i); + evX = e; + } + + rc2 = RTCritSectLeave(&m_eventQueueCS); + AssertRC(rc2); + + if (fFound) + break; + } + } + + if (fFound) + break; + + int rc2 = RTSemEventWait(m_eventQueueEvent, 25 /* ms */); + if ( RT_FAILURE(rc2) + && rc2 != VERR_TIMEOUT) + { + LogFlowFunc(("Waiting failed with rc=%Rrc\n", rc2)); + break; + } + } + while (RTTimeMilliTS() - uiStart < uTimeoutMS); + + LogFlowThisFunc(("Returning fFound=%RTbool, msRuntime=%RU64\n", fFound, RTTimeMilliTS() - uiStart)); + return fFound; +} + +/** + * Waits for an X11 client message of a specific type. + * + * @returns IPRT status code. + * @param evMsg Reference where to store the event into. + * @param aType Event type to wait for. + * @param uTimeoutMS Timeout (in ms) to wait for the event. + */ +bool DragInstance::waitForX11ClientMsg(XClientMessageEvent &evMsg, Atom aType, + RTMSINTERVAL uTimeoutMS /* = 100 */) +{ + LogFlowThisFunc(("aType=%s, uTimeoutMS=%RU32, cEventQueue=%zu\n", + xAtomToString(aType).c_str(), uTimeoutMS, m_eventQueueList.size())); + + bool fFound = false; + const uint64_t uiStart = RTTimeMilliTS(); + do + { + /* Check if there is a client message in the queue. */ + for (size_t i = 0; i < m_eventQueueList.size(); i++) + { + int rc2 = RTCritSectEnter(&m_eventQueueCS); + if (RT_SUCCESS(rc2)) + { + XEvent e = m_eventQueueList.at(i).m_Event; + if ( e.type == ClientMessage + && e.xclient.message_type == aType) + { + m_eventQueueList.removeAt(i); + evMsg = e.xclient; + + fFound = true; + } + + if (e.type == ClientMessage) + { + LogFlowThisFunc(("Client message: Type=%ld (%s)\n", + e.xclient.message_type, xAtomToString(e.xclient.message_type).c_str())); + } + else + LogFlowThisFunc(("X message: Type=%d\n", e.type)); + + rc2 = RTCritSectLeave(&m_eventQueueCS); + AssertRC(rc2); + + if (fFound) + break; + } + } + + if (fFound) + break; + + int rc2 = RTSemEventWait(m_eventQueueEvent, 25 /* ms */); + if ( RT_FAILURE(rc2) + && rc2 != VERR_TIMEOUT) + { + LogFlowFunc(("Waiting failed with rc=%Rrc\n", rc2)); + break; + } + } + while (RTTimeMilliTS() - uiStart < uTimeoutMS); + + LogFlowThisFunc(("Returning fFound=%RTbool, msRuntime=%RU64\n", fFound, RTTimeMilliTS() - uiStart)); + return fFound; +} +#endif /* VBOX_WITH_DRAG_AND_DROP_GH */ + +/* + * Host -> Guest + */ + +/** + * Host -> Guest: Event signalling that the host's (mouse) cursor just entered the VM's (guest's) display + * area. + * + * @returns IPRT status code. + * @param lstFormats List of supported formats from the host. + * @param dndListActionsAllowed (ORed) List of supported actions from the host. + */ +int DragInstance::hgEnter(const RTCList<RTCString> &lstFormats, uint32_t dndListActionsAllowed) +{ + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + + if (m_enmMode != Unknown) + return VERR_INVALID_STATE; + + reset(); + +#ifdef DEBUG + LogFlowThisFunc(("dndListActionsAllowed=0x%x, lstFormats=%zu: ", dndListActionsAllowed, lstFormats.size())); + for (size_t i = 0; i < lstFormats.size(); ++i) + LogFlow(("'%s' ", lstFormats.at(i).c_str())); + LogFlow(("\n")); +#endif + + int rc; + + do + { + /* Check if the VM session has changed and reconnect to the HGCM service if necessary. */ + rc = checkForSessionChange(); + AssertRCBreak(rc); + + /* Append all actual (MIME) formats we support to the list. + * These must come last, after the default Atoms above. */ + rc = appendFormatsToList(lstFormats, m_lstAtomFormats); + AssertRCBreak(rc); + + rc = wndXDnDSetFormatList(m_wndProxy.hWnd, xAtom(XA_XdndTypeList), m_lstAtomFormats); + AssertRCBreak(rc); + + /* Announce the possible actions. */ + VBoxDnDAtomList lstActions; + rc = toAtomActions(dndListActionsAllowed, lstActions); + AssertRCBreak(rc); + + rc = wndXDnDSetActionList(m_wndProxy.hWnd, lstActions); + AssertRCBreak(rc); + + /* Set the DnD selection owner to our window. */ + /** @todo Don't use CurrentTime -- according to ICCCM section 2.1. */ + XSetSelectionOwner(m_pDisplay, xAtom(XA_XdndSelection), m_wndProxy.hWnd, CurrentTime); + + m_enmMode = HG; + m_enmState = Dragging; + + } while (0); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Host -> Guest: Event signalling that the host's (mouse) cursor has left the VM's (guest's) + * display area. + */ +int DragInstance::hgLeave(void) +{ + if (m_enmMode == HG) /* Only reset if in the right operation mode. */ + reset(); + + return VINF_SUCCESS; +} + +/** + * Host -> Guest: Event signalling that the host's (mouse) cursor has been moved within the VM's + * (guest's) display area. + * + * @returns IPRT status code. + * @param uPosX Relative X position within the guest's display area. + * @param uPosY Relative Y position within the guest's display area. + * @param dndActionDefault Default action the host wants to perform on the guest + * as soon as the operation successfully finishes. + */ +int DragInstance::hgMove(uint32_t uPosX, uint32_t uPosY, VBOXDNDACTION dndActionDefault) +{ + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + LogFlowThisFunc(("uPosX=%RU32, uPosY=%RU32, dndActionDefault=0x%x\n", uPosX, uPosY, dndActionDefault)); + + if ( m_enmMode != HG + || m_enmState != Dragging) + { + return VERR_INVALID_STATE; + } + + int rc = VINF_SUCCESS; + int xRc = Success; + + /* Move the mouse cursor within the guest. */ + mouseCursorMove(uPosX, uPosY); + + long newVer = -1; /* This means the current window is _not_ XdndAware. */ + + /* Search for the application window below the cursor. */ + Window wndCursor = gX11->applicationWindowBelowCursor(m_wndRoot); + if (wndCursor != None) + { + /* Temp stuff for the XGetWindowProperty call. */ + Atom atmTmp; + int fmt; + unsigned long cItems, cbRemaining; + unsigned char *pcData = NULL; + + /* Query the XdndAware property from the window. We are interested in + * the version and if it is XdndAware at all. */ + xRc = XGetWindowProperty(m_pDisplay, wndCursor, xAtom(XA_XdndAware), + 0, 2, False, AnyPropertyType, + &atmTmp, &fmt, &cItems, &cbRemaining, &pcData); + if (xRc != Success) + { + VBClLogError("Error getting properties of cursor window=%#x: %s\n", wndCursor, gX11->xErrorToString(xRc).c_str()); + } + else + { + if (pcData == NULL || fmt != 32 || cItems != 1) + { + /** @todo Do we need to deal with this? */ + VBClLogError("Wrong window properties for window %#x: pcData=%#x, iFmt=%d, cItems=%ul\n", + wndCursor, pcData, fmt, cItems); + } + else + { + /* Get the current window's Xdnd version. */ + newVer = reinterpret_cast<long *>(pcData)[0]; + } + + XFree(pcData); + } + } + +#ifdef DEBUG + char *pszNameCursor = wndX11GetNameA(wndCursor); + AssertPtr(pszNameCursor); + char *pszNameCur = wndX11GetNameA(m_wndCur); + AssertPtr(pszNameCur); + + LogFlowThisFunc(("wndCursor=%x ('%s', Xdnd version %ld), wndCur=%x ('%s', Xdnd version %ld)\n", + wndCursor, pszNameCursor, newVer, m_wndCur, pszNameCur, m_curVer)); + + RTStrFree(pszNameCursor); + RTStrFree(pszNameCur); +#endif + + if ( wndCursor != m_wndCur + && m_curVer != -1) + { + LogFlowThisFunc(("XA_XdndLeave: window=%#x\n", m_wndCur)); + + char *pszWndName = wndX11GetNameA(m_wndCur); + AssertPtr(pszWndName); + VBClLogInfo("Left old window %#x ('%s'), Xdnd version=%ld\n", m_wndCur, pszWndName, newVer); + RTStrFree(pszWndName); + + /* We left the current XdndAware window. Announce this to the current indow. */ + XClientMessageEvent m; + RT_ZERO(m); + m.type = ClientMessage; + m.display = m_pDisplay; + m.window = m_wndCur; + m.message_type = xAtom(XA_XdndLeave); + m.format = 32; + m.data.l[XdndLeaveWindow] = m_wndProxy.hWnd; + + xRc = XSendEvent(m_pDisplay, m_wndCur, False, NoEventMask, reinterpret_cast<XEvent*>(&m)); + if (xRc == 0) + VBClLogError("Error sending XA_XdndLeave event to old window=%#x: %s\n", m_wndCur, gX11->xErrorToString(xRc).c_str()); + + /* Reset our current window. */ + m_wndCur = 0; + m_curVer = -1; + } + + /* + * Do we have a new Xdnd-aware window which now is under the cursor? + */ + if ( wndCursor != m_wndCur + && newVer != -1) + { + LogFlowThisFunc(("XA_XdndEnter: window=%#x\n", wndCursor)); + + char *pszWndName = wndX11GetNameA(wndCursor); + AssertPtr(pszWndName); + VBClLogInfo("Entered new window %#x ('%s'), supports Xdnd version=%ld\n", wndCursor, pszWndName, newVer); + RTStrFree(pszWndName); + + /* + * We enter a new window. Announce the XdndEnter event to the new + * window. The first three mime types are attached to the event (the + * others could be requested by the XdndTypeList property from the + * window itself). + */ + XClientMessageEvent m; + RT_ZERO(m); + m.type = ClientMessage; + m.display = m_pDisplay; + m.window = wndCursor; + m.message_type = xAtom(XA_XdndEnter); + m.format = 32; + m.data.l[XdndEnterWindow] = m_wndProxy.hWnd; + m.data.l[XdndEnterFlags] = RT_MAKE_U32_FROM_U8( + /* Bit 0 is set if the source supports more than three data types. */ + m_lstAtomFormats.size() > 3 ? RT_BIT(0) : 0, + /* Reserved for future use. */ + 0, 0, + /* Protocol version to use. */ + RT_MIN(VBOX_XDND_VERSION, newVer)); + m.data.l[XdndEnterType1] = m_lstAtomFormats.value(0, None); /* First data type to use. */ + m.data.l[XdndEnterType2] = m_lstAtomFormats.value(1, None); /* Second data type to use. */ + m.data.l[XdndEnterType3] = m_lstAtomFormats.value(2, None); /* Third data type to use. */ + + xRc = XSendEvent(m_pDisplay, wndCursor, False, NoEventMask, reinterpret_cast<XEvent*>(&m)); + if (xRc == 0) + VBClLogError("Error sending XA_XdndEnter event to window=%#x: %s\n", wndCursor, gX11->xErrorToString(xRc).c_str()); + } + + if (newVer != -1) + { + Assert(wndCursor != None); + + LogFlowThisFunc(("XA_XdndPosition: xPos=%RU32, yPos=%RU32 to window=%#x\n", uPosX, uPosY, wndCursor)); + + /* + * Send a XdndPosition event with the proposed action to the guest. + */ + Atom atmAction = toAtomAction(dndActionDefault); + LogFlowThisFunc(("strAction=%s\n", xAtomToString(atmAction).c_str())); + + XClientMessageEvent m; + RT_ZERO(m); + m.type = ClientMessage; + m.display = m_pDisplay; + m.window = wndCursor; + m.message_type = xAtom(XA_XdndPosition); + m.format = 32; + m.data.l[XdndPositionWindow] = m_wndProxy.hWnd; /* X window ID of source window. */ + m.data.l[XdndPositionFlags] = 0; /* Reserved, set to 0. */ + m.data.l[XdndPositionXY] = RT_MAKE_U32(uPosY, uPosX); /* Cursor coordinates relative to the root window. */ + m.data.l[XdndPositionTimeStamp] = CurrentTime; /* Timestamp for retrieving data. */ + m.data.l[XdndPositionAction] = atmAction; /* Actions requested by the user. */ + + xRc = XSendEvent(m_pDisplay, wndCursor, False, NoEventMask, reinterpret_cast<XEvent*>(&m)); + if (xRc == 0) + VBClLogError("Error sending XA_XdndPosition event to current window=%#x: %s\n", wndCursor, gX11->xErrorToString(xRc).c_str()); + } + + if (newVer == -1) + { + /* No window to process, so send a ignore ack event to the host. */ + rc = VbglR3DnDHGSendAckOp(&m_dndCtx, VBOX_DND_ACTION_IGNORE); + } + else + { + Assert(wndCursor != None); + + m_wndCur = wndCursor; + m_curVer = newVer; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Host -> Guest: Event signalling that the host has dropped the data over the VM (guest) window. + * + * @returns IPRT status code. + * @param uPosX Relative X position within the guest's display area. + * @param uPosY Relative Y position within the guest's display area. + * @param dndActionDefault Default action the host wants to perform on the guest + * as soon as the operation successfully finishes. + */ +int DragInstance::hgDrop(uint32_t uPosX, uint32_t uPosY, VBOXDNDACTION dndActionDefault) +{ + RT_NOREF3(uPosX, uPosY, dndActionDefault); + LogFlowThisFunc(("wndCur=%RU32, wndProxy=%RU32, mode=%RU32, state=%RU32\n", m_wndCur, m_wndProxy.hWnd, m_enmMode, m_enmState)); + LogFlowThisFunc(("uPosX=%RU32, uPosY=%RU32, dndActionDefault=0x%x\n", uPosX, uPosY, dndActionDefault)); + + if ( m_enmMode != HG + || m_enmState != Dragging) + { + return VERR_INVALID_STATE; + } + + /* Set the state accordingly. */ + m_enmState = Dropped; + + /* + * Ask the host to send the raw data, as we don't (yet) know which format + * the guest exactly expects. As blocking in a SelectionRequest message turned + * out to be very unreliable (e.g. with KDE apps) we request to start transferring + * file/directory data (if any) here. + */ + char szFormat[] = { "text/uri-list" }; + + int rc = VbglR3DnDHGSendReqData(&m_dndCtx, szFormat); + VBClLogInfo("Drop event from host resulted in: %Rrc\n", rc); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Host -> Guest: Event signalling that the host has finished sending drag'n drop + * data to the guest for further processing. + * + * @returns IPRT status code. + * @param pMeta Pointer to meta data from host. + */ +int DragInstance::hgDataReceive(PVBGLR3GUESTDNDMETADATA pMeta) +{ + LogFlowThisFunc(("enmMode=%RU32, enmState=%RU32\n", m_enmMode, m_enmState)); + LogFlowThisFunc(("enmMetaType=%RU32\n", pMeta->enmType)); + + if ( m_enmMode != HG + || m_enmState != Dropped) + { + return VERR_INVALID_STATE; + } + + void *pvData = NULL; + size_t cbData = 0; + + int rc = VINF_SUCCESS; /* Shut up GCC. */ + + switch (pMeta->enmType) + { + case VBGLR3GUESTDNDMETADATATYPE_RAW: + { + AssertBreakStmt(pMeta->u.Raw.pvMeta != NULL, rc = VERR_INVALID_POINTER); + pvData = pMeta->u.Raw.pvMeta; + AssertBreakStmt(pMeta->u.Raw.cbMeta, rc = VERR_INVALID_PARAMETER); + cbData = pMeta->u.Raw.cbMeta; + + rc = VINF_SUCCESS; + break; + } + + case VBGLR3GUESTDNDMETADATATYPE_URI_LIST: + { + const char *pcszRootPath = DnDTransferListGetRootPathAbs(&pMeta->u.URI.Transfer); + AssertPtrBreakStmt(pcszRootPath, VERR_INVALID_POINTER); + + VBClLogInfo("Transfer list root directory is '%s'\n", pcszRootPath); + + /* Note: Use the URI format here, as X' DnD spec says so. */ + rc = DnDTransferListGetRootsEx(&pMeta->u.URI.Transfer, DNDTRANSFERLISTFMT_URI, pcszRootPath, + DND_PATH_SEPARATOR_STR, (char **)&pvData, &cbData); + break; + } + + default: + AssertFailedStmt(rc = VERR_NOT_IMPLEMENTED); + break; + } + + if (RT_FAILURE(rc)) + return rc; + + /* + * At this point all data needed (including sent files/directories) should + * be on the guest, so proceed working on communicating with the target window. + */ + VBClLogInfo("Received %RU32 bytes of meta data from host\n", cbData); + + /* Destroy any old data. */ + if (m_pvSelReqData) + { + Assert(m_cbSelReqData); + + RTMemFree(m_pvSelReqData); /** @todo RTMemRealloc? */ + m_cbSelReqData = 0; + } + + /** @todo Handle incremental transfers. */ + + /* Make a copy of the data. This data later then will be used to fill into + * the selection request. */ + if (cbData) + { + m_pvSelReqData = RTMemAlloc(cbData); + if (!m_pvSelReqData) + return VERR_NO_MEMORY; + + memcpy(m_pvSelReqData, pvData, cbData); + m_cbSelReqData = cbData; + } + + /* + * Send a drop event to the current window (target). + * This window in turn then will raise a SelectionRequest message to our proxy window, + * which we will handle in our onX11SelectionRequest handler. + * + * The SelectionRequest will tell us in which format the target wants the data from the host. + */ + XClientMessageEvent m; + RT_ZERO(m); + m.type = ClientMessage; + m.display = m_pDisplay; + m.window = m_wndCur; + m.message_type = xAtom(XA_XdndDrop); + m.format = 32; + m.data.l[XdndDropWindow] = m_wndProxy.hWnd; /* Source window. */ + m.data.l[XdndDropFlags] = 0; /* Reserved for future use. */ + m.data.l[XdndDropTimeStamp] = CurrentTime; /* Our DnD data does not rely on any timing, so just use the current time. */ + + int xRc = XSendEvent(m_pDisplay, m_wndCur, False /* Propagate */, NoEventMask, reinterpret_cast<XEvent*>(&m)); + if (xRc == 0) + VBClLogError("Error sending XA_XdndDrop event to window=%#x: %s\n", m_wndCur, gX11->xErrorToString(xRc).c_str()); + XFlush(m_pDisplay); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Checks if the VM session has changed (can happen when restoring the VM from a saved state) + * and do a reconnect to the DnD HGCM service. + * + * @returns IPRT status code. + */ +int DragInstance::checkForSessionChange(void) +{ + uint64_t uSessionID; + int rc = VbglR3GetSessionId(&uSessionID); + if ( RT_SUCCESS(rc) + && uSessionID != m_dndCtx.uSessionID) + { + LogFlowThisFunc(("VM session has changed to %RU64\n", uSessionID)); + + rc = VbglR3DnDDisconnect(&m_dndCtx); + AssertRC(rc); + + rc = VbglR3DnDConnect(&m_dndCtx); + AssertRC(rc); + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +#ifdef VBOX_WITH_DRAG_AND_DROP_GH +/** + * Guest -> Host: Event signalling that the host is asking whether there is a pending + * drag event on the guest (to the host). + * + * @returns IPRT status code. + */ +int DragInstance::ghIsDnDPending(void) +{ + LogFlowThisFunc(("mode=%RU32, state=%RU32\n", m_enmMode, m_enmState)); + + int rc; + + RTCString strFormats = "\r\n"; /** @todo If empty, IOCTL fails with VERR_ACCESS_DENIED. */ + VBOXDNDACTION dndActionDefault = VBOX_DND_ACTION_IGNORE; + VBOXDNDACTIONLIST dndActionList = VBOX_DND_ACTION_IGNORE; + + /* Currently in wrong mode? Bail out. */ + if (m_enmMode == HG) + { + rc = VERR_INVALID_STATE; + } + /* Message already processed successfully? */ + else if ( m_enmMode == GH + && ( m_enmState == Dragging + || m_enmState == Dropped) + ) + { + /* No need to query for the source window again. */ + rc = VINF_SUCCESS; + } + else + { + /* Check if the VM session has changed and reconnect to the HGCM service if necessary. */ + rc = checkForSessionChange(); + + /* Determine the current window which currently has the XdndSelection set. */ + Window wndSelection = XGetSelectionOwner(m_pDisplay, xAtom(XA_XdndSelection)); + LogFlowThisFunc(("wndSelection=%#x, wndProxy=%#x, wndCur=%#x\n", wndSelection, m_wndProxy.hWnd, m_wndCur)); + + /* Is this another window which has a Xdnd selection and not our proxy window? */ + if ( RT_SUCCESS(rc) + && wndSelection + && wndSelection != m_wndCur) + { + char *pszWndName = wndX11GetNameA(wndSelection); + AssertPtr(pszWndName); + VBClLogInfo("New guest source window %#x ('%s')\n", wndSelection, pszWndName); + + /* Start over. */ + reset(); + + /* Map the window on the current cursor position, which should provoke + * an XdndEnter event. */ + rc = proxyWinShow(); + if (RT_SUCCESS(rc)) + { + rc = mouseCursorFakeMove(); + if (RT_SUCCESS(rc)) + { + bool fWaitFailed = false; /* Waiting for status changed failed? */ + + /* Wait until we're in "Dragging" state. */ + rc = waitForStatusChange(Dragging, 100 /* 100ms timeout */); + + /* + * Note: Don't wait too long here, as this mostly will make + * the drag and drop experience on the host being laggy + * and unresponsive. + * + * Instead, let the host query multiple times with 100ms + * timeout each (see above) and only report an error if + * the overall querying time has been exceeded.< + */ + if (RT_SUCCESS(rc)) + { + m_enmMode = GH; + } + else if (rc == VERR_TIMEOUT) + { + /** @todo Make m_cFailedPendingAttempts configurable. For slower window managers? */ + if (m_cFailedPendingAttempts++ > 50) /* Tolerate up to 5s total (100ms for each slot). */ + fWaitFailed = true; + else + rc = VINF_SUCCESS; + } + else if (RT_FAILURE(rc)) + fWaitFailed = true; + + if (fWaitFailed) + { + VBClLogError("Error mapping proxy window to guest source window %#x ('%s'), rc=%Rrc\n", + wndSelection, pszWndName, rc); + + /* Reset the counter in any case. */ + m_cFailedPendingAttempts = 0; + } + } + } + + RTStrFree(pszWndName); + } + else + VBClLogInfo("No guest source window\n"); + } + + /* + * Acknowledge to the host in any case, regardless + * if something failed here or not. Be responsive. + */ + + int rc2 = RTCritSectEnter(&m_dataCS); + if (RT_SUCCESS(rc2)) + { + RTCString strFormatsCur = gX11->xAtomListToString(m_lstAtomFormats); + if (!strFormatsCur.isEmpty()) + { + strFormats = strFormatsCur; + dndActionDefault = VBOX_DND_ACTION_COPY; /** @todo Handle default action! */ + dndActionList = VBOX_DND_ACTION_COPY; /** @todo Ditto. */ + dndActionList |= toHGCMActions(m_lstAtomActions); + } + + RTCritSectLeave(&m_dataCS); + } + + rc2 = VbglR3DnDGHSendAckPending(&m_dndCtx, dndActionDefault, dndActionList, + strFormats.c_str(), strFormats.length() + 1 /* Include termination */); + LogFlowThisFunc(("uClientID=%RU32, dndActionDefault=0x%x, dndActionList=0x%x, strFormats=%s, rc=%Rrc\n", + m_dndCtx.uClientID, dndActionDefault, dndActionList, strFormats.c_str(), rc2)); + if (RT_FAILURE(rc2)) + { + VBClLogError("Error reporting pending drag and drop operation status to host: %Rrc\n", rc2); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Guest -> Host: Event signalling that the host has dropped the item(s) on the + * host side. + * + * @returns IPRT status code. + * @param strFormat Requested format to send to the host. + * @param dndActionRequested Requested action to perform on the guest. + */ +int DragInstance::ghDropped(const RTCString &strFormat, VBOXDNDACTION dndActionRequested) +{ + LogFlowThisFunc(("mode=%RU32, state=%RU32, strFormat=%s, dndActionRequested=0x%x\n", + m_enmMode, m_enmState, strFormat.c_str(), dndActionRequested)); + + /* Currently in wrong mode? Bail out. */ + if ( m_enmMode == Unknown + || m_enmMode == HG) + { + return VERR_INVALID_STATE; + } + + if ( m_enmMode == GH + && m_enmState != Dragging) + { + return VERR_INVALID_STATE; + } + + int rc = VINF_SUCCESS; + + m_enmState = Dropped; + +#ifdef DEBUG + XWindowAttributes xwa; + XGetWindowAttributes(m_pDisplay, m_wndCur, &xwa); + LogFlowThisFunc(("wndProxy=%RU32, wndCur=%RU32, x=%d, y=%d, width=%d, height=%d\n", + m_wndProxy.hWnd, m_wndCur, xwa.x, xwa.y, xwa.width, xwa.height)); + + Window wndSelection = XGetSelectionOwner(m_pDisplay, xAtom(XA_XdndSelection)); + LogFlowThisFunc(("wndSelection=%#x\n", wndSelection)); +#endif + + /* We send a fake mouse move event to the current window, cause + * this should have the grab. */ + mouseCursorFakeMove(); + + /** + * The fake button release event above should lead to a XdndDrop event from the + * source window. Because of showing our proxy window, other Xdnd events can + * occur before, e.g. a XdndPosition event. We are not interested + * in those, so just try to get the right one. + */ + + XClientMessageEvent evDnDDrop; + bool fDrop = waitForX11ClientMsg(evDnDDrop, xAtom(XA_XdndDrop), 5 * 1000 /* 5s timeout */); + if (fDrop) + { + LogFlowThisFunc(("XA_XdndDrop\n")); + + /* Request to convert the selection in the specific format and + * place it to our proxy window as property. */ + Assert(evDnDDrop.message_type == xAtom(XA_XdndDrop)); + + Window wndSource = evDnDDrop.data.l[XdndDropWindow]; /* Source window which has sent the message. */ + Assert(wndSource == m_wndCur); + + Atom aFormat = gX11->stringToxAtom(strFormat.c_str()); + + Time tsDrop; + if (m_curVer >= 1) + tsDrop = evDnDDrop.data.l[XdndDropTimeStamp]; + else + tsDrop = CurrentTime; + + XConvertSelection(m_pDisplay, xAtom(XA_XdndSelection), aFormat, xAtom(XA_XdndSelection), + m_wndProxy.hWnd, tsDrop); + + /* Wait for the selection notify event. */ + XEvent evSelNotify; + RT_ZERO(evSelNotify); + if (waitForX11Msg(evSelNotify, SelectionNotify, 5 * 1000 /* 5s timeout */)) + { + bool fCancel = false; + + /* Make some paranoid checks. */ + if ( evSelNotify.xselection.type == SelectionNotify + && evSelNotify.xselection.display == m_pDisplay + && evSelNotify.xselection.selection == xAtom(XA_XdndSelection) + && evSelNotify.xselection.requestor == m_wndProxy.hWnd + && evSelNotify.xselection.target == aFormat) + { + LogFlowThisFunc(("Selection notfiy (from wnd=%#x)\n", m_wndCur)); + + Atom aPropType; + int iPropFormat; + unsigned long cItems, cbRemaining; + unsigned char *pcData = NULL; + int xRc = XGetWindowProperty(m_pDisplay, m_wndProxy.hWnd, + xAtom(XA_XdndSelection) /* Property */, + 0 /* Offset */, + VBOX_MAX_XPROPERTIES /* Length of 32-bit multiples */, + True /* Delete property? */, + AnyPropertyType, /* Property type */ + &aPropType, &iPropFormat, &cItems, &cbRemaining, &pcData); + if (xRc != Success) + VBClLogError("Error getting XA_XdndSelection property of proxy window=%#x: %s\n", + m_wndProxy.hWnd, gX11->xErrorToString(xRc).c_str()); + + LogFlowThisFunc(("strType=%s, iPropFormat=%d, cItems=%RU32, cbRemaining=%RU32\n", + gX11->xAtomToString(aPropType).c_str(), iPropFormat, cItems, cbRemaining)); + + if ( aPropType != None + && pcData != NULL + && iPropFormat >= 8 + && cItems > 0 + && cbRemaining == 0) + { + size_t cbData = cItems * (iPropFormat / 8); + LogFlowThisFunc(("cbData=%zu\n", cbData)); + + /* For whatever reason some of the string MIME types are not + * zero terminated. Check that and correct it when necessary, + * because the guest side wants this in any case. */ + if ( m_lstAllowedFormats.contains(strFormat) + && pcData[cbData - 1] != '\0') + { + unsigned char *pvDataTmp = static_cast<unsigned char*>(RTMemAlloc(cbData + 1)); + if (pvDataTmp) + { + memcpy(pvDataTmp, pcData, cbData); + pvDataTmp[cbData++] = '\0'; + + rc = VbglR3DnDGHSendData(&m_dndCtx, strFormat.c_str(), pvDataTmp, cbData); + RTMemFree(pvDataTmp); + } + else + rc = VERR_NO_MEMORY; + } + else + { + /* Send the raw data to the host. */ + rc = VbglR3DnDGHSendData(&m_dndCtx, strFormat.c_str(), pcData, cbData); + LogFlowThisFunc(("Sent strFormat=%s, rc=%Rrc\n", strFormat.c_str(), rc)); + } + + if (RT_SUCCESS(rc)) + { + rc = m_wndProxy.sendFinished(wndSource, dndActionRequested); + } + else + fCancel = true; + } + else + { + if (aPropType == xAtom(XA_INCR)) + { + /** @todo Support incremental transfers. */ + AssertMsgFailed(("Incremental transfers are not supported yet\n")); + + VBClLogError("Incremental transfers are not supported yet\n"); + rc = VERR_NOT_IMPLEMENTED; + } + else + { + VBClLogError("Not supported data type: %s\n", gX11->xAtomToString(aPropType).c_str()); + rc = VERR_NOT_SUPPORTED; + } + + fCancel = true; + } + + if (fCancel) + { + VBClLogInfo("Cancelling dropping to host\n"); + + /* Cancel the operation -- inform the source window by + * sending a XdndFinished message so that the source can toss the required data. */ + rc = m_wndProxy.sendFinished(wndSource, VBOX_DND_ACTION_IGNORE); + } + + /* Cleanup. */ + if (pcData) + XFree(pcData); + } + else + rc = VERR_INVALID_PARAMETER; + } + else + rc = VERR_TIMEOUT; + } + else + rc = VERR_TIMEOUT; + + /* Inform the host on error. */ + if (RT_FAILURE(rc)) + { + int rc2 = VbglR3DnDGHSendError(&m_dndCtx, rc); + LogFlowThisFunc(("Sending error %Rrc to host resulted in %Rrc\n", rc, rc2)); RT_NOREF(rc2); + /* This is not fatal for us, just ignore. */ + } + + /* At this point, we have either successfully transfered any data or not. + * So reset our internal state because we are done here for the current (ongoing) + * drag and drop operation. */ + reset(); + + LogFlowFuncLeaveRC(rc); + return rc; +} +#endif /* VBOX_WITH_DRAG_AND_DROP_GH */ + +/* + * Helpers + */ + +/** + * Fakes moving the mouse cursor to provoke various drag and drop + * events such as entering a target window or moving within a + * source window. + * + * Not the most elegant and probably correct function, but does + * the work for now. + * + * @returns IPRT status code. + */ +int DragInstance::mouseCursorFakeMove(void) const +{ + int iScreenID = XDefaultScreen(m_pDisplay); + /** @todo What about multiple screens? Test this! */ + + const int iScrX = XDisplayWidth(m_pDisplay, iScreenID); + const int iScrY = XDisplayHeight(m_pDisplay, iScreenID); + + int fx, fy, rx, ry; + Window wndTemp, wndChild; + int wx, wy; unsigned int mask; + XQueryPointer(m_pDisplay, m_wndRoot, &wndTemp, &wndChild, &rx, &ry, &wx, &wy, &mask); + + /* + * Apply some simple clipping and change the position slightly. + */ + + /* FakeX */ + if (rx == 0) fx = 1; + else if (rx == iScrX) fx = iScrX - 1; + else fx = rx + 1; + + /* FakeY */ + if (ry == 0) fy = 1; + else if (ry == iScrY) fy = iScrY - 1; + else fy = ry + 1; + + /* + * Move the cursor to trigger the wanted events. + */ + LogFlowThisFunc(("cursorRootX=%d, cursorRootY=%d\n", fx, fy)); + int rc = mouseCursorMove(fx, fy); + if (RT_SUCCESS(rc)) + { + /* Move the cursor back to its original position. */ + rc = mouseCursorMove(rx, ry); + } + + return rc; +} + +/** + * Moves the mouse pointer to a specific position. + * + * @returns IPRT status code. + * @param iPosX Absolute X coordinate. + * @param iPosY Absolute Y coordinate. + */ +int DragInstance::mouseCursorMove(int iPosX, int iPosY) const +{ + int iScreenID = XDefaultScreen(m_pDisplay); + /** @todo What about multiple screens? Test this! */ + + const int iScrX = XDisplayWidth(m_pDisplay, iScreenID); + const int iScrY = XDisplayHeight(m_pDisplay, iScreenID); + + iPosX = RT_CLAMP(iPosX, 0, iScrX); + iPosY = RT_CLAMP(iPosY, 0, iScrY); + + LogFlowThisFunc(("iPosX=%d, iPosY=%d\n", iPosX, iPosY)); + + /* Move the guest pointer to the DnD position, so we can find the window + * below that position. */ + XWarpPointer(m_pDisplay, None, m_wndRoot, 0, 0, 0, 0, iPosX, iPosY); + return VINF_SUCCESS; +} + +/** + * Sends a mouse button event to a specific window. + * + * @param wndDest Window to send the mouse button event to. + * @param rx X coordinate relative to the root window's origin. + * @param ry Y coordinate relative to the root window's origin. + * @param iButton Mouse button to press/release. + * @param fPress Whether to press or release the mouse button. + */ +void DragInstance::mouseButtonSet(Window wndDest, int rx, int ry, int iButton, bool fPress) +{ + LogFlowThisFunc(("wndDest=%#x, rx=%d, ry=%d, iBtn=%d, fPress=%RTbool\n", + wndDest, rx, ry, iButton, fPress)); + +#ifdef VBOX_DND_WITH_XTEST + /** @todo Make this check run only once. */ + int ev, er, ma, mi; + if (XTestQueryExtension(m_pDisplay, &ev, &er, &ma, &mi)) + { + LogFlowThisFunc(("XText extension available\n")); + + int xRc = XTestFakeButtonEvent(m_pDisplay, 1, fPress ? True : False, CurrentTime); + if (Rc == 0) + VBClLogError("Error sending XTestFakeButtonEvent event: %s\n", gX11->xErrorToString(xRc).c_str()); + XFlush(m_pDisplay); + } + else + { +#endif + LogFlowThisFunc(("Note: XText extension not available or disabled\n")); + + unsigned int mask = 0; + + if ( rx == -1 + && ry == -1) + { + Window wndRoot, wndChild; + int wx, wy; + XQueryPointer(m_pDisplay, m_wndRoot, &wndRoot, &wndChild, &rx, &ry, &wx, &wy, &mask); + LogFlowThisFunc(("Mouse pointer is at root x=%d, y=%d\n", rx, ry)); + } + + XButtonEvent eBtn; + RT_ZERO(eBtn); + + eBtn.display = m_pDisplay; + eBtn.root = m_wndRoot; + eBtn.window = wndDest; + eBtn.subwindow = None; + eBtn.same_screen = True; + eBtn.time = CurrentTime; + eBtn.button = iButton; + eBtn.state = mask | (iButton == 1 ? Button1MotionMask : + iButton == 2 ? Button2MotionMask : + iButton == 3 ? Button3MotionMask : + iButton == 4 ? Button4MotionMask : + iButton == 5 ? Button5MotionMask : 0); + eBtn.type = fPress ? ButtonPress : ButtonRelease; + eBtn.send_event = False; + eBtn.x_root = rx; + eBtn.y_root = ry; + + XTranslateCoordinates(m_pDisplay, eBtn.root, eBtn.window, eBtn.x_root, eBtn.y_root, &eBtn.x, &eBtn.y, &eBtn.subwindow); + LogFlowThisFunc(("state=0x%x, x=%d, y=%d\n", eBtn.state, eBtn.x, eBtn.y)); + + int xRc = XSendEvent(m_pDisplay, wndDest, True /* fPropagate */, + ButtonPressMask, + reinterpret_cast<XEvent*>(&eBtn)); + if (xRc == 0) + VBClLogError("Error sending XButtonEvent event to window=%#x: %s\n", wndDest, gX11->xErrorToString(xRc).c_str()); + + XFlush(m_pDisplay); + +#ifdef VBOX_DND_WITH_XTEST + } +#endif +} + +/** + * Shows the (invisible) proxy window. The proxy window is needed for intercepting + * drags from the host to the guest or from the guest to the host. It acts as a proxy + * between the host and the actual (UI) element on the guest OS. + * + * To not make it miss any actions this window gets spawned across the entire guest + * screen (think of an umbrella) to (hopefully) capture everything. A proxy window + * which follows the cursor would be far too slow here. + * + * @returns IPRT status code. + * @param piRootX X coordinate relative to the root window's origin. Optional. + * @param piRootY Y coordinate relative to the root window's origin. Optional. + */ +int DragInstance::proxyWinShow(int *piRootX /* = NULL */, int *piRootY /* = NULL */) const +{ + /* piRootX is optional. */ + /* piRootY is optional. */ + + LogFlowThisFuncEnter(); + + int rc = VINF_SUCCESS; + +#if 0 +# ifdef VBOX_DND_WITH_XTEST + XTestGrabControl(m_pDisplay, False); +# endif +#endif + + /* Get the mouse pointer position and determine if we're on the same screen as the root window + * and return the current child window beneath our mouse pointer, if any. */ + int iRootX, iRootY; + int iChildX, iChildY; + unsigned int iMask; + Window wndRoot, wndChild; + Bool fInRootWnd = XQueryPointer(m_pDisplay, m_wndRoot, &wndRoot, &wndChild, + &iRootX, &iRootY, &iChildX, &iChildY, &iMask); + + LogFlowThisFunc(("fInRootWnd=%RTbool, wndRoot=%RU32, wndChild=%RU32, iRootX=%d, iRootY=%d\n", + RT_BOOL(fInRootWnd), wndRoot, wndChild, iRootX, iRootY)); RT_NOREF(fInRootWnd); + + if (piRootX) + *piRootX = iRootX; + if (piRootY) + *piRootY = iRootY; + + XSynchronize(m_pDisplay, True /* Enable sync */); + + /* Bring our proxy window into foreground. */ + XMapWindow(m_pDisplay, m_wndProxy.hWnd); + XRaiseWindow(m_pDisplay, m_wndProxy.hWnd); + + /* Spawn our proxy window over the entire screen, making it an easy drop target for the host's cursor. */ + LogFlowThisFunc(("Proxy window x=%d, y=%d, width=%d, height=%d\n", + m_wndProxy.iX, m_wndProxy.iY, m_wndProxy.iWidth, m_wndProxy.iHeight)); + XMoveResizeWindow(m_pDisplay, m_wndProxy.hWnd, m_wndProxy.iX, m_wndProxy.iY, m_wndProxy.iWidth, m_wndProxy.iHeight); + + XFlush(m_pDisplay); + + XSynchronize(m_pDisplay, False /* Disable sync */); + +#if 0 +# ifdef VBOX_DND_WITH_XTEST + XTestGrabControl(m_pDisplay, True); +# endif +#endif + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Hides the (invisible) proxy window. + */ +int DragInstance::proxyWinHide(void) +{ + LogFlowFuncEnter(); + + XUnmapWindow(m_pDisplay, m_wndProxy.hWnd); + XFlush(m_pDisplay); + + m_eventQueueList.clear(); + + return VINF_SUCCESS; /** @todo Add error checking. */ +} + +/** + * Allocates the name (title) of an X window. + * The returned pointer must be freed using RTStrFree(). + * + * @returns Pointer to the allocated window name. + * @param wndThis Window to retrieve name for. + * + * @remark If the window title is not available, the text + * "<No name>" will be returned. + */ +char *DragInstance::wndX11GetNameA(Window wndThis) const +{ + char *pszName = NULL; + + XTextProperty propName; + if (XGetWMName(m_pDisplay, wndThis, &propName)) + { + if (propName.value) + pszName = RTStrDup((char *)propName.value); /** @todo UTF8? */ + XFree(propName.value); + } + + if (!pszName) /* No window name found? */ + pszName = RTStrDup("<No name>"); + + return pszName; +} + +/** + * Clear a window's supported/accepted actions list. + * + * @param wndThis Window to clear the list for. + */ +void DragInstance::wndXDnDClearActionList(Window wndThis) const +{ + XDeleteProperty(m_pDisplay, wndThis, xAtom(XA_XdndActionList)); +} + +/** + * Clear a window's supported/accepted formats list. + * + * @param wndThis Window to clear the list for. + */ +void DragInstance::wndXDnDClearFormatList(Window wndThis) const +{ + XDeleteProperty(m_pDisplay, wndThis, xAtom(XA_XdndTypeList)); +} + +/** + * Retrieves a window's supported/accepted XDnD actions. + * + * @returns IPRT status code. + * @param wndThis Window to retrieve the XDnD actions for. + * @param lstActions Reference to VBoxDnDAtomList to store the action into. + */ +int DragInstance::wndXDnDGetActionList(Window wndThis, VBoxDnDAtomList &lstActions) const +{ + Atom iActType = None; + int iActFmt; + unsigned long cItems, cbData; + unsigned char *pcbData = NULL; + + /* Fetch the possible list of actions, if this property is set. */ + int xRc = XGetWindowProperty(m_pDisplay, wndThis, + xAtom(XA_XdndActionList), + 0, VBOX_MAX_XPROPERTIES, + False, XA_ATOM, &iActType, &iActFmt, &cItems, &cbData, &pcbData); + if (xRc != Success) + { + LogFlowThisFunc(("Error getting XA_XdndActionList atoms from window=%#x: %s\n", + wndThis, gX11->xErrorToString(xRc).c_str())); + return VERR_NOT_FOUND; + } + + LogFlowThisFunc(("wndThis=%#x, cItems=%RU32, pcbData=%p\n", wndThis, cItems, pcbData)); + + if (cItems > 0) + { + AssertPtr(pcbData); + Atom *paData = reinterpret_cast<Atom *>(pcbData); + + for (unsigned i = 0; i < RT_MIN(VBOX_MAX_XPROPERTIES, cItems); i++) + { + LogFlowThisFunc(("\t%s\n", gX11->xAtomToString(paData[i]).c_str())); + lstActions.append(paData[i]); + } + + XFree(pcbData); + } + + return VINF_SUCCESS; +} + +/** + * Retrieves a window's supported/accepted XDnD formats. + * + * @returns IPRT status code. + * @param wndThis Window to retrieve the XDnD formats for. + * @param lstTypes Reference to VBoxDnDAtomList to store the formats into. + */ +int DragInstance::wndXDnDGetFormatList(Window wndThis, VBoxDnDAtomList &lstTypes) const +{ + Atom iActType = None; + int iActFmt; + unsigned long cItems, cbData; + unsigned char *pcbData = NULL; + + int xRc = XGetWindowProperty(m_pDisplay, wndThis, + xAtom(XA_XdndTypeList), + 0, VBOX_MAX_XPROPERTIES, + False, XA_ATOM, &iActType, &iActFmt, &cItems, &cbData, &pcbData); + if (xRc != Success) + { + LogFlowThisFunc(("Error getting XA_XdndTypeList atoms from window=%#x: %s\n", + wndThis, gX11->xErrorToString(xRc).c_str())); + return VERR_NOT_FOUND; + } + + LogFlowThisFunc(("wndThis=%#x, cItems=%RU32, pcbData=%p\n", wndThis, cItems, pcbData)); + + if (cItems > 0) + { + AssertPtr(pcbData); + Atom *paData = reinterpret_cast<Atom *>(pcbData); + + for (unsigned i = 0; i < RT_MIN(VBOX_MAX_XPROPERTIES, cItems); i++) + { + LogFlowThisFunc(("\t%s\n", gX11->xAtomToString(paData[i]).c_str())); + lstTypes.append(paData[i]); + } + + XFree(pcbData); + } + + return VINF_SUCCESS; +} + +/** + * Sets (replaces) a window's XDnD accepted/allowed actions. + * + * @returns IPRT status code. + * @param wndThis Window to set the format list for. + * @param lstActions Reference to list of XDnD actions to set. + */ +int DragInstance::wndXDnDSetActionList(Window wndThis, const VBoxDnDAtomList &lstActions) const +{ + if (lstActions.isEmpty()) + return VINF_SUCCESS; + + XChangeProperty(m_pDisplay, wndThis, + xAtom(XA_XdndActionList), + XA_ATOM, 32, PropModeReplace, + reinterpret_cast<const unsigned char*>(lstActions.raw()), + lstActions.size()); + + return VINF_SUCCESS; +} + +/** + * Sets (replaces) a window's XDnD accepted format list. + * + * @returns IPRT status code. + * @param wndThis Window to set the format list for. + * @param atmProp Property to set. + * @param lstFormats Reference to list of XDnD formats to set. + */ +int DragInstance::wndXDnDSetFormatList(Window wndThis, Atom atmProp, const VBoxDnDAtomList &lstFormats) const +{ + if (lstFormats.isEmpty()) + return VERR_INVALID_PARAMETER; + + /* Add the property with the property data to the window. */ + XChangeProperty(m_pDisplay, wndThis, atmProp, + XA_ATOM, 32, PropModeReplace, + reinterpret_cast<const unsigned char*>(lstFormats.raw()), + lstFormats.size()); + + return VINF_SUCCESS; +} + +/** + * Appends a RTCString list to VBoxDnDAtomList list. + * + * @returns IPRT status code. + * @param lstFormats Reference to RTCString list to convert. + * @param lstAtoms Reference to VBoxDnDAtomList list to store results in. + */ +int DragInstance::appendFormatsToList(const RTCList<RTCString> &lstFormats, VBoxDnDAtomList &lstAtoms) const +{ + for (size_t i = 0; i < lstFormats.size(); ++i) + lstAtoms.append(XInternAtom(m_pDisplay, lstFormats.at(i).c_str(), False)); + + return VINF_SUCCESS; +} + +/** + * Appends a raw-data string list to VBoxDnDAtomList list. + * + * @returns IPRT status code. + * @param pvData Pointer to string data to convert. + * @param cbData Size (in bytes) to convert. + * @param lstAtoms Reference to VBoxDnDAtomList list to store results in. + */ +int DragInstance::appendDataToList(const void *pvData, uint32_t cbData, VBoxDnDAtomList &lstAtoms) const +{ + RT_NOREF1(lstAtoms); + AssertPtrReturn(pvData, VERR_INVALID_POINTER); + AssertReturn(cbData, VERR_INVALID_PARAMETER); + + const char *pszStr = (char *)pvData; + uint32_t cbStr = cbData; + + int rc = VINF_SUCCESS; + + VBoxDnDAtomList lstAtom; + while (cbStr) + { + size_t cbSize = RTStrNLen(pszStr, cbStr); + + /* Create a copy with max N chars, so that we are on the save side, + * even if the data isn't zero terminated. */ + char *pszTmp = RTStrDupN(pszStr, cbSize); + if (!pszTmp) + { + rc = VERR_NO_MEMORY; + break; + } + + lstAtom.append(XInternAtom(m_pDisplay, pszTmp, False)); + RTStrFree(pszTmp); + + pszStr += cbSize + 1; + cbStr -= cbSize + 1; + } + + return rc; +} + +/** + * Converts a HGCM-based drag'n drop action to a Atom-based drag'n drop action. + * + * @returns Converted Atom-based drag'n drop action. + * @param dndAction HGCM drag'n drop actions to convert. + */ +/* static */ +Atom DragInstance::toAtomAction(VBOXDNDACTION dndAction) +{ + /* Ignore is None. */ + return (isDnDCopyAction(dndAction) ? xAtom(XA_XdndActionCopy) : + isDnDMoveAction(dndAction) ? xAtom(XA_XdndActionMove) : + isDnDLinkAction(dndAction) ? xAtom(XA_XdndActionLink) : + None); +} + +/** + * Converts HGCM-based drag'n drop actions to a VBoxDnDAtomList list. + * + * @returns IPRT status code. + * @param dndActionList HGCM drag'n drop actions to convert. + * @param lstAtoms Reference to VBoxDnDAtomList to store actions in. + */ +/* static */ +int DragInstance::toAtomActions(VBOXDNDACTIONLIST dndActionList, VBoxDnDAtomList &lstAtoms) +{ + if (hasDnDCopyAction(dndActionList)) + lstAtoms.append(xAtom(XA_XdndActionCopy)); + if (hasDnDMoveAction(dndActionList)) + lstAtoms.append(xAtom(XA_XdndActionMove)); + if (hasDnDLinkAction(dndActionList)) + lstAtoms.append(xAtom(XA_XdndActionLink)); + + return VINF_SUCCESS; +} + +/** + * Converts an Atom-based drag'n drop action to a HGCM drag'n drop action. + * + * @returns HGCM drag'n drop action. + * @param atom Atom-based drag'n drop action to convert. + */ +/* static */ +uint32_t DragInstance::toHGCMAction(Atom atom) +{ + uint32_t uAction = VBOX_DND_ACTION_IGNORE; + + if (atom == xAtom(XA_XdndActionCopy)) + uAction = VBOX_DND_ACTION_COPY; + else if (atom == xAtom(XA_XdndActionMove)) + uAction = VBOX_DND_ACTION_MOVE; + else if (atom == xAtom(XA_XdndActionLink)) + uAction = VBOX_DND_ACTION_LINK; + + return uAction; +} + +/** + * Converts an VBoxDnDAtomList list to an HGCM action list. + * + * @returns ORed HGCM action list. + * @param lstActions List of Atom-based actions to convert. + */ +/* static */ +uint32_t DragInstance::toHGCMActions(const VBoxDnDAtomList &lstActions) +{ + uint32_t uActions = VBOX_DND_ACTION_IGNORE; + + for (size_t i = 0; i < lstActions.size(); i++) + uActions |= toHGCMAction(lstActions.at(i)); + + return uActions; +} + +/********************************************************************************************************************************* + * VBoxDnDProxyWnd implementation. * + ********************************************************************************************************************************/ + +VBoxDnDProxyWnd::VBoxDnDProxyWnd(void) + : pDisp(NULL) + , hWnd(0) + , iX(0) + , iY(0) + , iWidth(0) + , iHeight(0) +{ + +} + +VBoxDnDProxyWnd::~VBoxDnDProxyWnd(void) +{ + destroy(); +} + +int VBoxDnDProxyWnd::init(Display *pDisplay) +{ + /** @todo What about multiple screens? Test this! */ + int iScreenID = XDefaultScreen(pDisplay); + + iWidth = XDisplayWidth(pDisplay, iScreenID); + iHeight = XDisplayHeight(pDisplay, iScreenID); + pDisp = pDisplay; + + return VINF_SUCCESS; +} + +void VBoxDnDProxyWnd::destroy(void) +{ + +} + +int VBoxDnDProxyWnd::sendFinished(Window hWndSource, VBOXDNDACTION dndAction) +{ + /* Was the drop accepted by the host? That is, anything than ignoring. */ + bool fDropAccepted = dndAction > VBOX_DND_ACTION_IGNORE; + + LogFlowFunc(("dndAction=0x%x\n", dndAction)); + + /* Confirm the result of the transfer to the target window. */ + XClientMessageEvent m; + RT_ZERO(m); + m.type = ClientMessage; + m.display = pDisp; + m.window = hWnd; + m.message_type = xAtom(XA_XdndFinished); + m.format = 32; + m.data.l[XdndFinishedWindow] = hWnd; /* Target window. */ + m.data.l[XdndFinishedFlags] = fDropAccepted ? RT_BIT(0) : 0; /* Was the drop accepted? */ + m.data.l[XdndFinishedAction] = fDropAccepted ? DragInstance::toAtomAction(dndAction) : None; /* Action used on accept. */ + + int xRc = XSendEvent(pDisp, hWndSource, True, NoEventMask, reinterpret_cast<XEvent*>(&m)); + if (xRc == 0) + { + VBClLogError("Error sending XA_XdndFinished event to source window=%#x: %s\n", + hWndSource, gX11->xErrorToString(xRc).c_str()); + + return VERR_GENERAL_FAILURE; /** @todo Fudge. */ + } + + return VINF_SUCCESS; +} + +/********************************************************************************************************************************* + * DragAndDropService implementation. * + ********************************************************************************************************************************/ + +/** + * Initializes the drag and drop service. + * + * @returns IPRT status code. + */ +int DragAndDropService::init(void) +{ + LogFlowFuncEnter(); + + /* Connect to the x11 server. */ + m_pDisplay = XOpenDisplay(NULL); + if (!m_pDisplay) + { + VBClLogFatalError("Unable to connect to X server -- running in a terminal session?\n"); + return VERR_NOT_FOUND; + } + + xHelpers *pHelpers = xHelpers::getInstance(m_pDisplay); + if (!pHelpers) + return VERR_NO_MEMORY; + + int rc; + + do + { + rc = RTSemEventCreate(&m_hEventSem); + AssertRCBreak(rc); + + rc = RTCritSectInit(&m_eventQueueCS); + AssertRCBreak(rc); + + rc = VbglR3DnDConnect(&m_dndCtx); + AssertRCBreak(rc); + + /* Event thread for events coming from the HGCM device. */ + rc = RTThreadCreate(&m_hHGCMThread, hgcmEventThread, this, + 0, RTTHREADTYPE_MSG_PUMP, RTTHREADFLAGS_WAITABLE, "dndHGCM"); + AssertRCBreak(rc); + + rc = RTThreadUserWait(m_hHGCMThread, 10 * 1000 /* 10s timeout */); + AssertRCBreak(rc); + + if (ASMAtomicReadBool(&m_fStop)) + break; + + /* Event thread for events coming from the x11 system. */ + rc = RTThreadCreate(&m_hX11Thread, x11EventThread, this, + 0, RTTHREADTYPE_MSG_PUMP, RTTHREADFLAGS_WAITABLE, "dndX11"); + AssertRCBreak(rc); + + rc = RTThreadUserWait(m_hX11Thread, 10 * 1000 /* 10s timeout */); + AssertRCBreak(rc); + + if (ASMAtomicReadBool(&m_fStop)) + break; + + } while (0); + + if (m_fStop) + rc = VERR_GENERAL_FAILURE; /** @todo Fudge! */ + + if (RT_FAILURE(rc)) + VBClLogError("Failed to initialize, rc=%Rrc\n", rc); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Main loop for the drag and drop service which does the HGCM message + * processing and routing to the according drag and drop instance(s). + * + * @returns IPRT status code. + * @param fDaemonised Whether to run in daemonized or not. Does not + * apply for this service. + */ +int DragAndDropService::run(bool fDaemonised /* = false */) +{ + RT_NOREF1(fDaemonised); + LogFlowThisFunc(("fDaemonised=%RTbool\n", fDaemonised)); + + int rc; + do + { + m_pCurDnD = new DragInstance(m_pDisplay, this); + if (!m_pCurDnD) + { + rc = VERR_NO_MEMORY; + break; + } + + /* Note: For multiple screen support in VBox it is not necessary to use + * another screen number than zero. Maybe in the future it will become + * necessary if VBox supports multiple X11 screens. */ + rc = m_pCurDnD->init(0 /* uScreenID */); + /* Note: Can return VINF_PERMISSION_DENIED if HGCM host service is not available. */ + if (rc != VINF_SUCCESS) + { + if (RT_FAILURE(rc)) + VBClLogError("Unable to connect to drag and drop service, rc=%Rrc\n", rc); + else if (rc == VINF_PERMISSION_DENIED) + VBClLogError("Not available on host, terminating\n"); + break; + } + + VBClLogInfo("Started\n"); + VBClLogInfo("%sr%s\n", RTBldCfgVersion(), RTBldCfgRevisionStr()); + + /* Enter the main event processing loop. */ + do + { + DnDEvent e; + RT_ZERO(e); + + LogFlowFunc(("Waiting for new event ...\n")); + rc = RTSemEventWait(m_hEventSem, RT_INDEFINITE_WAIT); + if (RT_FAILURE(rc)) + break; + + AssertMsg(m_eventQueue.size(), ("Event queue is empty when it shouldn't\n")); + + e = m_eventQueue.first(); + m_eventQueue.removeFirst(); + + if (e.enmType == DnDEvent::DnDEventType_HGCM) + { + PVBGLR3DNDEVENT pVbglR3Event = e.hgcm; + AssertPtrBreak(pVbglR3Event); + + LogFlowThisFunc(("HGCM event, enmType=%RU32\n", pVbglR3Event->enmType)); + switch (pVbglR3Event->enmType) + { + case VBGLR3DNDEVENTTYPE_HG_ENTER: + { + if (pVbglR3Event->u.HG_Enter.cbFormats) + { + RTCList<RTCString> lstFormats = + RTCString(pVbglR3Event->u.HG_Enter.pszFormats, pVbglR3Event->u.HG_Enter.cbFormats - 1).split("\r\n"); + rc = m_pCurDnD->hgEnter(lstFormats, pVbglR3Event->u.HG_Enter.dndLstActionsAllowed); + if (RT_FAILURE(rc)) + break; + /* Enter is always followed by a move event. */ + } + else + { + AssertMsgFailed(("cbFormats is 0\n")); + rc = VERR_INVALID_PARAMETER; + break; + } + + /* Note: After HOST_DND_FN_HG_EVT_ENTER there immediately is a move + * event, so fall through is intentional here. */ + RT_FALL_THROUGH(); + } + + case VBGLR3DNDEVENTTYPE_HG_MOVE: + { + rc = m_pCurDnD->hgMove(pVbglR3Event->u.HG_Move.uXpos, pVbglR3Event->u.HG_Move.uYpos, + pVbglR3Event->u.HG_Move.dndActionDefault); + break; + } + + case VBGLR3DNDEVENTTYPE_HG_LEAVE: + { + rc = m_pCurDnD->hgLeave(); + break; + } + + case VBGLR3DNDEVENTTYPE_HG_DROP: + { + rc = m_pCurDnD->hgDrop(pVbglR3Event->u.HG_Drop.uXpos, pVbglR3Event->u.HG_Drop.uYpos, + pVbglR3Event->u.HG_Drop.dndActionDefault); + break; + } + + /* Note: VbglR3DnDRecvNextMsg() will return HOST_DND_FN_HG_SND_DATA_HDR when + * the host has finished copying over all the data to the guest. + * + * The actual data transfer (and message processing for it) will be done + * internally by VbglR3DnDRecvNextMsg() to not duplicate any code for different + * platforms. + * + * The data header now will contain all the (meta) data the guest needs in + * order to complete the DnD operation. */ + case VBGLR3DNDEVENTTYPE_HG_RECEIVE: + { + rc = m_pCurDnD->hgDataReceive(&pVbglR3Event->u.HG_Received.Meta); + break; + } + + case VBGLR3DNDEVENTTYPE_HG_CANCEL: + { + m_pCurDnD->reset(); /** @todo Test this! */ + break; + } + +#ifdef VBOX_WITH_DRAG_AND_DROP_GH + case VBGLR3DNDEVENTTYPE_GH_ERROR: + { + m_pCurDnD->reset(); + break; + } + + case VBGLR3DNDEVENTTYPE_GH_REQ_PENDING: + { + rc = m_pCurDnD->ghIsDnDPending(); + break; + } + + case VBGLR3DNDEVENTTYPE_GH_DROP: + { + rc = m_pCurDnD->ghDropped(pVbglR3Event->u.GH_Drop.pszFormat, pVbglR3Event->u.GH_Drop.dndActionRequested); + break; + } +#endif + default: + { + VBClLogError("Received unsupported message '%RU32'\n", pVbglR3Event->enmType); + rc = VERR_NOT_SUPPORTED; + break; + } + } + + LogFlowFunc(("Message %RU32 processed with %Rrc\n", pVbglR3Event->enmType, rc)); + if (RT_FAILURE(rc)) + { + /* Tell the user. */ + VBClLogError("Processing message %RU32 failed with %Rrc\n", pVbglR3Event->enmType, rc); + + /* If anything went wrong, do a reset and start over. */ + m_pCurDnD->reset(); + } + + VbglR3DnDEventFree(e.hgcm); + e.hgcm = NULL; + } + else if (e.enmType == DnDEvent::DnDEventType_X11) + { + m_pCurDnD->onX11Event(e.x11); + } + else + AssertMsgFailed(("Unknown event queue type %RU32\n", e.enmType)); + + /* + * Make sure that any X11 requests have actually been sent to the + * server, since we are waiting for responses using poll() on + * another thread which will not automatically trigger flushing. + */ + XFlush(m_pDisplay); + + } while (!ASMAtomicReadBool(&m_fStop)); + + VBClLogInfo("Stopped with rc=%Rrc\n", rc); + + } while (0); + + if (m_pCurDnD) + { + delete m_pCurDnD; + m_pCurDnD = NULL; + } + + LogFlowFuncLeaveRC(rc); + return rc; +} + +void DragAndDropService::cleanup(void) +{ + LogFlowFuncEnter(); + + VBClLogInfo("Terminating ...\n"); + + /* Set stop flag first. */ + ASMAtomicXchgBool(&m_fStop, true); + + /* Disconnect from the HGCM host service, which in turn will make the HGCM thread stop. */ + VbglR3DnDDisconnect(&m_dndCtx); + + /* + * Wait for threads to terminate. + */ + int rcThread, rc2; + if (m_hHGCMThread != NIL_RTTHREAD) + { + VBClLogInfo("Terminating HGCM thread ...\n"); + + rc2 = RTThreadWait(m_hHGCMThread, 30 * 1000 /* 30s timeout */, &rcThread); + if (RT_SUCCESS(rc2)) + rc2 = rcThread; + + if (RT_FAILURE(rc2)) + VBClLogInfo("Error waiting for HGCM thread to terminate: %Rrc\n", rc2); + } + + if (m_hX11Thread != NIL_RTTHREAD) + { + VBClLogInfo("Terminating X11 thread ...\n"); + + rc2 = RTThreadWait(m_hX11Thread, 200 /* 200ms timeout */, &rcThread); + if (RT_SUCCESS(rc2)) + rc2 = rcThread; + + if (RT_FAILURE(rc2)) + VBClLogError("Error waiting for X11 thread to terminate: %Rrc\n", rc2); + } + + VBClLogInfo("Terminating threads done\n"); + + xHelpers::destroyInstance(); + + VbglR3Term(); +} + +/** + * Static callback function for HGCM message processing thread. An internal + * message queue will be filled which then will be processed by the according + * drag'n drop instance. + * + * @returns IPRT status code. + * @param hThread Thread handle to use. + * @param pvUser Pointer to DragAndDropService instance to use. + */ +/* static */ +DECLCALLBACK(int) DragAndDropService::hgcmEventThread(RTTHREAD hThread, void *pvUser) +{ + AssertPtrReturn(pvUser, VERR_INVALID_PARAMETER); + DragAndDropService *pThis = static_cast<DragAndDropService*>(pvUser); + + /* Let the service instance know in any case. */ + int rc = RTThreadUserSignal(hThread); + AssertRCReturn(rc, rc); + + /* Number of invalid messages skipped in a row. */ + int cMsgSkippedInvalid = 0; + DnDEvent e; + + do + { + RT_ZERO(e); + e.enmType = DnDEvent::DnDEventType_HGCM; + + /* Wait for new events. */ + rc = VbglR3DnDEventGetNext(&pThis->m_dndCtx, &e.hgcm); + if (RT_SUCCESS(rc)) + { + cMsgSkippedInvalid = 0; /* Reset skipped messages count. */ + pThis->m_eventQueue.append(e); + + rc = RTSemEventSignal(pThis->m_hEventSem); + if (RT_FAILURE(rc)) + break; + } + else + { + if (rc == VERR_INTERRUPTED) /* Can happen due to disconnect, for instance. */ + rc = VINF_SUCCESS; + + if (RT_FAILURE(rc)) + { + VBClLogError("Processing next message failed with rc=%Rrc\n", rc); + + /* Old(er) hosts either are broken regarding DnD support or otherwise + * don't support the stuff we do on the guest side, so make sure we + * don't process invalid messages forever. */ + + if (cMsgSkippedInvalid++ > 32) + { + VBClLogError("Too many invalid/skipped messages from host, exiting ...\n"); + break; + } + } + } + + } while (!ASMAtomicReadBool(&pThis->m_fStop)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Static callback function for X11 message processing thread. All X11 messages + * will be directly routed to the according drag'n drop instance. + * + * @returns IPRT status code. + * @param hThread Thread handle to use. + * @param pvUser Pointer to DragAndDropService instance to use. + */ +/* static */ +DECLCALLBACK(int) DragAndDropService::x11EventThread(RTTHREAD hThread, void *pvUser) +{ + AssertPtrReturn(pvUser, VERR_INVALID_PARAMETER); + DragAndDropService *pThis = static_cast<DragAndDropService*>(pvUser); + AssertPtr(pThis); + + int rc = VINF_SUCCESS; + + /* Note: Nothing to initialize here (yet). */ + + /* Set stop indicator on failure. */ + if (RT_FAILURE(rc)) + ASMAtomicXchgBool(&pThis->m_fStop, true); + + /* Let the service instance know in any case. */ + int rc2 = RTThreadUserSignal(hThread); + AssertRC(rc2); + + DnDEvent e; + do + { + /* + * Wait for new events. We can't use XIfEvent here, cause this locks + * the window connection with a mutex and if no X11 events occurs this + * blocks any other calls we made to X11. So instead check for new + * events and if there are not any new one, sleep for a certain amount + * of time. + */ + if (XEventsQueued(pThis->m_pDisplay, QueuedAfterFlush) > 0) + { + RT_ZERO(e); + e.enmType = DnDEvent::DnDEventType_X11; + + /* XNextEvent will block until a new X event becomes available. */ + XNextEvent(pThis->m_pDisplay, &e.x11); + { + /* At the moment we only have one drag instance. */ + DragInstance *pInstance = pThis->m_pCurDnD; + AssertPtr(pInstance); + + pInstance->onX11Event(e.x11); + } + } + else + RTThreadSleep(25 /* ms */); + + } while (!ASMAtomicReadBool(&pThis->m_fStop)); + + LogFlowFuncLeaveRC(rc); + return rc; +} + +/** Drag and drop magic number, start of a UUID. */ +#define DRAGANDDROPSERVICE_MAGIC 0x67c97173 + +/** VBoxClient service class wrapping the logic for the service while + * the main VBoxClient code provides the daemon logic needed by all services. + */ +struct DRAGANDDROPSERVICE +{ + /** The service interface. */ + struct VBCLSERVICE *pInterface; + /** Magic number for sanity checks. */ + uint32_t uMagic; + /** Service object. */ + DragAndDropService mDragAndDrop; +}; + +static const char *getName() +{ + return "Drag and Drop (DnD)"; +} + +static const char *getPidFilePath() +{ + return ".vboxclient-draganddrop.pid"; +} + +static int init(struct VBCLSERVICE **ppInterface) +{ + struct DRAGANDDROPSERVICE *pSelf = (struct DRAGANDDROPSERVICE *)ppInterface; + + if (pSelf->uMagic != DRAGANDDROPSERVICE_MAGIC) + VBClLogFatalError("Bad DnD service object!\n"); + return pSelf->mDragAndDrop.init(); +} + +static int run(struct VBCLSERVICE **ppInterface, bool fDaemonised) +{ + struct DRAGANDDROPSERVICE *pSelf = (struct DRAGANDDROPSERVICE *)ppInterface; + + if (pSelf->uMagic != DRAGANDDROPSERVICE_MAGIC) + VBClLogFatalError("Bad DnD service object!\n"); + return pSelf->mDragAndDrop.run(fDaemonised); +} + +static void cleanup(struct VBCLSERVICE **ppInterface) +{ + struct DRAGANDDROPSERVICE *pSelf = (struct DRAGANDDROPSERVICE *)ppInterface; + + if (pSelf->uMagic != DRAGANDDROPSERVICE_MAGIC) + VBClLogFatalError("Bad DnD service object!\n"); + return pSelf->mDragAndDrop.cleanup(); +} + +struct VBCLSERVICE vbclDragAndDropInterface = +{ + getName, + getPidFilePath, + init, + run, + cleanup +}; + +/* Static factory. */ +struct VBCLSERVICE **VBClGetDragAndDropService(void) +{ + struct DRAGANDDROPSERVICE *pService = + (struct DRAGANDDROPSERVICE *)RTMemAlloc(sizeof(*pService)); + + if (!pService) + VBClLogFatalError("Out of memory\n"); + pService->pInterface = &vbclDragAndDropInterface; + pService->uMagic = DRAGANDDROPSERVICE_MAGIC; + new(&pService->mDragAndDrop) DragAndDropService(); + return &pService->pInterface; +} diff --git a/src/VBox/Additions/x11/VBoxClient/hostversion.cpp b/src/VBox/Additions/x11/VBoxClient/hostversion.cpp new file mode 100644 index 00000000..76df50c9 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/hostversion.cpp @@ -0,0 +1,229 @@ +/* $Id: hostversion.cpp $ */ +/** @file + * X11 guest client - host version check. + */ + +/* + * Copyright (C) 2011-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ +#include <stdio.h> +#include <iprt/assert.h> +#include <iprt/errcore.h> +#include <iprt/mem.h> +#include <iprt/ldr.h> +#include <iprt/string.h> +#include <iprt/thread.h> + +#ifdef VBOX_WITH_DBUS +# include <VBox/dbus.h> +#endif +#include <VBox/log.h> +#include <VBox/VBoxGuestLib.h> +#ifdef VBOX_OSE +# include <VBox/version.h> +#endif + +#include "VBoxClient.h" + +static const char *getName() +{ + return "Host Version Check"; +} + +static const char *getPidFilePath() +{ + return ".vboxclient-hostversion.pid"; +} + +static int showNotify(const char *pszHeader, const char *pszBody) +{ + int rc; +# ifdef VBOX_WITH_DBUS + DBusConnection *conn; + DBusMessage* msg = NULL; + conn = dbus_bus_get (DBUS_BUS_SESSION, NULL); + if (conn == NULL) + { + LogRelFlowFunc(("Could not retrieve D-BUS session bus!\n")); + rc = VERR_INVALID_HANDLE; + } + else + { + msg = dbus_message_new_method_call("org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "Notify"); + if (msg == NULL) + { + VBClLogError("Could not create D-BUS message!\n"); + rc = VERR_INVALID_HANDLE; + } + else + rc = VINF_SUCCESS; + } + if (RT_SUCCESS(rc)) + { + uint32_t msg_replace_id = 0; + const char *msg_app = "VBoxClient"; + const char *msg_icon = ""; + const char *msg_summary = pszHeader; + const char *msg_body = pszBody; + int32_t msg_timeout = -1; /* Let the notification server decide */ + + DBusMessageIter iter; + DBusMessageIter array; + /*DBusMessageIter dict; - unused */ + /*DBusMessageIter value; - unused */ + /*DBusMessageIter variant; - unused */ + /*DBusMessageIter data; - unused */ + + /* Format: UINT32 org.freedesktop.Notifications.Notify + * (STRING app_name, UINT32 replaces_id, STRING app_icon, STRING summary, STRING body, + * ARRAY actions, DICT hints, INT32 expire_timeout) + */ + dbus_message_iter_init_append(msg,&iter); + dbus_message_iter_append_basic(&iter,DBUS_TYPE_STRING,&msg_app); + dbus_message_iter_append_basic(&iter,DBUS_TYPE_UINT32,&msg_replace_id); + dbus_message_iter_append_basic(&iter,DBUS_TYPE_STRING,&msg_icon); + dbus_message_iter_append_basic(&iter,DBUS_TYPE_STRING,&msg_summary); + dbus_message_iter_append_basic(&iter,DBUS_TYPE_STRING,&msg_body); + dbus_message_iter_open_container(&iter,DBUS_TYPE_ARRAY,DBUS_TYPE_STRING_AS_STRING,&array); + dbus_message_iter_close_container(&iter,&array); + dbus_message_iter_open_container(&iter,DBUS_TYPE_ARRAY,"{sv}",&array); + dbus_message_iter_close_container(&iter,&array); + dbus_message_iter_append_basic(&iter,DBUS_TYPE_INT32,&msg_timeout); + + DBusError err; + dbus_error_init(&err); + + DBusMessage *reply; + reply = dbus_connection_send_with_reply_and_block(conn, msg, 30 * 1000 /* 30 seconds timeout */, &err); + if (dbus_error_is_set(&err)) + VBClLogError("D-BUS returned an error while sending the notification: %s", err.message); + else if (reply) + { + dbus_connection_flush(conn); + dbus_message_unref(reply); + } + if (dbus_error_is_set(&err)) + dbus_error_free(&err); + } + if (msg != NULL) + dbus_message_unref(msg); +# else + /** @todo Implement me */ + RT_NOREF(pszHeader, pszBody); + rc = VINF_SUCCESS; +# endif /* VBOX_WITH_DBUS */ + return rc; +} + +/** @todo Move this part in VbglR3 and just provide a callback for the platform-specific + notification stuff, since this is very similar to the VBoxTray code. */ +static int run(struct VBCLSERVICE **ppInterface, bool fDaemonised) +{ + int rc; + LogFlowFunc(("\n")); + + RT_NOREF(ppInterface); + + /* Because we need desktop notifications to be displayed, wait + * some time to make the desktop environment load (as a work around). */ + if (fDaemonised) + RTThreadSleep(30 * 1000 /* Wait 30 seconds */); + +# ifdef VBOX_WITH_DBUS + rc = RTDBusLoadLib(); + if (RT_FAILURE(rc)) + VBClLogError("D-Bus seems not to be installed; no host version check/notification done\n"); +# else + rc = VERR_NOT_IMPLEMENTED; +# endif /* VBOX_WITH_DBUS */ + +# ifdef VBOX_WITH_GUEST_PROPS + uint32_t uGuestPropSvcClientID; + if (RT_SUCCESS(rc)) + { + rc = VbglR3GuestPropConnect(&uGuestPropSvcClientID); + if (RT_FAILURE(rc)) + VBClLogError("Cannot connect to guest property service while chcking for host version! rc = %Rrc\n", rc); + } + + if (RT_SUCCESS(rc)) + { + char *pszHostVersion; + char *pszGuestVersion; + bool bUpdate; + + rc = VbglR3HostVersionCheckForUpdate(uGuestPropSvcClientID, &bUpdate, &pszHostVersion, &pszGuestVersion); + if (RT_SUCCESS(rc)) + { + if (bUpdate) + { + char szMsg[1024]; + char szTitle[64]; + + /** @todo add some translation macros here */ + RTStrPrintf(szTitle, sizeof(szTitle), "VirtualBox Guest Additions update available!"); +#ifndef VBOX_OSE + RTStrPrintf(szMsg, sizeof(szMsg), "Your guest is currently running the Guest Additions version %s. " + "We recommend updating to the latest version (%s) by choosing the " + "install option from the Devices menu.", pszGuestVersion, pszHostVersion); +#else +/* This is the message which appears for non-Oracle builds of the +* Guest Additions. Distributors are encouraged to customise this. */ + RTStrPrintf(szMsg, sizeof(szMsg), "Your virtual machine is currently running the Guest Additions version %s. Since you are running a version of the Guest Additions provided by the operating system you installed in the virtual machine we recommend that you update it to at least version %s using that system's update features, or alternatively that you remove this version and then install the " VBOX_VENDOR_SHORT " Guest Additions package using the install option from the Devices menu. Please consult the documentation for the operating system you are running to find out how to update or remove the current Guest Additions package.", pszGuestVersion, pszHostVersion); +#endif + rc = showNotify(szTitle, szMsg); + VBClLogInfo("VirtualBox Guest Additions update available!\n"); + if (RT_FAILURE(rc)) + VBClLogError("Could not show version notifier tooltip! rc = %d\n", rc); + } + + /* Store host version to not notify again */ + rc = VbglR3HostVersionLastCheckedStore(uGuestPropSvcClientID, pszHostVersion); + + VbglR3GuestPropReadValueFree(pszHostVersion); + VbglR3GuestPropReadValueFree(pszGuestVersion); + } + VbglR3GuestPropDisconnect(uGuestPropSvcClientID); + } +# endif /* VBOX_WITH_GUEST_PROPS */ + VbglR3Term(); + LogFlowFunc(("returning %Rrc\n", rc)); + return rc; +} + +struct VBCLSERVICE vbclHostVersionInterface = +{ + getName, + getPidFilePath, + VBClServiceDefaultHandler, /* init */ + run, + VBClServiceDefaultCleanup +}; + +struct HOSTVERSIONSERVICE +{ + struct VBCLSERVICE *pInterface; +}; + +/* Static factory */ +struct VBCLSERVICE **VBClGetHostVersionService() +{ + struct HOSTVERSIONSERVICE *pService = + (struct HOSTVERSIONSERVICE *)RTMemAlloc(sizeof(*pService)); + + if (!pService) + VBClLogFatalError("Out of memory\n"); + pService->pInterface = &vbclHostVersionInterface; + return &pService->pInterface; +} diff --git a/src/VBox/Additions/x11/VBoxClient/logging.cpp b/src/VBox/Additions/x11/VBoxClient/logging.cpp new file mode 100644 index 00000000..d72c4e56 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/logging.cpp @@ -0,0 +1,269 @@ +/* $Id: logging.cpp $ */ +/** @file + * VirtualBox Guest Additions - X11 Client. + */ + +/* + * Copyright (C) 2006-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + + +#include <stdlib.h> +#include <iprt/buildconfig.h> +#include <iprt/file.h> +#include <iprt/process.h> +#include <iprt/stream.h> +#include <iprt/system.h> +#include <VBox/VBoxGuestLib.h> +#include <package-generated.h> +#include "VBoxClient.h" + +/** Logging parameters. */ +/** @todo Make this configurable later. */ +static PRTLOGGER g_pLoggerRelease = NULL; +static uint32_t g_cHistory = 10; /* Enable log rotation, 10 files. */ +static uint32_t g_uHistoryFileTime = RT_SEC_1DAY; /* Max 1 day per file. */ +static uint64_t g_uHistoryFileSize = 100 * _1M; /* Max 100MB per file. */ + +extern unsigned g_cRespawn; +extern unsigned g_cVerbosity; + +/** + * Notifies the desktop environment with a message. + * + * @param pszMessage Message to notify desktop environment with. + */ +int vbclLogNotify(const char *pszMessage) +{ + AssertPtrReturn(pszMessage, VERR_INVALID_POINTER); + + int rc = VINF_SUCCESS; + + if (g_cRespawn == 0) + { + char *pszCommand = RTStrAPrintf2("notify-send \"VBoxClient: %s\"", pszMessage); + if (pszCommand) + { + int status = system(pszCommand); + + RTStrFree(pszCommand); + + if (WEXITSTATUS(status) != 0) /* Utility or extension not available. */ + { + pszCommand = RTStrAPrintf2("xmessage -buttons OK:0 -center \"VBoxClient: %s\"", + pszMessage); + if (pszCommand) + { + status = system(pszCommand); + if (WEXITSTATUS(status) != 0) /* Utility or extension not available. */ + { + RTPrintf("VBoxClient: %s", pszMessage); + } + + RTStrFree(pszCommand); + } + else + rc = VERR_NO_MEMORY; + } + } + else + rc = VERR_NO_MEMORY; + } + + return rc; +} + +/** + * Logs a fatal error, notifies the desktop environment via a message and + * exits the application immediately. + * + * @param pszFormat Format string to log. + * @param ... Variable arguments for format string. Optional. + */ +void VBClLogFatalError(const char *pszFormat, ...) +{ + va_list args; + va_start(args, pszFormat); + char *psz = NULL; + RTStrAPrintfV(&psz, pszFormat, args); + va_end(args); + + AssertPtr(psz); + LogFlowFunc(("%s", psz)); + LogRel(("%s", psz)); + + vbclLogNotify(psz); + + RTStrFree(psz); +} + +/** + * Logs an error message to the (release) logging instance. + * + * @param pszFormat Format string to log. + */ +void VBClLogError(const char *pszFormat, ...) +{ + va_list args; + va_start(args, pszFormat); + char *psz = NULL; + RTStrAPrintfV(&psz, pszFormat, args); + va_end(args); + + AssertPtr(psz); + LogFlowFunc(("%s", psz)); + LogRel(("%s", psz)); + + RTStrFree(psz); +} + +/** + * Logs an info message to the (release) logging instance. + * + * @param pszFormat Format string to log. + */ +void VBClLogInfo(const char *pszFormat, ...) +{ + va_list args; + va_start(args, pszFormat); + char *psz = NULL; + RTStrAPrintfV(&psz, pszFormat, args); + va_end(args); + + AssertPtr(psz); + LogFlowFunc(("%s", psz)); + LogRel(("%s", psz)); + + RTStrFree(psz); +} + +/** + * @callback_method_impl{FNRTLOGPHASE, Release logger callback} + */ +static DECLCALLBACK(void) vbClLogHeaderFooter(PRTLOGGER pLoggerRelease, RTLOGPHASE enmPhase, PFNRTLOGPHASEMSG pfnLog) +{ + /* Some introductory information. */ + static RTTIMESPEC s_TimeSpec; + char szTmp[256]; + if (enmPhase == RTLOGPHASE_BEGIN) + RTTimeNow(&s_TimeSpec); + RTTimeSpecToString(&s_TimeSpec, szTmp, sizeof(szTmp)); + + switch (enmPhase) + { + case RTLOGPHASE_BEGIN: + { + pfnLog(pLoggerRelease, + "VBoxClient %s r%s (verbosity: %u) %s (%s %s) release log\n" + "Log opened %s\n", + RTBldCfgVersion(), RTBldCfgRevisionStr(), g_cVerbosity, VBOX_BUILD_TARGET, + __DATE__, __TIME__, szTmp); + + int vrc = RTSystemQueryOSInfo(RTSYSOSINFO_PRODUCT, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Product: %s\n", szTmp); + vrc = RTSystemQueryOSInfo(RTSYSOSINFO_RELEASE, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Release: %s\n", szTmp); + vrc = RTSystemQueryOSInfo(RTSYSOSINFO_VERSION, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Version: %s\n", szTmp); + vrc = RTSystemQueryOSInfo(RTSYSOSINFO_SERVICE_PACK, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Service Pack: %s\n", szTmp); + + /* the package type is interesting for Linux distributions */ + char szExecName[RTPATH_MAX]; + char *pszExecName = RTProcGetExecutablePath(szExecName, sizeof(szExecName)); + pfnLog(pLoggerRelease, + "Executable: %s\n" + "Process ID: %u\n" + "Package type: %s" +#ifdef VBOX_OSE + " (OSE)" +#endif + "\n", + pszExecName ? pszExecName : "unknown", + RTProcSelf(), + VBOX_PACKAGE_STRING); + break; + } + + case RTLOGPHASE_PREROTATE: + pfnLog(pLoggerRelease, "Log rotated - Log started %s\n", szTmp); + break; + + case RTLOGPHASE_POSTROTATE: + pfnLog(pLoggerRelease, "Log continuation - Log started %s\n", szTmp); + break; + + case RTLOGPHASE_END: + pfnLog(pLoggerRelease, "End of log file - Log started %s\n", szTmp); + break; + + default: + /* nothing */ + break; + } +} + +/** + * Creates the default release logger outputting to the specified file. + * + * Pass NULL to disabled logging. + * + * @return IPRT status code. + * @param pszLogFile Filename for log output. NULL disables custom handling. + */ +int VBClLogCreate(const char *pszLogFile) +{ + if (!pszLogFile) + return VINF_SUCCESS; + + /* Create release logger (stdout + file). */ + static const char * const s_apszGroups[] = VBOX_LOGGROUP_NAMES; + RTUINT fFlags = RTLOGFLAGS_PREFIX_THREAD | RTLOGFLAGS_PREFIX_TIME; +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + fFlags |= RTLOGFLAGS_USECRLF; +#endif + int rc = RTLogCreateEx(&g_pLoggerRelease, fFlags, "all", +#ifdef DEBUG + "VBOXCLIENT_LOG", +#else + "VBOXCLIENT_RELEASE_LOG", +#endif + RT_ELEMENTS(s_apszGroups), s_apszGroups, UINT32_MAX /*cMaxEntriesPerGroup*/, + RTLOGDEST_STDOUT | RTLOGDEST_USER, + vbClLogHeaderFooter, g_cHistory, g_uHistoryFileSize, g_uHistoryFileTime, + NULL /*pErrInfo*/, "%s", pszLogFile ? pszLogFile : ""); + if (RT_SUCCESS(rc)) + { + /* register this logger as the release logger */ + RTLogRelSetDefaultInstance(g_pLoggerRelease); + + /* Explicitly flush the log in case of VBOXSERVICE_RELEASE_LOG=buffered. */ + RTLogFlush(g_pLoggerRelease); + } + + return rc; +} + +/** + * Destroys the currently active logging instance. + */ +void VBClLogDestroy(void) +{ + RTLogDestroy(RTLogRelSetDefaultInstance(NULL)); +} + +void foo() +{ +} diff --git a/src/VBox/Additions/x11/VBoxClient/main.cpp b/src/VBox/Additions/x11/VBoxClient/main.cpp new file mode 100644 index 00000000..652dc2a1 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/main.cpp @@ -0,0 +1,449 @@ +/* $Id: main.cpp $ */ +/** @file + * VirtualBox Guest Additions - X11 Client. + */ + +/* + * Copyright (C) 2006-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <sys/wait.h> +#include <stdlib.h> /* For exit */ +#include <signal.h> +#include <X11/Xlib.h> +#include "product-generated.h" +#include <iprt/buildconfig.h> +#include <iprt/critsect.h> +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/message.h> +#include <iprt/path.h> +#include <iprt/stream.h> +#include <VBox/VBoxGuestLib.h> +#include <VBox/err.h> +#include "VBoxClient.h" + + +/********************************************************************************************************************************* +* Defines * +*********************************************************************************************************************************/ +#define VBOXCLIENT_OPT_NORESPAWN 950 + +#define VBOXCLIENT_OPT_SERVICES 980 +#define VBOXCLIENT_OPT_CHECKHOSTVERSION VBOXCLIENT_OPT_SERVICES +#define VBOXCLIENT_OPT_CLIPBOARD VBOXCLIENT_OPT_SERVICES + 1 +#define VBOXCLIENT_OPT_DRAGANDDROP VBOXCLIENT_OPT_SERVICES + 2 +#define VBOXCLIENT_OPT_SEAMLESS VBOXCLIENT_OPT_SERVICES + 3 +#define VBOXCLIENT_OPT_VMSVGA VBOXCLIENT_OPT_SERVICES + 4 + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/*static int (*gpfnOldIOErrorHandler)(Display *) = NULL; - unused */ + +/** Object representing the service we are running. This has to be global + * so that the cleanup routine can access it. */ +struct VBCLSERVICE **g_pService; +/** The name of our pidfile. It is global for the benefit of the cleanup + * routine. */ +static char g_szPidFile[RTPATH_MAX] = ""; +/** The file handle of our pidfile. It is global for the benefit of the + * cleanup routine. */ +static RTFILE g_hPidFile; +/** Global critical section held during the clean-up routine (to prevent it + * being called on multiple threads at once) or things which may not happen + * during clean-up (e.g. pausing and resuming the service). + */ +static RTCRITSECT g_critSect; +/** Counter of how often our daemon has been respawned. */ +unsigned g_cRespawn = 0; +/** Logging verbosity level. */ +unsigned g_cVerbosity = 0; +static char g_szLogFile[RTPATH_MAX + 128] = ""; + + +/** + * Clean up if we get a signal or something. + * + * This is extern so that we can call it from other compilation units. + */ +void VBClCleanUp(bool fExit /*=true*/) +{ + /* We never release this, as we end up with a call to exit(3) which is not + * async-safe. Unless we fix this application properly, we should be sure + * never to exit from anywhere except from this method. */ + int rc = RTCritSectEnter(&g_critSect); + if (RT_FAILURE(rc)) + VBClLogFatalError("Failure while acquiring the global critical section, rc=%Rrc\n", rc); + if (g_pService) + (*g_pService)->cleanup(g_pService); + if (g_szPidFile[0] && g_hPidFile) + VbglR3ClosePidFile(g_szPidFile, g_hPidFile); + + VBClLogDestroy(); + + if (fExit) + exit(RTEXITCODE_SUCCESS); +} + +/** + * A standard signal handler which cleans up and exits. + */ +static void vboxClientSignalHandler(int cSignal) +{ + VBClLogInfo("Terminated with signal %d\n", cSignal); + /** Disable seamless mode */ + VBClLogInfo("Terminating ...\n"); + VBClCleanUp(); +} + +/** + * Xlib error handler for certain errors that we can't avoid. + */ +static int vboxClientXLibErrorHandler(Display *pDisplay, XErrorEvent *pError) +{ + char errorText[1024]; + + XGetErrorText(pDisplay, pError->error_code, errorText, sizeof(errorText)); + VBClLogError("An X Window protocol error occurred: %s (error code %d). Request code: %d, minor code: %d, serial number: %d\n", errorText, pError->error_code, pError->request_code, pError->minor_code, pError->serial); + return 0; +} + +/** + * Xlib error handler for fatal errors. This often means that the programme is still running + * when X exits. + */ +static int vboxClientXLibIOErrorHandler(Display *pDisplay) +{ + RT_NOREF1(pDisplay); + VBClLogError("A fatal guest X Window error occurred. This may just mean that the Window system was shut down while the client was still running\n"); + VBClCleanUp(); + return 0; /* We should never reach this. */ +} + +/** + * Reset all standard termination signals to call our signal handler, which + * cleans up and exits. + */ +static void vboxClientSetSignalHandlers(void) +{ + struct sigaction sigAction; + + LogRelFlowFuncEnter(); + sigAction.sa_handler = vboxClientSignalHandler; + sigemptyset(&sigAction.sa_mask); + sigAction.sa_flags = 0; + sigaction(SIGHUP, &sigAction, NULL); + sigaction(SIGINT, &sigAction, NULL); + sigaction(SIGQUIT, &sigAction, NULL); + sigaction(SIGPIPE, &sigAction, NULL); + sigaction(SIGALRM, &sigAction, NULL); + sigaction(SIGTERM, &sigAction, NULL); + sigaction(SIGUSR1, &sigAction, NULL); + sigaction(SIGUSR2, &sigAction, NULL); + LogRelFlowFuncLeave(); +} + +/** + * Print out a usage message and exit with success. + */ +static void vboxClientUsage(const char *pcszFileName) +{ + RTPrintf("Usage: %s " +#ifdef VBOX_WITH_SHARED_CLIPBOARD + "--clipboard|" +#endif +#ifdef VBOX_WITH_DRAG_AND_DROP + "--draganddrop|" +#endif +# ifdef VBOX_WITH_GUEST_PROPS + "--checkhostversion|" +#endif + "--seamless|" + "--vmsvga" + "[-d|--nodaemon]\n", pcszFileName); + RTPrintf("Starts the VirtualBox DRM/X Window System guest services.\n\n"); + RTPrintf("Options:\n"); +#ifdef VBOX_WITH_SHARED_CLIPBOARD + RTPrintf(" --clipboard starts the shared clipboard service\n"); +#endif +#ifdef VBOX_WITH_DRAG_AND_DROP + RTPrintf(" --draganddrop starts the drag and drop service\n"); +#endif +#ifdef VBOX_WITH_GUEST_PROPS + RTPrintf(" --checkhostversion starts the host version notifier service\n"); +#endif + RTPrintf(" --seamless starts the seamless windows service\n"); + RTPrintf(" --vmsvga starts VMSVGA dynamic resizing for x11/Wayland guests\n"); + RTPrintf(" -f, --foreground run in the foreground (no daemonizing)\n"); + RTPrintf(" -d, --nodaemon continues running as a system service\n"); + RTPrintf(" -h, --help shows this help text\n"); + RTPrintf(" -v, --verbose increases logging verbosity level\n"); + RTPrintf(" -V, --version shows version information\n"); + RTPrintf("\n"); +} + +/** + * Complains about seeing more than one service specification. + * + * @returns RTEXITCODE_SYNTAX. + */ +static int vbclSyntaxOnlyOneService(void) +{ + RTMsgError("More than one service specified! Only one, please."); + return RTEXITCODE_SYNTAX; +} + +/** + * The main loop for the VBoxClient daemon. + * @todo Clean up for readability. + */ +int main(int argc, char *argv[]) +{ + /* Initialise our runtime before all else. */ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* This should never be called twice in one process - in fact one Display + * object should probably never be used from multiple threads anyway. */ + if (!XInitThreads()) + VBClLogFatalError("Failed to initialize X11 threads\n"); + + /* Get our file name for usage info and hints. */ + const char *pcszFileName = RTPathFilename(argv[0]); + if (!pcszFileName) + pcszFileName = "VBoxClient"; + + /* Parse our option(s). */ + static const RTGETOPTDEF s_aOptions[] = + { + { "--nodaemon", 'd', RTGETOPT_REQ_NOTHING }, + { "--foreground", 'f', RTGETOPT_REQ_NOTHING }, + { "--help", 'h', RTGETOPT_REQ_NOTHING }, + { "--logfile", 'l', RTGETOPT_REQ_STRING }, + { "--no-respawn", VBOXCLIENT_OPT_NORESPAWN, RTGETOPT_REQ_NOTHING }, + { "--version", 'V', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + + /* Services */ + { "--checkhostversion", VBOXCLIENT_OPT_CHECKHOSTVERSION, RTGETOPT_REQ_NOTHING }, +#ifdef VBOX_WITH_SHARED_CLIPBOARD + { "--clipboard", VBOXCLIENT_OPT_CLIPBOARD, RTGETOPT_REQ_NOTHING }, +#endif +#ifdef VBOX_WITH_DRAG_AND_DROP + { "--draganddrop", VBOXCLIENT_OPT_DRAGANDDROP, RTGETOPT_REQ_NOTHING }, +#endif + { "--seamless", VBOXCLIENT_OPT_SEAMLESS, RTGETOPT_REQ_NOTHING }, + { "--vmsvga", VBOXCLIENT_OPT_VMSVGA, RTGETOPT_REQ_NOTHING }, + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + rc = RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 0, 0 /* fFlags */); + AssertRC(rc); + + bool fDaemonise = true; + bool fRespawn = true; + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + case 'd': + { + fDaemonise = false; + break; + } + + case 'h': + { + vboxClientUsage(pcszFileName); + return RTEXITCODE_SUCCESS; + } + + case 'f': + { + fDaemonise = false; + fRespawn = false; + break; + } + + case 'l': + { + rc = RTStrCopy(g_szLogFile, sizeof(g_szLogFile), ValueUnion.psz); + if (RT_FAILURE(rc)) + VBClLogFatalError("Unable to create log file path, rc=%Rrc\n", rc); + break; + } + + case 'n': + { + fRespawn = false; + break; + } + + case 'v': + { + g_cVerbosity++; + break; + } + + case 'V': + { + RTPrintf("%sr%s\n", RTBldCfgVersion(), RTBldCfgRevisionStr()); + return RTEXITCODE_SUCCESS; + } + + /* Services */ + + case VBOXCLIENT_OPT_CHECKHOSTVERSION: + { + if (g_pService) + return vbclSyntaxOnlyOneService(); + g_pService = VBClGetHostVersionService(); + break; + } + +#ifdef VBOX_WITH_SHARED_CLIPBOARD + case VBOXCLIENT_OPT_CLIPBOARD: + { + if (g_pService) + return vbclSyntaxOnlyOneService(); + g_pService = VBClGetClipboardService(); + break; + } +#endif +#ifdef VBOX_WITH_DRAG_AND_DROP + case VBOXCLIENT_OPT_DRAGANDDROP: + { + if (g_pService) + return vbclSyntaxOnlyOneService(); + g_pService = VBClGetDragAndDropService(); + break; + } +#endif + case VBOXCLIENT_OPT_SEAMLESS: + { + if (g_pService) + return vbclSyntaxOnlyOneService(); + g_pService = VBClGetSeamlessService(); + break; + } + + case VBOXCLIENT_OPT_VMSVGA: + { + if (g_pService) + return vbclSyntaxOnlyOneService(); + g_pService = VBClDisplaySVGAX11Service(); + break; + } + + case VERR_GETOPT_UNKNOWN_OPTION: + { + RTMsgError("unrecognized option '%s'", ValueUnion.psz); + RTMsgInfo("Try '%s --help' for more information", pcszFileName); + return RTEXITCODE_SYNTAX; + } + + case VINF_GETOPT_NOT_OPTION: + default: + break; + + } /* switch */ + } /* while RTGetOpt */ + + if (!g_pService) + { + RTMsgError("No service specified. Quitting because nothing to do!"); + return RTEXITCODE_SYNTAX; + } + + /* Initialize VbglR3 before we do anything else with the logger. */ + rc = VbglR3InitUser(); + if (RT_FAILURE(rc)) + VBClLogFatalError("VbglR3InitUser failed: %Rrc", rc); + + rc = VBClLogCreate(g_szLogFile[0] ? g_szLogFile : NULL); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to create release log '%s', rc=%Rrc\n", + g_szLogFile[0] ? g_szLogFile : "<None>", rc); + + LogRel(("Service: %s\n", (*g_pService)->getName())); + + if (!fDaemonise) + { + /* If the user is running in "no daemon" mode, send critical logging to stdout as well. */ + PRTLOGGER pReleaseLog = RTLogRelGetDefaultInstance(); + if (pReleaseLog) + { + rc = RTLogDestinations(pReleaseLog, "stdout"); + if (RT_FAILURE(rc)) + return RTMsgErrorExitFailure("Failed to redivert error output, rc=%Rrc", rc); + } + } + + rc = RTCritSectInit(&g_critSect); + if (RT_FAILURE(rc)) + VBClLogFatalError("Initialising critical section failed: %Rrc\n", rc); + if ((*g_pService)->getPidFilePath) + { + rc = RTPathUserHome(g_szPidFile, sizeof(g_szPidFile)); + if (RT_FAILURE(rc)) + VBClLogFatalError("Getting home directory for PID file failed: %Rrc\n", rc); + rc = RTPathAppend(g_szPidFile, sizeof(g_szPidFile), + (*g_pService)->getPidFilePath()); + if (RT_FAILURE(rc)) + VBClLogFatalError("Creating PID file path failed: %Rrc\n", rc); + if (fDaemonise) + rc = VbglR3Daemonize(false /* fNoChDir */, false /* fNoClose */, fRespawn, &g_cRespawn); + if (RT_FAILURE(rc)) + VBClLogFatalError("Daemonizing failed: %Rrc\n", rc); + if (g_szPidFile[0]) + rc = VbglR3PidFile(g_szPidFile, &g_hPidFile); + if (rc == VERR_FILE_LOCK_VIOLATION) /* Already running. */ + return RTEXITCODE_SUCCESS; + if (RT_FAILURE(rc)) + VBClLogFatalError("Creating PID file failed: %Rrc\n", rc); + } + /* Set signal handlers to clean up on exit. */ + vboxClientSetSignalHandlers(); +#ifndef VBOXCLIENT_WITHOUT_X11 + /* Set an X11 error handler, so that we don't die when we get unavoidable + * errors. */ + XSetErrorHandler(vboxClientXLibErrorHandler); + /* Set an X11 I/O error handler, so that we can shutdown properly on + * fatal errors. */ + XSetIOErrorHandler(vboxClientXLibIOErrorHandler); +#endif + rc = (*g_pService)->init(g_pService); + if (RT_SUCCESS(rc)) + { + rc = (*g_pService)->run(g_pService, fDaemonise); + if (RT_FAILURE(rc)) + VBClLogError("Running service failed: %Rrc\n", rc); + } + else + { + /** @todo r=andy Should we return an appropriate exit code if the service failed to init? + * Must be tested carefully with our init scripts first. */ + VBClLogError("Initializing service failed: %Rrc\n", rc); + } + VBClCleanUp(false /*fExit*/); + return RTEXITCODE_SUCCESS; +} diff --git a/src/VBox/Additions/x11/VBoxClient/seamless-x11.cpp b/src/VBox/Additions/x11/VBoxClient/seamless-x11.cpp new file mode 100644 index 00000000..290232b1 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/seamless-x11.cpp @@ -0,0 +1,519 @@ +/* $Id: seamless-x11.cpp $ */ +/** @file + * X11 Seamless mode. + */ + +/* + * Copyright (C) 2008-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + + +/********************************************************************************************************************************* +* Header files * +*********************************************************************************************************************************/ + +#include <iprt/errcore.h> +#include <iprt/assert.h> +#include <iprt/vector.h> +#include <VBox/log.h> + +#include "seamless-x11.h" +#include "VBoxClient.h" + +#include <X11/Xatom.h> +#include <X11/Xmu/WinUtil.h> + +#include <limits.h> + +#ifdef TESTCASE +#undef DefaultRootWindow +#define DefaultRootWindow XDefaultRootWindow +#endif + +/***************************************************************************** +* Static functions * +*****************************************************************************/ + +static unsigned char *XXGetProperty (Display *aDpy, Window aWnd, Atom aPropType, + const char *aPropName, unsigned long *nItems) +{ + LogRelFlowFuncEnter(); + Atom propNameAtom = XInternAtom (aDpy, aPropName, + True /* only_if_exists */); + if (propNameAtom == None) + { + return NULL; + } + + Atom actTypeAtom = None; + int actFmt = 0; + unsigned long nBytesAfter = 0; + unsigned char *propVal = 0; + int rc = XGetWindowProperty (aDpy, aWnd, propNameAtom, + 0, LONG_MAX, False /* delete */, + aPropType, &actTypeAtom, &actFmt, + nItems, &nBytesAfter, &propVal); + if (rc != Success) + return NULL; + + LogRelFlowFuncLeave(); + return propVal; +} + +/** + * Initialise the guest and ensure that it is capable of handling seamless mode + * + * @param pHostCallback host callback. + * @returns true if it can handle seamless, false otherwise + */ +int SeamlessX11::init(PFNSENDREGIONUPDATE pHostCallback) +{ + int rc = VINF_SUCCESS; + + LogRelFlowFuncEnter(); + if (mHostCallback != NULL) /* Assertion */ + { + VBClLogError("Attempting to initialise seamless guest object twice!\n"); + return VERR_INTERNAL_ERROR; + } + if (!(mDisplay = XOpenDisplay(NULL))) + { + VBClLogError("Seamless guest object failed to acquire a connection to the display\n"); + return VERR_ACCESS_DENIED; + } + mHostCallback = pHostCallback; + mEnabled = false; + unmonitorClientList(); + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Read information about currently visible windows in the guest and subscribe to X11 + * events about changes to this information. + * + * @note This class does not contain its own event thread, so an external thread must + * call nextConfigurationEvent() for as long as events are wished. + * @todo This function should switch the guest to fullscreen mode. + */ +int SeamlessX11::start(void) +{ + int rc = VINF_SUCCESS; + /** Dummy values for XShapeQueryExtension */ + int error, event; + + LogRelFlowFuncEnter(); + if (mEnabled) + return VINF_SUCCESS; + mSupportsShape = XShapeQueryExtension(mDisplay, &event, &error); + mEnabled = true; + monitorClientList(); + rebuildWindowTree(); + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** Stop reporting seamless events to the host. Free information about guest windows + and stop requesting updates. */ +void SeamlessX11::stop(void) +{ + LogRelFlowFuncEnter(); + if (!mEnabled) + return; + mEnabled = false; + unmonitorClientList(); + freeWindowTree(); + LogRelFlowFuncLeave(); +} + +void SeamlessX11::monitorClientList(void) +{ + LogRelFlowFuncEnter(); + XSelectInput(mDisplay, DefaultRootWindow(mDisplay), PropertyChangeMask | SubstructureNotifyMask); +} + +void SeamlessX11::unmonitorClientList(void) +{ + LogRelFlowFuncEnter(); + XSelectInput(mDisplay, DefaultRootWindow(mDisplay), PropertyChangeMask); +} + +/** + * Recreate the table of toplevel windows of clients on the default root window of the + * X server. + */ +void SeamlessX11::rebuildWindowTree(void) +{ + LogRelFlowFuncEnter(); + freeWindowTree(); + addClients(DefaultRootWindow(mDisplay)); + mChanged = true; +} + + +/** + * Look at the list of children of a virtual root window and add them to the list of clients + * if they belong to a client which is not a virtual root. + * + * @param hRoot the virtual root window to be examined + */ +void SeamlessX11::addClients(const Window hRoot) +{ + /** Unused out parameters of XQueryTree */ + Window hRealRoot, hParent; + /** The list of children of the root supplied, raw pointer */ + Window *phChildrenRaw = NULL; + /** The list of children of the root supplied, auto-pointer */ + Window *phChildren; + /** The number of children of the root supplied */ + unsigned cChildren; + + LogRelFlowFuncEnter(); + if (!XQueryTree(mDisplay, hRoot, &hRealRoot, &hParent, &phChildrenRaw, &cChildren)) + return; + phChildren = phChildrenRaw; + for (unsigned i = 0; i < cChildren; ++i) + addClientWindow(phChildren[i]); + XFree(phChildrenRaw); + LogRelFlowFuncLeave(); +} + + +void SeamlessX11::addClientWindow(const Window hWin) +{ + LogRelFlowFuncEnter(); + XWindowAttributes winAttrib; + bool fAddWin = true; + Window hClient = XmuClientWindow(mDisplay, hWin); + + if (isVirtualRoot(hClient)) + fAddWin = false; + if (fAddWin && !XGetWindowAttributes(mDisplay, hWin, &winAttrib)) + { + VBClLogError("Failed to get the window attributes for window %d\n", hWin); + fAddWin = false; + } + if (fAddWin && (winAttrib.map_state == IsUnmapped)) + fAddWin = false; + XSizeHints dummyHints; + long dummyLong; + /* Apparently (?) some old kwin versions had unwanted client windows + * without normal hints. */ + if (fAddWin && (!XGetWMNormalHints(mDisplay, hClient, &dummyHints, + &dummyLong))) + { + LogRelFlowFunc(("window %lu, client window %lu has no size hints\n", hWin, hClient)); + fAddWin = false; + } + if (fAddWin) + { + XRectangle *pRects = NULL; + int cRects = 0, iOrdering; + bool hasShape = false; + + LogRelFlowFunc(("adding window %lu, client window %lu\n", hWin, + hClient)); + if (mSupportsShape) + { + XShapeSelectInput(mDisplay, hWin, ShapeNotifyMask); + pRects = XShapeGetRectangles(mDisplay, hWin, ShapeBounding, &cRects, &iOrdering); + if (!pRects) + cRects = 0; + else + { + if ( (cRects > 1) + || (pRects[0].x != 0) + || (pRects[0].y != 0) + || (pRects[0].width != winAttrib.width) + || (pRects[0].height != winAttrib.height) + ) + hasShape = true; + } + } + mGuestWindows.addWindow(hWin, hasShape, winAttrib.x, winAttrib.y, + winAttrib.width, winAttrib.height, cRects, + pRects); + } + LogRelFlowFuncLeave(); +} + + +/** + * Checks whether a window is a virtual root. + * @returns true if it is, false otherwise + * @param hWin the window to be examined + */ +bool SeamlessX11::isVirtualRoot(Window hWin) +{ + unsigned char *windowTypeRaw = NULL; + Atom *windowType; + unsigned long ulCount; + bool rc = false; + + LogRelFlowFuncEnter(); + windowTypeRaw = XXGetProperty(mDisplay, hWin, XA_ATOM, WM_TYPE_PROP, &ulCount); + if (windowTypeRaw != NULL) + { + windowType = (Atom *)(windowTypeRaw); + if ( (ulCount != 0) + && (*windowType == XInternAtom(mDisplay, WM_TYPE_DESKTOP_PROP, True))) + rc = true; + } + if (windowTypeRaw) + XFree(windowTypeRaw); + LogRelFlowFunc(("returning %RTbool\n", rc)); + return rc; +} + +DECLCALLBACK(int) VBoxGuestWinFree(VBoxGuestWinInfo *pInfo, void *pvParam) +{ + Display *pDisplay = (Display *)pvParam; + + XShapeSelectInput(pDisplay, pInfo->Core.Key, 0); + delete pInfo; + return VINF_SUCCESS; +} + +/** + * Free all information in the tree of visible windows + */ +void SeamlessX11::freeWindowTree(void) +{ + /* We use post-increment in the operation to prevent the iterator from being invalidated. */ + LogRelFlowFuncEnter(); + mGuestWindows.detachAll(VBoxGuestWinFree, mDisplay); + LogRelFlowFuncLeave(); +} + + +/** + * Waits for a position or shape-related event from guest windows + * + * @note Called from the guest event thread. + */ +void SeamlessX11::nextConfigurationEvent(void) +{ + XEvent event; + + LogRelFlowFuncEnter(); + /* Start by sending information about the current window setup to the host. We do this + here because we want to send all such information from a single thread. */ + if (mChanged && mEnabled) + { + updateRects(); + mHostCallback(mpRects, mcRects); + } + mChanged = false; + /* We execute this even when seamless is disabled, as it also waits for + * enable and disable notification. */ + XNextEvent(mDisplay, &event); + if (!mEnabled) + return; + switch (event.type) + { + case ConfigureNotify: + { + XConfigureEvent *pConf = &event.xconfigure; + LogRelFlowFunc(("configure event, window=%lu, x=%i, y=%i, w=%i, h=%i, send_event=%RTbool\n", + (unsigned long) pConf->window, (int) pConf->x, + (int) pConf->y, (int) pConf->width, + (int) pConf->height, pConf->send_event)); + } + doConfigureEvent(event.xconfigure.window); + break; + case MapNotify: + LogRelFlowFunc(("map event, window=%lu, send_event=%RTbool\n", + (unsigned long) event.xmap.window, + event.xmap.send_event)); + rebuildWindowTree(); + break; + case PropertyNotify: + if ( event.xproperty.atom != XInternAtom(mDisplay, "_NET_CLIENT_LIST", True /* only_if_exists */) + || event.xproperty.window != DefaultRootWindow(mDisplay)) + break; + LogRelFlowFunc(("_NET_CLIENT_LIST property event on root window\n")); + rebuildWindowTree(); + break; + case VBoxShapeNotify: /* This is defined wrong in my X11 header files! */ + LogRelFlowFunc(("shape event, window=%lu, send_event=%RTbool\n", + (unsigned long) event.xany.window, + event.xany.send_event)); + /* the window member in xany is in the same place as in the shape event */ + doShapeEvent(event.xany.window); + break; + case UnmapNotify: + LogRelFlowFunc(("unmap event, window=%lu, send_event=%RTbool\n", + (unsigned long) event.xunmap.window, + event.xunmap.send_event)); + rebuildWindowTree(); + break; + default: + break; + } + LogRelFlowFunc(("processed event\n")); +} + +/** + * Handle a configuration event in the seamless event thread by setting the new position. + * + * @param hWin the window to be examined + */ +void SeamlessX11::doConfigureEvent(Window hWin) +{ + VBoxGuestWinInfo *pInfo = mGuestWindows.find(hWin); + if (pInfo) + { + XWindowAttributes winAttrib; + + if (!XGetWindowAttributes(mDisplay, hWin, &winAttrib)) + return; + pInfo->mX = winAttrib.x; + pInfo->mY = winAttrib.y; + pInfo->mWidth = winAttrib.width; + pInfo->mHeight = winAttrib.height; + mChanged = true; + } +} + +/** + * Handle a window shape change event in the seamless event thread. + * + * @param hWin the window to be examined + */ +void SeamlessX11::doShapeEvent(Window hWin) +{ + LogRelFlowFuncEnter(); + VBoxGuestWinInfo *pInfo = mGuestWindows.find(hWin); + if (pInfo) + { + XRectangle *pRects; + int cRects = 0, iOrdering; + + pRects = XShapeGetRectangles(mDisplay, hWin, ShapeBounding, &cRects, + &iOrdering); + if (!pRects) + cRects = 0; + pInfo->mhasShape = true; + if (pInfo->mpRects) + XFree(pInfo->mpRects); + pInfo->mcRects = cRects; + pInfo->mpRects = pRects; + mChanged = true; + } + LogRelFlowFuncLeave(); +} + +/** + * Gets the list of visible rectangles + */ +RTRECT *SeamlessX11::getRects(void) +{ + return mpRects; +} + +/** + * Gets the number of rectangles in the visible rectangle list + */ +size_t SeamlessX11::getRectCount(void) +{ + return mcRects; +} + +RTVEC_DECL(RectList, RTRECT) + +DECLCALLBACK(int) getRectsCallback(VBoxGuestWinInfo *pInfo, + struct RectList *pRects) +{ + if (pInfo->mhasShape) + { + for (int i = 0; i < pInfo->mcRects; ++i) + { + RTRECT *pRect; + + pRect = RectListPushBack(pRects); + if (!pRect) + return VERR_NO_MEMORY; + pRect->xLeft = pInfo->mX + + pInfo->mpRects[i].x; + pRect->yBottom = pInfo->mY + + pInfo->mpRects[i].y + + pInfo->mpRects[i].height; + pRect->xRight = pInfo->mX + + pInfo->mpRects[i].x + + pInfo->mpRects[i].width; + pRect->yTop = pInfo->mY + + pInfo->mpRects[i].y; + } + } + else + { + RTRECT *pRect; + + pRect = RectListPushBack(pRects); + if (!pRect) + return VERR_NO_MEMORY; + pRect->xLeft = pInfo->mX; + pRect->yBottom = pInfo->mY + + pInfo->mHeight; + pRect->xRight = pInfo->mX + + pInfo->mWidth; + pRect->yTop = pInfo->mY; + } + return VINF_SUCCESS; +} + +/** + * Updates the list of seamless rectangles + */ +int SeamlessX11::updateRects(void) +{ + LogRelFlowFuncEnter(); + struct RectList rects = RTVEC_INITIALIZER; + + if (mcRects != 0) + { + int rc = RectListReserve(&rects, mcRects * 2); + if (RT_FAILURE(rc)) + return rc; + } + mGuestWindows.doWithAll((PVBOXGUESTWINCALLBACK)getRectsCallback, + &rects); + if (mpRects) + RTMemFree(mpRects); + mcRects = RectListSize(&rects); + mpRects = RectListDetach(&rects); + LogRelFlowFuncLeave(); + return VINF_SUCCESS; +} + +/** + * Send a client event to wake up the X11 seamless event loop prior to stopping it. + * + * @note This function should only be called from the host event thread. + */ +bool SeamlessX11::interruptEventWait(void) +{ + bool rc = false; + Display *pDisplay = XOpenDisplay(NULL); + + LogRelFlowFuncEnter(); + if (pDisplay == NULL) + VBClLogFatalError("Failed to open X11 display\n"); + /* Message contents set to zero. */ + XClientMessageEvent clientMessage = { ClientMessage, 0, 0, 0, 0, 0, 8 }; + + if (XSendEvent(pDisplay, DefaultRootWindow(mDisplay), false, + PropertyChangeMask, (XEvent *)&clientMessage)) + rc = true; + XCloseDisplay(pDisplay); + LogRelFlowFunc(("returning %RTbool\n", rc)); + return rc; +} diff --git a/src/VBox/Additions/x11/VBoxClient/seamless-x11.h b/src/VBox/Additions/x11/VBoxClient/seamless-x11.h new file mode 100644 index 00000000..de876c5e --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/seamless-x11.h @@ -0,0 +1,274 @@ +/* $Id: seamless-x11.h $ */ +/** @file + * Seamless mode - X11 guests. + */ + +/* + * Copyright (C) 2006-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +#ifndef GA_INCLUDED_SRC_x11_VBoxClient_seamless_x11_h +#define GA_INCLUDED_SRC_x11_VBoxClient_seamless_x11_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <VBox/log.h> +#include <iprt/avl.h> +#ifdef RT_NEED_NEW_AND_DELETE +# include <iprt/mem.h> +# include <new> +#endif + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/extensions/shape.h> + +#define WM_TYPE_PROP "_NET_WM_WINDOW_TYPE" +#define WM_TYPE_DESKTOP_PROP "_NET_WM_WINDOW_TYPE_DESKTOP" + +/* This is defined wrong in my X11 header files! */ +#define VBoxShapeNotify 64 + +/** + * Callback which provides the interface for notifying the host of changes to + * the X11 window configuration, mainly split out from @a VBoxGuestSeamlessHost + * to simplify the unit test. + */ +typedef void FNSENDREGIONUPDATE(RTRECT *pRects, size_t cRects); +typedef FNSENDREGIONUPDATE *PFNSENDREGIONUPDATE; + +/** Structure containing information about a guest window's position and visible area. + Used inside of VBoxGuestWindowList. */ +struct VBoxGuestWinInfo { +public: + /** Header structure for insertion into an AVL tree */ + AVLU32NODECORE Core; + /** Is the window currently mapped? */ + bool mhasShape; + /** Co-ordinates in the guest screen. */ + int mX, mY; + /** Window dimensions. */ + int mWidth, mHeight; + /** Number of rectangles used to represent the visible area. */ + int mcRects; + /** Rectangles representing the visible area. These must be allocated + * by XMalloc and will be freed automatically if non-null when the class + * is destroyed. */ + XRectangle *mpRects; + /** Constructor. */ + VBoxGuestWinInfo(bool hasShape, int x, int y, int w, int h, int cRects, + XRectangle *pRects) + : mhasShape(hasShape), mX(x), mY(y), mWidth(w), mHeight(h), + mcRects(cRects), mpRects(pRects) {} + + /** Destructor */ + ~VBoxGuestWinInfo() + { + if (mpRects) + XFree(mpRects); + } +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif + +private: + // We don't want a copy constructor or assignment operator + VBoxGuestWinInfo(const VBoxGuestWinInfo&); + VBoxGuestWinInfo& operator=(const VBoxGuestWinInfo&); +}; + +/** Callback type used for "DoWithAll" calls */ +typedef DECLCALLBACK(int) VBOXGUESTWINCALLBACK(VBoxGuestWinInfo *, void *); +/** Pointer to VBOXGUESTWINCALLBACK */ +typedef VBOXGUESTWINCALLBACK *PVBOXGUESTWINCALLBACK; + +DECLCALLBACK(int) inline VBoxGuestWinCleanup(VBoxGuestWinInfo *pInfo, void *) +{ + delete pInfo; + return VINF_SUCCESS; +} + +/** + * This class is just a wrapper around a map of structures containing + * information about the windows on the guest system. It has a function for + * adding a structure (see addWindow) and one for removing it by window + * handle (see removeWindow). + */ +class VBoxGuestWindowList +{ +private: + // We don't want a copy constructor or an assignment operator + VBoxGuestWindowList(const VBoxGuestWindowList&); + VBoxGuestWindowList& operator=(const VBoxGuestWindowList&); + + // Private class members + AVLU32TREE mWindows; + +public: + // Constructor + VBoxGuestWindowList(void) : mWindows(NULL) {} + // Destructor + ~VBoxGuestWindowList() + { + /** @todo having this inside the container class hard codes that the + * elements have to be allocated with the "new" operator, and + * I don't see a need to require this. */ + doWithAll(VBoxGuestWinCleanup, NULL); + } + +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif + + // Standard operations + VBoxGuestWinInfo *find(Window hWin) + { + return (VBoxGuestWinInfo *)RTAvlU32Get(&mWindows, hWin); + } + + void detachAll(PVBOXGUESTWINCALLBACK pCallback, void *pvParam) + { + RTAvlU32Destroy(&mWindows, (PAVLU32CALLBACK)pCallback, pvParam); + } + + int doWithAll(PVBOXGUESTWINCALLBACK pCallback, void *pvParam) + { + return RTAvlU32DoWithAll(&mWindows, 1, (PAVLU32CALLBACK)pCallback, + pvParam); + } + + bool addWindow(Window hWin, bool isMapped, int x, int y, int w, int h, int cRects, + XRectangle *pRects) + { + LogRelFlowFunc(("hWin=%lu, isMapped=%RTbool, x=%d, y=%d, w=%d, h=%d, cRects=%d\n", + (unsigned long) hWin, isMapped, x, y, w, h, cRects)); + VBoxGuestWinInfo *pInfo = new VBoxGuestWinInfo(isMapped, x, y, w, h, cRects, + pRects); + pInfo->Core.Key = hWin; + LogRelFlowFuncLeave(); + return RTAvlU32Insert(&mWindows, &pInfo->Core); + } + + VBoxGuestWinInfo *removeWindow(Window hWin) + { + LogRelFlowFuncEnter(); + return (VBoxGuestWinInfo *)RTAvlU32Remove(&mWindows, hWin); + } +}; + +class SeamlessX11 +{ +private: + // We don't want a copy constructor or assignment operator + SeamlessX11(const SeamlessX11&); + SeamlessX11& operator=(const SeamlessX11&); + + // Private member variables + /** Pointer to the host callback. */ + PFNSENDREGIONUPDATE mHostCallback; + /** Our connection to the X11 display we are running on. */ + Display *mDisplay; + /** Class to keep track of visible guest windows. */ + VBoxGuestWindowList mGuestWindows; + /** The current set of seamless rectangles. */ + RTRECT *mpRects; + /** The current number of seamless rectangles. */ + int mcRects; + /** Do we support the X shaped window extension? */ + bool mSupportsShape; + /** Is seamless mode currently enabled? */ + bool mEnabled; + /** Have there been changes since the last time we sent a notification? */ + bool mChanged; + + // Private methods + + // Methods to manage guest window information + /** + * Store information about a desktop window and register for structure events on it. + * If it is mapped, go through the list of it's children and add information about + * mapped children to the tree of visible windows, making sure that those windows are + * not already in our list of desktop windows. + * + * @param hWin the window concerned - should be a "desktop" window + */ + void monitorClientList(void); + void unmonitorClientList(void); + void rebuildWindowTree(void); + void addClients(const Window hRoot); + bool isVirtualRoot(Window hWin); + void addClientWindow(Window hWin); + void freeWindowTree(void); + void updateHostSeamlessInfo(void); + int updateRects(void); + +public: + /** + * Initialise the guest and ensure that it is capable of handling seamless mode + * @param pHostCallback Host interface callback to notify of window configuration + * changes. + * + * @returns iprt status code + */ + int init(PFNSENDREGIONUPDATE pHostCallback); + + /** + * Shutdown seamless event monitoring. + */ + void uninit(void) + { + if (mHostCallback) + stop(); + mHostCallback = NULL; + if (mDisplay) + XCloseDisplay(mDisplay); + mDisplay = NULL; + } + + /** + * Initialise seamless event reporting in the guest. + * + * @returns IPRT status code + */ + int start(void); + /** Stop reporting seamless events. */ + void stop(void); + /** Get the current list of visible rectangles. */ + RTRECT *getRects(void); + /** Get the number of visible rectangles in the current list */ + size_t getRectCount(void); + + /** Process next event in the guest event queue - called by the event thread. */ + void nextConfigurationEvent(void); + /** Wake up the event thread if it is waiting for an event so that it can exit. */ + bool interruptEventWait(void); + + /* Methods to handle X11 events. These are public so that the unit test + * can call them. */ + void doConfigureEvent(Window hWin); + void doShapeEvent(Window hWin); + + SeamlessX11(void) + : mHostCallback(NULL), mDisplay(NULL), mpRects(NULL), mcRects(0), + mSupportsShape(false), mEnabled(false), mChanged(false) {} + + ~SeamlessX11() + { + uninit(); + } + +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif +}; + +#endif /* !GA_INCLUDED_SRC_x11_VBoxClient_seamless_x11_h */ diff --git a/src/VBox/Additions/x11/VBoxClient/seamless.cpp b/src/VBox/Additions/x11/VBoxClient/seamless.cpp new file mode 100644 index 00000000..67338d24 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/seamless.cpp @@ -0,0 +1,344 @@ +/* $Id: seamless.cpp $ */ +/** @file + * X11 Guest client - seamless mode: main logic, communication with the host and + * wrapper interface for the main code of the VBoxClient deamon. The + * X11-specific parts are split out into their own file for ease of testing. + */ + +/* + * Copyright (C) 2006-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + + +/********************************************************************************************************************************* +* Header files * +*********************************************************************************************************************************/ +#include <X11/Xlib.h> + +#include <VBox/log.h> +#include <VBox/VBoxGuestLib.h> +#include <iprt/errcore.h> +#include <iprt/mem.h> + +#include "VBoxClient.h" +#include "seamless.h" + +#include <new> + +SeamlessMain::SeamlessMain(void) +{ + LogRelFlowFuncEnter(); + mX11MonitorThread = NIL_RTTHREAD; + mX11MonitorThreadStopping = false; + mMode = VMMDev_Seamless_Disabled; + mfPaused = true; +} + +SeamlessMain::~SeamlessMain() +{ + LogRelFlowFuncEnter(); + stop(); +} + +/** + * Update the set of visible rectangles in the host. + */ +static void sendRegionUpdate(RTRECT *pRects, size_t cRects) +{ + LogRelFlowFuncEnter(); + if (cRects && !pRects) /* Assertion */ + { + VBClLogError(("Region update called with NULL pointer!\n")); + return; + } + VbglR3SeamlessSendRects(cRects, pRects); + LogRelFlowFuncLeave(); +} + +/** + * initialise the service. + */ +int SeamlessMain::init(void) +{ + int rc; + const char *pcszStage; + + LogRelFlowFuncEnter(); + do { + pcszStage = "Connecting to the X server"; + rc = mX11Monitor.init(sendRegionUpdate); + if (RT_FAILURE(rc)) + break; + pcszStage = "Setting guest IRQ filter mask"; + rc = VbglR3CtlFilterMask(VMMDEV_EVENT_SEAMLESS_MODE_CHANGE_REQUEST, 0); + if (RT_FAILURE(rc)) + break; + pcszStage = "Reporting support for seamless capability"; + rc = VbglR3SeamlessSetCap(true); + if (RT_FAILURE(rc)) + break; + rc = startX11MonitorThread(); + if (RT_FAILURE(rc)) + break; + } while(0); + if (RT_FAILURE(rc)) + VBClLogError("Failed to start in stage '%s' -- error: %Rrc\n", pcszStage, rc); + return rc; +} + +/** + * Run the main service thread which listens for host state change + * notifications. + * @returns iprt status value. Service will be set to the stopped state on + * failure. + */ +int SeamlessMain::run(void) +{ + int rc = VINF_SUCCESS; + + LogRelFlowFuncEnter(); + /* This will only exit if something goes wrong. */ + while (RT_SUCCESS(rc) || rc == VERR_INTERRUPTED) + { + if (RT_FAILURE(rc)) + /* If we are not stopping, sleep for a bit to avoid using up too + much CPU while retrying. */ + RTThreadYield(); + rc = nextStateChangeEvent(); + } + if (RT_FAILURE(rc)) + { + VBClLogError("Event loop failed with error: %Rrc\n", rc); + stop(); + } + return rc; +} + +/** Stops the service. */ +void SeamlessMain::stop() +{ + LogRelFlowFuncEnter(); + VbglR3SeamlessSetCap(false); + VbglR3CtlFilterMask(0, VMMDEV_EVENT_SEAMLESS_MODE_CHANGE_REQUEST); + stopX11MonitorThread(); + mX11Monitor.uninit(); + LogRelFlowFuncLeave(); +} + +/** + * Waits for a seamless state change events from the host and dispatch it. + * + * @returns IRPT return code. + */ +int SeamlessMain::nextStateChangeEvent(void) +{ + VMMDevSeamlessMode newMode = VMMDev_Seamless_Disabled; + + LogRelFlowFuncEnter(); + int rc = VbglR3SeamlessWaitEvent(&newMode); + if (RT_SUCCESS(rc)) + { + mMode = newMode; + switch (newMode) + { + case VMMDev_Seamless_Visible_Region: + /* A simplified seamless mode, obtained by making the host VM window + * borderless and making the guest desktop transparent. */ + LogRelFlowFunc(("\"Visible region\" mode requested (VBoxClient)\n")); + break; + case VMMDev_Seamless_Disabled: + LogRelFlowFunc(("\"Disabled\" mode requested (VBoxClient)\n")); + break; + case VMMDev_Seamless_Host_Window: + /* One host window represents one guest window. Not yet implemented. */ + LogRelFunc(("Unsupported \"host window\" mode requested (VBoxClient)\n")); + return VERR_NOT_SUPPORTED; + default: + LogRelFunc(("Unsupported mode %d requested (VBoxClient)\n", + newMode)); + return VERR_NOT_SUPPORTED; + } + } + if (RT_SUCCESS(rc) || rc == VERR_TRY_AGAIN) + { + if (mMode == VMMDev_Seamless_Visible_Region) + mfPaused = false; + else + mfPaused = true; + mX11Monitor.interruptEventWait(); + } + else + { + LogRelFunc(("VbglR3SeamlessWaitEvent returned %Rrc (VBoxClient)\n", rc)); + } + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * The actual X11 window configuration change monitor thread function. + */ +int SeamlessMain::x11MonitorThread(RTTHREAD hThreadSelf, void *pvUser) +{ + RT_NOREF1(hThreadSelf); + SeamlessMain *pHost = (SeamlessMain *)pvUser; + int rc = VINF_SUCCESS; + + LogRelFlowFuncEnter(); + while (!pHost->mX11MonitorThreadStopping) + { + if (!pHost->mfPaused) + { + rc = pHost->mX11Monitor.start(); + if (RT_FAILURE(rc)) + VBClLogFatalError("Failed to change the X11 seamless service state, mfPaused=%RTbool, rc=%Rrc\n", + pHost->mfPaused, rc); + } + pHost->mX11Monitor.nextConfigurationEvent(); + if (pHost->mfPaused || pHost->mX11MonitorThreadStopping) + pHost->mX11Monitor.stop(); + } + LogRelFlowFuncLeaveRC(rc); + return rc; +} + +/** + * Start the X11 window configuration change monitor thread. + */ +int SeamlessMain::startX11MonitorThread(void) +{ + int rc; + + mX11MonitorThreadStopping = false; + if (isX11MonitorThreadRunning()) + return VINF_SUCCESS; + rc = RTThreadCreate(&mX11MonitorThread, x11MonitorThread, this, 0, + RTTHREADTYPE_MSG_PUMP, RTTHREADFLAGS_WAITABLE, + "X11 events"); + if (RT_FAILURE(rc)) + LogRelFunc(("Warning: failed to start X11 monitor thread (VBoxClient)\n")); + return rc; +} + +/** + * Send a signal to the thread function that it should exit + */ +int SeamlessMain::stopX11MonitorThread(void) +{ + int rc; + + mX11MonitorThreadStopping = true; + if (!isX11MonitorThreadRunning()) + return VINF_SUCCESS; + mX11Monitor.interruptEventWait(); + rc = RTThreadWait(mX11MonitorThread, RT_INDEFINITE_WAIT, NULL); + if (RT_SUCCESS(rc)) + mX11MonitorThread = NIL_RTTHREAD; + else + LogRelThisFunc(("Failed to stop X11 monitor thread, rc=%Rrc!\n", + rc)); + return rc; +} + +/** Service magic number, start of a UUID. */ +#define SEAMLESSSERVICE_MAGIC 0xd28ba727 + +/** VBoxClient service class wrapping the logic for the seamless service while + * the main VBoxClient code provides the daemon logic needed by all services. + */ +struct SEAMLESSSERVICE +{ + /** The service interface. */ + struct VBCLSERVICE *pInterface; + /** Magic number for sanity checks. */ + uint32_t magic; + /** Seamless service object. */ + SeamlessMain mSeamless; + /** Are we initialised yet? */ + bool mIsInitialised; +}; + +static const char *getName() +{ + return "Seamless"; +} + +static const char *getPidFilePath(void) +{ + return ".vboxclient-seamless.pid"; +} + +static struct SEAMLESSSERVICE *getClassFromInterface(struct VBCLSERVICE ** + ppInterface) +{ + struct SEAMLESSSERVICE *pSelf = (struct SEAMLESSSERVICE *)ppInterface; + if (pSelf->magic != SEAMLESSSERVICE_MAGIC) + VBClLogFatalError("Bad seamless service object!\n"); + return pSelf; +} + +static int init(struct VBCLSERVICE **ppInterface) +{ + struct SEAMLESSSERVICE *pSelf = getClassFromInterface(ppInterface); + + if (pSelf->mIsInitialised) + return VERR_INTERNAL_ERROR; + + int rc = pSelf->mSeamless.init(); + if (RT_FAILURE(rc)) + return rc; + pSelf->mIsInitialised = true; + return VINF_SUCCESS; +} + +static int run(struct VBCLSERVICE **ppInterface, bool fDaemonised) +{ + RT_NOREF1(fDaemonised); + struct SEAMLESSSERVICE *pSelf = getClassFromInterface(ppInterface); + int rc; + + if (!pSelf->mIsInitialised) + return VERR_INTERNAL_ERROR; + /* This only exits on error. */ + rc = pSelf->mSeamless.run(); + pSelf->mIsInitialised = false; + return rc; +} + +static void cleanup(struct VBCLSERVICE **ppInterface) +{ + RT_NOREF(ppInterface); + VbglR3SeamlessSetCap(false); + VbglR3Term(); +} + +struct VBCLSERVICE vbclSeamlessInterface = +{ + getName, + getPidFilePath, + init, + run, + cleanup +}; + +struct VBCLSERVICE **VBClGetSeamlessService() +{ + struct SEAMLESSSERVICE *pService = + (struct SEAMLESSSERVICE *)RTMemAlloc(sizeof(*pService)); + + if (!pService) + VBClLogFatalError("Out of memory\n"); + pService->pInterface = &vbclSeamlessInterface; + pService->magic = SEAMLESSSERVICE_MAGIC; + new(&pService->mSeamless) SeamlessMain(); + pService->mIsInitialised = false; + return &pService->pInterface; +} diff --git a/src/VBox/Additions/x11/VBoxClient/seamless.h b/src/VBox/Additions/x11/VBoxClient/seamless.h new file mode 100644 index 00000000..3ef159d0 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/seamless.h @@ -0,0 +1,116 @@ +/* $Id: seamless.h $ */ +/** @file + * X11 Guest client - seamless mode, missing proper description while using the + * potentially confusing word 'host'. + */ + +/* + * Copyright (C) 2006-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +#ifndef GA_INCLUDED_SRC_x11_VBoxClient_seamless_h +#define GA_INCLUDED_SRC_x11_VBoxClient_seamless_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/thread.h> + +#include <VBox/log.h> +#include <VBox/VBoxGuestLib.h> /* for the R3 guest library functions */ + +#include "seamless-x11.h" + +/** + * Interface to the host + */ +class SeamlessMain +{ +private: + // We don't want a copy constructor or assignment operator + SeamlessMain(const SeamlessMain&); + SeamlessMain& operator=(const SeamlessMain&); + + /** X11 event monitor object */ + SeamlessX11 mX11Monitor; + + /** Thread to start and stop when we enter and leave seamless mode which + * monitors X11 windows in the guest. */ + RTTHREAD mX11MonitorThread; + /** Should the X11 monitor thread be stopping? */ + volatile bool mX11MonitorThreadStopping; + + /** The current seamless mode we are in. */ + VMMDevSeamlessMode mMode; + /** Is the service currently paused? */ + volatile bool mfPaused; + + /** + * Waits for a seamless state change events from the host and dispatch it. This is + * meant to be called by the host event monitor thread exclusively. + * + * @returns IRPT return code. + */ + int nextStateChangeEvent(void); + + /** Thread function to monitor X11 window configuration changes. */ + static DECLCALLBACK(int) x11MonitorThread(RTTHREAD self, void *pvUser); + + /** Helper to start the X11 monitor thread. */ + int startX11MonitorThread(void); + + /** Helper to stop the X11 monitor thread again. */ + int stopX11MonitorThread(void); + + /** Is the service currently actively monitoring X11 windows? */ + bool isX11MonitorThreadRunning() + { + return mX11MonitorThread != NIL_RTTHREAD; + } + +public: + SeamlessMain(void); + virtual ~SeamlessMain(); +#ifdef RT_NEED_NEW_AND_DELETE + RTMEM_IMPLEMENT_NEW_AND_DELETE(); +#endif + + /** + * Initialise the service. + */ + int init(void); + + /** + * Run the service. + * @returns iprt status value + */ + int run(void); + + /** + * Stops the service. + */ + void stop(); + + /** Pause the service loop. This must be safe to call on a different thread + * and potentially before @a run is or after it exits. + * This is called by the VT monitoring thread to allow the service to disable + * itself when the X server is switched out. If the monitoring functionality + * is available then @a pause or @a resume will be called as soon as it starts + * up. */ + int pause(); + /** Resume after pausing. The same applies here as for @a pause. */ + int resume(); + + /** Run a few tests to be sure everything is working as intended. */ + int selfTest(); +}; + +#endif /* !GA_INCLUDED_SRC_x11_VBoxClient_seamless_h */ diff --git a/src/VBox/Additions/x11/VBoxClient/testcase/tstSeamlessX11-auto.cpp b/src/VBox/Additions/x11/VBoxClient/testcase/tstSeamlessX11-auto.cpp new file mode 100644 index 00000000..3a0499c4 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/testcase/tstSeamlessX11-auto.cpp @@ -0,0 +1,763 @@ +/* $Id: tstSeamlessX11-auto.cpp $ */ +/** @file + * Automated test of the X11 seamless Additions code. + * @todo Better separate test data from implementation details! + */ + +/* + * Copyright (C) 2007-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +#include <stdlib.h> /* exit() */ + +#include <X11/Xatom.h> +#include <X11/Xmu/WinUtil.h> + +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/path.h> +#include <iprt/semaphore.h> +#include <iprt/stream.h> +#include <iprt/string.h> + +#include "../seamless.h" + +#undef DefaultRootWindow + +/****************************************************** +* Mock X11 functions needed by the seamless X11 class * +******************************************************/ + +int XFree(void *data) +{ + RTMemFree(data); + return 0; +} + +#define TEST_DISPLAY ((Display *)0xffff) +#define TEST_ROOT ((Window)1) + +void VBClLogError(const char *pszFormat, ...) +{ + va_list args; + va_start(args, pszFormat); + char *psz = NULL; + RTStrAPrintfV(&psz, pszFormat, args); + va_end(args); + + AssertPtr(psz); + RTPrintf("Error: %s", psz); + + RTStrFree(psz); +} + +/** Exit with a fatal error. */ +void VBClLogFatalError(const char *pszFormat, ...) +{ + va_list args; + va_start(args, pszFormat); + char *psz = NULL; + RTStrAPrintfV(&psz, pszFormat, args); + va_end(args); + + AssertPtr(psz); + RTPrintf("Fatal error: %s", psz); + + RTStrFree(psz); + + exit(1); +} + +extern "C" Display *XOpenDisplay(const char *display_name); +Display *XOpenDisplay(const char *display_name) +{ + RT_NOREF1(display_name); + return TEST_DISPLAY; +} + +extern "C" int XCloseDisplay(Display *display); +int XCloseDisplay(Display *display) +{ + RT_NOREF1(display); + Assert(display == TEST_DISPLAY); + return 0; +} + +enum +{ + ATOM_PROP = 1, + ATOM_DESKTOP_PROP +}; + +extern "C" Atom XInternAtom(Display *display, const char *atom_name, Bool only_if_exists); +Atom XInternAtom(Display *display, const char *atom_name, Bool only_if_exists) +{ + RT_NOREF2(only_if_exists, display); + Assert(display == TEST_DISPLAY); + if (!RTStrCmp(atom_name, WM_TYPE_PROP)) + return (Atom) ATOM_PROP; + if (!RTStrCmp(atom_name, WM_TYPE_DESKTOP_PROP)) + return (Atom) ATOM_DESKTOP_PROP; + AssertFailed(); + return (Atom)0; +} + +/** The window (if any) on which the WM_TYPE_PROP property is set to the + * WM_TYPE_DESKTOP_PROP atom. */ +static Window g_hSmlsDesktopWindow = 0; + +extern "C" int XGetWindowProperty(Display *display, Window w, Atom property, + long long_offset, long long_length, + Bool delProp, Atom req_type, + Atom *actual_type_return, + int *actual_format_return, + unsigned long *nitems_return, + unsigned long *bytes_after_return, + unsigned char **prop_return); +int XGetWindowProperty(Display *display, Window w, Atom property, + long long_offset, long long_length, Bool delProp, + Atom req_type, Atom *actual_type_return, + int *actual_format_return, + unsigned long *nitems_return, + unsigned long *bytes_after_return, + unsigned char **prop_return) +{ + RT_NOREF2(display, long_length); + Assert(display == TEST_DISPLAY); + Atom atomType = XInternAtom (display, WM_TYPE_PROP, true); + Atom atomTypeDesktop = XInternAtom (display, WM_TYPE_DESKTOP_PROP, true); + /* We only handle things we expect. */ + AssertReturn((req_type == XA_ATOM) || (req_type == AnyPropertyType), + 0xffff); + AssertReturn(property == atomType, 0xffff); + *actual_type_return = XA_ATOM; + *actual_format_return = sizeof(Atom) * 8; + *nitems_return = 0; + *bytes_after_return = sizeof(Atom); + *prop_return = NULL; + if ((w != g_hSmlsDesktopWindow) || (g_hSmlsDesktopWindow == 0)) + return Success; + AssertReturn(long_offset == 0, 0); + AssertReturn(delProp == false, 0); + unsigned char *pProp; + pProp = (unsigned char *)RTMemDup(&atomTypeDesktop, + sizeof(atomTypeDesktop)); + AssertReturn(pProp, 0xffff); + *nitems_return = 1; + *prop_return = pProp; + *bytes_after_return = 0; + return 0; +} + +#if 0 /* unused */ +/** Sets the current set of properties for all mock X11 windows */ +static void smlsSetDesktopWindow(Window hWin) +{ + g_hSmlsDesktopWindow = hWin; +} +#endif + +extern "C" Bool XShapeQueryExtension(Display *dpy, int *event_basep, int *error_basep); +Bool XShapeQueryExtension(Display *dpy, int *event_basep, int *error_basep) +{ + RT_NOREF3(dpy, event_basep, error_basep); + Assert(dpy == TEST_DISPLAY); + return true; +} + +/* We silently ignore this for now. */ +extern "C" int XSelectInput(Display *display, Window w, long event_mask); +int XSelectInput(Display *display, Window w, long event_mask) +{ + RT_NOREF3(display, w, event_mask); + Assert(display == TEST_DISPLAY); + return 0; +} + +/* We silently ignore this for now. */ +extern "C" void XShapeSelectInput(Display *display, Window w, unsigned long event_mask); +void XShapeSelectInput(Display *display, Window w, unsigned long event_mask) +{ + RT_NOREF3(display, w, event_mask); + Assert(display == TEST_DISPLAY); +} + +extern "C" Window XDefaultRootWindow(Display *display); +Window XDefaultRootWindow(Display *display) +{ + RT_NOREF1(display); + Assert(display == TEST_DISPLAY); + return TEST_ROOT; +} + +static unsigned g_cSmlsWindows = 0; +static Window *g_paSmlsWindows = NULL; +static XWindowAttributes *g_paSmlsWinAttribs = NULL; +static const char **g_papszSmlsWinNames = NULL; + +extern "C" Status XQueryTree(Display *display, Window w, Window *root_return, + Window *parent_return, Window **children_return, + unsigned int *nchildren_return); +Status XQueryTree(Display *display, Window w, Window *root_return, + Window *parent_return, Window **children_return, + unsigned int *nchildren_return) +{ + RT_NOREF1(display); + Assert(display == TEST_DISPLAY); + AssertReturn(w == TEST_ROOT, False); /* We support nothing else */ + AssertPtrReturn(children_return, False); + AssertReturn(g_paSmlsWindows, False); + if (root_return) + *root_return = TEST_ROOT; + if (parent_return) + *parent_return = TEST_ROOT; + *children_return = (Window *)RTMemDup(g_paSmlsWindows, + g_cSmlsWindows * sizeof(Window)); + if (nchildren_return) + *nchildren_return = g_cSmlsWindows; + return (g_cSmlsWindows != 0); +} + +extern "C" Window XmuClientWindow(Display *dpy, Window win); +Window XmuClientWindow(Display *dpy, Window win) +{ + RT_NOREF1(dpy); + Assert(dpy == TEST_DISPLAY); + return win; +} + +extern "C" Status XGetWindowAttributes(Display *display, Window w, + XWindowAttributes *window_attributes_return); +Status XGetWindowAttributes(Display *display, Window w, + XWindowAttributes *window_attributes_return) +{ + RT_NOREF1(display); + Assert(display == TEST_DISPLAY); + AssertPtrReturn(window_attributes_return, 1); + for (unsigned i = 0; i < g_cSmlsWindows; ++i) + if (g_paSmlsWindows[i] == w) + { + *window_attributes_return = g_paSmlsWinAttribs[i]; + return 1; + } + return 0; +} + +extern "C" Status XGetWMNormalHints(Display *display, Window w, + XSizeHints *hints_return, + long *supplied_return); + +Status XGetWMNormalHints(Display *display, Window w, + XSizeHints *hints_return, long *supplied_return) +{ + RT_NOREF4(display, w, hints_return, supplied_return); + Assert(display == TEST_DISPLAY); + return 1; +} + +static void smlsSetWindowAttributes(XWindowAttributes *pAttribs, + Window *pWindows, unsigned cAttribs, + const char **paNames) +{ + g_paSmlsWinAttribs = pAttribs; + g_paSmlsWindows = pWindows; + g_cSmlsWindows = cAttribs; + g_papszSmlsWinNames = paNames; +} + +static Window g_SmlsShapedWindow = 0; +static int g_cSmlsShapeRectangles = 0; +static XRectangle *g_pSmlsShapeRectangles = NULL; + +extern "C" XRectangle *XShapeGetRectangles (Display *dpy, Window window, + int kind, int *count, + int *ordering); +XRectangle *XShapeGetRectangles (Display *dpy, Window window, int kind, + int *count, int *ordering) +{ + RT_NOREF2(dpy, kind); + Assert(dpy == TEST_DISPLAY); + if ((window != g_SmlsShapedWindow) || (window == 0)) + return NULL; /* Probably not correct, but works for us. */ + *count = g_cSmlsShapeRectangles; + *ordering = 0; + return (XRectangle *)RTMemDup(g_pSmlsShapeRectangles, + sizeof(XRectangle) + * g_cSmlsShapeRectangles); +} + +static void smlsSetShapeRectangles(Window window, int cRects, + XRectangle *pRects) +{ + g_SmlsShapedWindow = window; + g_cSmlsShapeRectangles = cRects; + g_pSmlsShapeRectangles = pRects; +} + +static int g_SmlsEventType = 0; +static Window g_SmlsEventWindow = 0; + +/* This should not be needed in the bits of the code we test. */ +extern "C" int XNextEvent(Display *display, XEvent *event_return); +int XNextEvent(Display *display, XEvent *event_return) +{ + RT_NOREF1(display); + Assert(display == TEST_DISPLAY); + event_return->xany.type = g_SmlsEventType; + event_return->xany.window = g_SmlsEventWindow; + event_return->xmap.window = g_SmlsEventWindow; + return True; +} + +static void smlsSetNextEvent(int type, Window window) +{ + g_SmlsEventType = type; + g_SmlsEventWindow = window; +} + +/* This should not be needed in the bits of the code we test. */ +extern "C" Status XSendEvent(Display *display, Window w, Bool propagate, + long event_mask, XEvent *event_send); +Status XSendEvent(Display *display, Window w, Bool propagate, + long event_mask, XEvent *event_send) +{ + RT_NOREF5(display, w, propagate, event_mask, event_send); + Assert(display == TEST_DISPLAY); + AssertFailedReturn(0); +} + +/* This should not be needed in the bits of the code we test. */ +extern "C" int XFlush(Display *display); +int XFlush(Display *display) +{ + RT_NOREF1(display); + Assert(display == TEST_DISPLAY); + AssertFailedReturn(0); +} + +/** Global "received a notification" flag. */ +static bool g_fNotified = false; + +/** Dummy host call-back. */ +static void sendRegionUpdate(RTRECT *pRects, size_t cRects) +{ + RT_NOREF2(pRects, cRects); + g_fNotified = true; +} + +static bool gotNotification(void) +{ + if (!g_fNotified) + return false; + g_fNotified = false; + return true; +} + +/***************************** +* The actual tests to be run * +*****************************/ + +/** The name of the unit test */ +static const char *g_pszTestName = NULL; + +/*** Test fixture data and data structures ***/ + +/** A structure describing a test fixture to be run through. Each fixture + * describes the state of the windows visible (and unmapped) on the X server + * before and after a particular event is delivered, and the expected + * on-screen positions of all interesting visible windows at the end of the + * fixture as reported by the code (currently in the order it is likely to + * report them in, @todo sort this). We expect that the set of visible + * windows will be the same whether we start the code before the event and + * handle it or start the code after the event. + */ +struct SMLSFIXTURE +{ + /** The number of windows visible before the event */ + unsigned cWindowsBefore; + /** An array of Window IDs for the visible and unmapped windows before + * the event */ + Window *pahWindowsBefore; + /** The window attributes matching the windows in @a paWindowsBefore */ + XWindowAttributes *paAttribsBefore; + /** The window names matching the windows in @a paWindowsBefore */ + const char **papszNamesBefore; + /** The shaped window before the event - we allow at most one of these. + * Zero for none. */ + Window hShapeWindowBefore; + /** The number of rectangles in the shaped window before the event. */ + int cShapeRectsBefore; + /** The rectangles in the shaped window before the event */ + XRectangle *paShapeRectsBefore; + /** The number of windows visible after the event */ + unsigned cWindowsAfter; + /** An array of Window IDs for the visible and unmapped windows after + * the event */ + Window *pahWindowsAfter; + /** The window attributes matching the windows in @a paWindowsAfter */ + XWindowAttributes *paAttribsAfter; + /** The window names matching the windows in @a paWindowsAfter */ + const char **papszNamesAfter; + /** The shaped window after the event - we allow at most one of these. + * Zero for none. */ + Window hShapeWindowAfter; + /** The number of rectangles in the shaped window after the event. */ + int cShapeRectsAfter; + /** The rectangles in the shaped window after the event */ + XRectangle *paShapeRectsAfter; + /** The event to delivered */ + int x11EventType; + /** The window for which the event in @enmEvent is delivered */ + Window hEventWindow; + /** The number of windows expected to be reported at the end of the + * fixture */ + unsigned cReportedRects; + /** The onscreen positions of those windows. */ + RTRECT *paReportedRects; + /** Do we expect notification after the event? */ + bool fExpectNotification; +}; + +/*** Test fixture to test the code against X11 configure (move) events ***/ + +static Window g_ahWin1[] = { 20 }; +static XWindowAttributes g_aAttrib1Before[] = +{ { 100, 200, 200, 300, 0, 0, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, IsViewable } +}; +static XRectangle g_aRectangle1[] = +{ + { 0, 0, 50, 50 }, + { 50, 50, 150, 250 } +}; +static XWindowAttributes g_aAttrib1After[] = +{ { 200, 300, 200, 300, 0, 0, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, IsViewable } +}; +static const char *g_apszNames1[] = { "Test Window" }; + +AssertCompile(RT_ELEMENTS(g_ahWin1) == RT_ELEMENTS(g_aAttrib1Before)); +AssertCompile(RT_ELEMENTS(g_ahWin1) == RT_ELEMENTS(g_aAttrib1After)); +AssertCompile(RT_ELEMENTS(g_ahWin1) == RT_ELEMENTS(g_apszNames1)); + +static RTRECT g_aRects1[] = +{ + { 200, 300, 250, 350 }, + { 250, 350, 400, 600 } +}; + +static SMLSFIXTURE g_testMove = +{ + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1Before, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1After, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + ConfigureNotify, + 20, + RT_ELEMENTS(g_aRects1), + g_aRects1, + true +}; + +/*** Test fixture to test the code against X11 configure (resize) events ***/ + +static XWindowAttributes g_aAttrib2Before[] = +{ { 100, 200, 200, 300, 0, 0, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, IsViewable } +}; +static XRectangle g_aRectangle2Before[] = +{ + { 0, 0, 50, 50 }, + { 50, 50, 100, 100 } +}; + +AssertCompile(RT_ELEMENTS(g_ahWin1) == RT_ELEMENTS(g_aAttrib2Before)); + +static SMLSFIXTURE g_testResize = +{ + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib2Before, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle2Before), + g_aRectangle2Before, + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1After, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + ConfigureNotify, + 20, + RT_ELEMENTS(g_aRects1), + g_aRects1, + true +}; + +/*** Test fixture to test the code against X11 map events ***/ + +static XWindowAttributes g_aAttrib3Before[] = +{ { 200, 300, 200, 300, 0, 0, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, IsUnmapped } +}; + +AssertCompile(RT_ELEMENTS(g_ahWin1) == RT_ELEMENTS(g_aAttrib3Before)); + +static SMLSFIXTURE g_testMap = +{ + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib3Before, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1After, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + MapNotify, + 20, + RT_ELEMENTS(g_aRects1), + g_aRects1, + true +}; + +/*** Test fixtures to test the code against X11 unmap events ***/ + +static XWindowAttributes g_aAttrib4After[] = +{ { 100, 200, 300, 400, 0, 0, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, IsUnmapped } +}; + +AssertCompile(RT_ELEMENTS(g_ahWin1) == RT_ELEMENTS(g_aAttrib4After)); + +static SMLSFIXTURE g_testUnmap = +{ + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1Before, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib4After, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + UnmapNotify, + 20, + 0, + NULL, + true +}; + +/*** A window we are not monitoring has been unmapped. Nothing should + *** happen, especially nothing bad. ***/ + +static RTRECT g_aRects2[] = +{ + { 100, 200, 150, 250 }, + { 150, 250, 300, 500 } +}; + +static SMLSFIXTURE g_testUnmapOther = +{ + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1Before, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1Before, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + UnmapNotify, + 21, + RT_ELEMENTS(g_aRects2), + g_aRects2, + false +}; + +/*** Test fixture to test the code against X11 shape events ***/ + +static XRectangle g_aRectangle5Before[] = +{ + { 0, 0, 200, 200 } +}; + +static SMLSFIXTURE g_testShape = +{ + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1After, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle5Before), + g_aRectangle5Before, + RT_ELEMENTS(g_ahWin1), + g_ahWin1, + g_aAttrib1After, + g_apszNames1, + 20, + RT_ELEMENTS(g_aRectangle1), + g_aRectangle1, + VBoxShapeNotify, + 20, + RT_ELEMENTS(g_aRects1), + g_aRects1, + true +}; + +/*** And the test code proper ***/ + +/** Compare two RTRECT structures */ +static bool smlsCompRect(RTRECT *pFirst, RTRECT *pSecond) +{ + return ( (pFirst->xLeft == pSecond->xLeft) + && (pFirst->yTop == pSecond->yTop) + && (pFirst->xRight == pSecond->xRight) + && (pFirst->yBottom == pSecond->yBottom)); +} + +static void smlsPrintDiffRects(RTRECT *pExp, RTRECT *pGot) +{ + RTPrintf(" Expected: %d, %d, %d, %d. Got: %d, %d, %d, %d\n", + pExp->xLeft, pExp->yTop, pExp->xRight, pExp->yBottom, + pGot->xLeft, pGot->yTop, pGot->xRight, pGot->yBottom); +} + +/** Run through a test fixture */ +static unsigned smlsDoFixture(SMLSFIXTURE *pFixture, const char *pszDesc) +{ + SeamlessX11 subject; + unsigned cErrs = 0; + + subject.init(sendRegionUpdate); + smlsSetWindowAttributes(pFixture->paAttribsBefore, + pFixture->pahWindowsBefore, + pFixture->cWindowsBefore, + pFixture->papszNamesBefore); + smlsSetShapeRectangles(pFixture->hShapeWindowBefore, + pFixture->cShapeRectsBefore, + pFixture->paShapeRectsBefore); + subject.start(); + smlsSetWindowAttributes(pFixture->paAttribsAfter, + pFixture->pahWindowsAfter, + pFixture->cWindowsAfter, + pFixture->papszNamesAfter); + smlsSetShapeRectangles(pFixture->hShapeWindowAfter, + pFixture->cShapeRectsAfter, + pFixture->paShapeRectsAfter); + smlsSetNextEvent(pFixture->x11EventType, pFixture->hEventWindow); + if (gotNotification()) /* Initial window tree rebuild */ + { + RTPrintf("%s: fixture: %s. Notification was set before the first event!!!\n", + g_pszTestName, pszDesc); + ++cErrs; + } + subject.nextConfigurationEvent(); + if (!gotNotification()) + { + RTPrintf("%s: fixture: %s. No notification was sent for the initial window tree rebuild.\n", + g_pszTestName, pszDesc); + ++cErrs; + } + smlsSetNextEvent(0, 0); + subject.nextConfigurationEvent(); + if (pFixture->fExpectNotification && !gotNotification()) + { + RTPrintf("%s: fixture: %s. No notification was sent after the event.\n", + g_pszTestName, pszDesc); + ++cErrs; + } + RTRECT *pRects = subject.getRects(); + size_t cRects = subject.getRectCount(); + if (cRects != pFixture->cReportedRects) + { + RTPrintf("%s: fixture: %s. Wrong number of rectangles reported after processing event (expected %u, got %u).\n", + g_pszTestName, pszDesc, pFixture->cReportedRects, + cRects); + ++cErrs; + } + else + for (unsigned i = 0; i < cRects; ++i) + if (!smlsCompRect(&pRects[i], &pFixture->paReportedRects[i])) + { + RTPrintf("%s: fixture: %s. Rectangle %u wrong after processing event.\n", + g_pszTestName, pszDesc, i); + smlsPrintDiffRects(&pFixture->paReportedRects[i], + &pRects[i]); + ++cErrs; + break; + } + subject.stop(); + subject.start(); + if (cRects != pFixture->cReportedRects) + { + RTPrintf("%s: fixture: %s. Wrong number of rectangles reported without processing event (expected %u, got %u).\n", + g_pszTestName, pszDesc, pFixture->cReportedRects, + cRects); + ++cErrs; + } + else + for (unsigned i = 0; i < cRects; ++i) + if (!smlsCompRect(&pRects[i], &pFixture->paReportedRects[i])) + { + RTPrintf("%s: fixture: %s. Rectangle %u wrong without processing event.\n", + g_pszTestName, pszDesc, i); + smlsPrintDiffRects(&pFixture->paReportedRects[i], + &pRects[i]); + ++cErrs; + break; + } + return cErrs; +} + +int main( int argc, char **argv) +{ + RTR3InitExe(argc, &argv, 0); + unsigned cErrs = 0; + g_pszTestName = RTPathFilename(argv[0]); + + RTPrintf("%s: TESTING\n", g_pszTestName); + cErrs += smlsDoFixture(&g_testMove, + "ConfigureNotify event (window moved)"); + // Currently not working + cErrs += smlsDoFixture(&g_testResize, + "ConfigureNotify event (window resized)"); + cErrs += smlsDoFixture(&g_testMap, "MapNotify event"); + cErrs += smlsDoFixture(&g_testUnmap, "UnmapNotify event"); + cErrs += smlsDoFixture(&g_testUnmapOther, + "UnmapNotify event for unmonitored window"); + cErrs += smlsDoFixture(&g_testShape, "ShapeNotify event"); + if (cErrs > 0) + RTPrintf("%u errors\n", cErrs); + return cErrs == 0 ? 0 : 1; +} diff --git a/src/VBox/Additions/x11/VBoxClient/testcase/tstSeamlessX11.cpp b/src/VBox/Additions/x11/VBoxClient/testcase/tstSeamlessX11.cpp new file mode 100644 index 00000000..102daa27 --- /dev/null +++ b/src/VBox/Additions/x11/VBoxClient/testcase/tstSeamlessX11.cpp @@ -0,0 +1,164 @@ +/* $Id: tstSeamlessX11.cpp $ */ +/** @file + * Linux seamless guest additions simulator in host. + */ + +/* + * Copyright (C) 2007-2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + +#include <stdlib.h> /* exit() */ + +#include <iprt/errcore.h> +#include <iprt/initterm.h> +#include <iprt/semaphore.h> +#include <iprt/string.h> +#include <iprt/stream.h> +#include <VBox/VBoxGuestLib.h> + +#include "../seamless.h" + +static RTSEMEVENT eventSem; + +void VBClLogError(const char *pszFormat, ...) +{ + va_list args; + va_start(args, pszFormat); + char *psz = NULL; + RTStrAPrintfV(&psz, pszFormat, args); + va_end(args); + + AssertPtr(psz); + RTPrintf("Error: %s", psz); + + RTStrFree(psz); +} + +/** Exit with a fatal error. */ +void VBClLogFatalError(const char *pszFormat, ...) +{ + va_list args; + va_start(args, pszFormat); + char *psz = NULL; + RTStrAPrintfV(&psz, pszFormat, args); + va_end(args); + + AssertPtr(psz); + RTPrintf("Fatal error: %s", psz); + + RTStrFree(psz); + + exit(1); +} + +int VBClStartVTMonitor() +{ + return VINF_SUCCESS; +} + +int VbglR3SeamlessSendRects(uint32_t cRects, PRTRECT pRects) +{ + RTPrintf("Received rectangle update (%u rectangles):\n", cRects); + for (unsigned i = 0; i < cRects; ++i) + { + RTPrintf(" xLeft: %d yTop: %d xRight: %d yBottom: %d\n", + pRects[i].xLeft, pRects[i].yTop, pRects[i].xRight, + pRects[i].yBottom); + } + return VINF_SUCCESS; +} + +int VbglR3SeamlessSetCap(bool bState) +{ + RTPrintf("%s\n", bState ? "Seamless capability set" + : "Seamless capability unset"); + return VINF_SUCCESS; +} + +int VbglR3CtlFilterMask(uint32_t u32OrMask, uint32_t u32NotMask) +{ + RTPrintf("IRQ filter mask changed. Or mask: 0x%x. Not mask: 0x%x\n", + u32OrMask, u32NotMask); + return VINF_SUCCESS; +} + +int VbglR3SeamlessWaitEvent(VMMDevSeamlessMode *pMode) +{ + static bool active = false; + + int rc = VINF_SUCCESS; + if (!active) + { + active = true; + *pMode = VMMDev_Seamless_Visible_Region; + } + else + rc = RTSemEventWait(eventSem, RT_INDEFINITE_WAIT); + return rc; +} + +VBGLR3DECL(int) VbglR3InitUser(void) { return VINF_SUCCESS; } +VBGLR3DECL(void) VbglR3Term(void) {} + +/** + * Xlib error handler for certain errors that we can't avoid. + */ +int vboxClientXLibErrorHandler(Display *pDisplay, XErrorEvent *pError) +{ + char errorText[1024]; + + if (pError->error_code == BadWindow) + { + /* This can be triggered if a guest application destroys a window before we notice. */ + RTPrintf("ignoring BadAtom error and returning\n"); + return 0; + } + XGetErrorText(pDisplay, pError->error_code, errorText, sizeof(errorText)); + RTPrintf("An X Window protocol error occurred: %s\n" + " Request code: %d\n" + " Minor code: %d\n" + " Serial number of the failed request: %d\n\n" + "exiting.\n", + errorText, (int)pError->request_code, (int)pError->minor_code, + (int)pError->serial); + exit(1); +} + +int main( int argc, char **argv) +{ + int rc = VINF_SUCCESS; + + RTR3InitExe(argc, &argv, 0); + RTPrintf("VirtualBox guest additions X11 seamless mode testcase\n"); + if (0 == XInitThreads()) + { + RTPrintf("Failed to initialise X11 threading, exiting.\n"); + exit(1); + } + /* Set an X11 error handler, so that we don't die when we get unavoidable errors. */ + XSetErrorHandler(vboxClientXLibErrorHandler); + RTPrintf("\nType Ctrl-C to exit...\n"); + RTSemEventCreate(&eventSem); + /** Our instance of the seamless class. */ + SeamlessMain seamless; + LogRel(("Starting seamless Guest Additions...\n")); + rc = seamless.init(); + if (rc != VINF_SUCCESS) + { + RTPrintf("Failed to initialise seamless Additions, rc = %Rrc\n", rc); + } + rc = seamless.run(); + if (rc != VINF_SUCCESS) + { + RTPrintf("Failed to run seamless Additions, rc = %Rrc\n", rc); + } + return rc; +} |