GuestDnDImpl.cpp revision b10a13ee0c4f4df2aa7cf2b164f0073fbd42e93c
/* $Id$ */
/** @file
* VirtualBox COM class implementation: Guest Drag and Drop parts
*/
/*
* Copyright (C) 2011-2014 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* 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 "GuestImpl.h"
#include "AutoCaller.h"
#ifdef VBOX_WITH_DRAG_AND_DROP
# include "ConsoleImpl.h"
# include "ProgressImpl.h"
# include "GuestDnDImpl.h"
# include <iprt/semaphore.h>
# include <VMMDev.h>
# ifdef LOG_GROUP
# endif
# define LOG_GROUP LOG_GROUP_GUEST_DND
/* How does this work:
*
* Drag and Drop is handled over the internal HGCM service for the host <->
* guest communication. Beside that we need to map the Drag and Drop protocols
* of the various OS's we support to our internal channels, this is also highly
* communicative in both directions. Unfortunately HGCM isn't really designed
* for that. Next we have to foul some of the components. This includes to
* trick X11 on the guest side, but also Qt needs to be tricked on the host
* side a little bit.
*
* The following components are involved:
*
* 1. GUI: Uses the Qt classes for Drag and Drop and mainly forward the content
* of it to the Main IGuest interface (see UIDnDHandler.cpp).
* 2. Main: Public interface for doing Drag and Drop. Also manage the IProgress
* interfaces for blocking the caller by showing a progress dialog (see
* this file).
* 3. HGCM service: Handle all messages from the host to the guest at once and
* encapsulate the internal communication details (see dndmanager.cpp and
* friends).
* 4. Guest additions: Split into the platform neutral part (see
* VBoxGuestR3LibDragAndDrop.cpp) and the guest OS specific parts.
* operations. Currently only X11 is supported (see draganddrop.cpp within
* VBoxClient).
*
* Host -> Guest:
* 1. There are DnD Enter, Move, Leave events which are send exactly like this
* to the guest. The info includes the pos, mimetypes and allowed actions.
* The guest has to respond with an action it would accept, so the GUI could
* change the cursor.
* 2. On drop, first a drop event is send. If this is accepted a drop data
* event follows. This blocks the GUI and shows some progress indicator.
*
* Guest -> Host:
* 1. The GUI is asking the guest if a DnD event is pending when the user moves
* the cursor out of the view window. If so, this returns the mimetypes and
* allowed actions.
* (2. On every mouse move this is asked again, to make sure the DnD event is
* still valid.)
* 3. On drop the host request the data from the guest. This blocks the GUI and
* shows some progress indicator.
*
* Some hints:
* m_sstrAllowedMimeTypes here in this file defines the allowed mime-types.
* This is necessary because we need special handling for some of the
* mime-types. E.g. for URI lists we need to transfer the actual dirs and
* files. Text EOL may to be changed. Also unknown mime-types may need special
* not done.
*
* Dropping of a directory, means recursively transferring _all_ the content.
*
* Directories and files are placed into the user's temporary directory on the
* guest (e.g. /tmp/VirtualBox Dropped Files). We can't delete them after the
* DnD operation, because we didn't know what the DnD target does with it. E.g.
* it could just be opened in place. This could lead ofc to filling up the disk
* within the guest. To inform the user about this, a small app could be
* developed which scans this directory regularly and inform the user with a
* tray icon hint (and maybe the possibility to clean this up instantly). The
* same has to be done in the G->H direction when it is implemented.
*
* Of course only regularly files are supported. Symlinks are resolved and
* transfered as regularly files. First we don't know if the other side support
* symlinks at all and second they could point to somewhere in a directory tree
* which not exists on the other side.
*
* is useful (and maybe necessary) for two things:
* 1. If a file is executable, it should be also after the transfer, so the
* user can just execute it, without manually tweaking the modes first.
* be in the guest.
* In any case, the user mode is always set to rwx (so that we can access it
* ourself, in e.g. for a cleanup case after cancel).
*
* Cancel is supported in both directions and cleans up all previous steps
*
* In general I propose the following changes in the VBox HGCM infrastructure
* for the future:
* - Currently it isn't really possible to send messages to the guest from the
* host. The host informs the guest just that there is something, the guest
* than has to ask which message and depending on that send the appropriate
* message to the host, which is filled with the right data.
* guest. This is now done here, but I guess was also necessary for e.g.
* guest execution. So something generic which brake this up into smaller
* blocks and send it would be nice (with all the error handling and such
* ofc).
* - I developed a "protocol" for the DnD communication here. So the host and
* the guest have always to match in the revision. This is ofc bad, because
* the additions could be outdated easily. So some generic protocol number
* support in HGCM for asking the host and the guest of the support version,
* would be nice. Ofc at least the host should be able to talk to the guest,
* even when the version is below the host one.
* All this stuff would be useful for the current services, but also for future
* onces.
*
* Todo:
* - Dragging out of the guest (partly done)
* - ESC doesn't really work (on Windows guests it's already implemented)
* - transfer of URIs (that is the files and patching of the data)
* - testing in a multi monitor setup
* ... in any case it seems a little bit difficult to handle from the Qt
* side. Maybe also a host specific implementation becomes necessary ...
* this would be really worst ofc.
* - Win guest support (maybe there have to be done a mapping between the
* official mime-types and Win Clipboard formats (see QWindowsMime, for an
* idea), for VBox internally only mime-types should be used)
* - EOL handling on text (should be shared with the clipboard code)
* - add configuration (GH, HG, Bidirectional, None), like for the clipboard
* - HG->GH and GH->HG-switch: Handle the case the user drags something out of
* the guest and than return to the source view (or another window in the
* multiple guest screen scenario).
* - add support for more mime-types (especially images, csv)
* - test unusual behavior:
* - DnD service crash in the guest during a DnD op (e.g. crash of VBoxClient or X11)
* - not expected order of the events between HGCM and the guest
* - Security considerations: We transfer a lot of memory between the guest and
* limits introduced to preventing DOS attacks or filling up all the memory
* (both in the host and the guest).
* - test, test, test ...
*/
class DnDGuestResponse
{
public:
virtual ~DnDGuestResponse(void);
public:
int notifyAboutGuestResponse(void);
void reset(void);
public:
private:
/** The actual MIME data.*/
void *m_pvData;
/** Size (in bytes) of MIME data. */
/** Dropped files directory on the host. */
/** The handle of the currently opened file being written to
* or read from. */
};
/** @todo This class needs a major cleanup. Later. */
class GuestDnDPrivate
{
private:
/** @todo Currently we only support one response. Maybe this needs to be extended at some time. */
: q_ptr(q)
, p(pGuest)
{}
virtual ~GuestDnDPrivate(void) { delete m_pDnDResponse; }
/* Static helpers. */
static void toHGCMActions(DragAndDropAction_T inDefAction, uint32_t *pOutDefAction, ComSafeArrayIn(DragAndDropAction_T, inAllowedActions), uint32_t *pOutAllowedActions);
/* Private q and parent pointer */
/* Private helper members. */
friend class GuestDnD;
};
/* What mime-types are supported by VirtualBox.
* Note: If you add something here, make sure you test it with all guest OS's!
** @todo Make this MIME list configurable / extendable (by extra data?). Currently
*/
/* static */
/* Uri's */
/* Text */
<< "UTF8_STRING"
<< "COMPOUND_TEXT"
<< "TEXT"
<< "STRING"
/* OpenOffice formates */
<< "application/x-openoffice-embed-source-xml;windows_formatname=\"Star Embed Source (XML)\""
<< "application/x-openoffice-drawing;windows_formatname=\"Drawing Format\"";
, m_defAction(0)
, m_allActions(0)
, m_pvData(0)
, m_cbData(0)
, m_cbDataCurrent(0)
, m_cbDataTotal(0)
{
}
DnDGuestResponse::~DnDGuestResponse(void)
{
reset();
}
{
/* pcbCurSize is optional. */
int rc = VINF_SUCCESS;
/** @todo Make reallocation scheme a bit smarter here. */
if (m_pvData)
{
if (pcbCurSize)
*pcbCurSize = m_cbData;
}
else
rc = VERR_NO_MEMORY;
return rc;
}
/* static */
{
switch (guestRc)
{
case VERR_ACCESS_DENIED:
strError += Utf8StrFmt(pGuest->tr("For one or more guest files or directories selected for transferring to the host your guest "
"user does not have the appropriate access rights for. Please make sure that all selected "
"elements can be accessed and that your guest user has the appropriate rights."));
break;
case VERR_NOT_FOUND:
/* Should not happen due to file locking on the guest, but anyway ... */
strError += Utf8StrFmt(pGuest->tr("One or more guest files or directories selected for transferring to the host were not"
"altered while the drag'n drop operation was in progress."));
break;
case VERR_SHARING_VIOLATION:
strError += Utf8StrFmt(pGuest->tr("One or more guest files or directories selected for transferring to the host were locked. "
"Please make sure that all selected elements can be accessed and that your guest user has "
"the appropriate rights."));
break;
default:
break;
}
return strError;
}
int DnDGuestResponse::notifyAboutGuestResponse(void)
{
return RTSemEventSignal(m_EventSem);
}
void DnDGuestResponse::reset(void)
{
m_defAction = 0;
m_allActions = 0;
m_strDropDir = "";
m_strFormat = "";
if (m_pvData)
{
}
m_cbData = 0;
m_cbDataCurrent = 0;
m_cbDataTotal = 0;
if (m_hFile != NIL_RTFILE)
{
}
m_strFile = "";
}
{
{
TRUE);
}
return rc;
}
{
LogFlowFunc(("uPercentage=%RU32, uState=%RU32, rcOp=%Rrc\n",
int vrc = VINF_SUCCESS;
if (!m_progress.isNull())
{
if (!fCompleted)
{
{
reset();
}
{
reset();
}
else /* uState == DragAndDropSvc::DND_PROGRESS_RUNNING */
{
|| uPercentage >= 100)
}
}
}
return vrc;
}
{
if (cbDataTotal)
{
#ifndef DEBUG_andy
AssertMsg(m_cbDataTotal <= cbDataTotal, ("New data size must not be smaller (%zu) than old value (%zu)\n",
#endif
}
/** @todo Don't use anonymous enums (uint32_t). */
if (m_cbDataCurrent >= m_cbDataTotal)
#ifdef DEBUG_andy
LogFlowFunc(("Updating transfer status (%zu/%zu), status=%ld\n",
#else
("More data transferred (%zu) than initially announced (%zu), cbDataAdd=%zu\n",
#endif
/** @todo For now we instantly confirm the cancel. Check if the
* guest should first clean up stuff itself and than really confirm
* the cancel request by an extra message. */
if (rc == VERR_CANCELLED)
return rc;
}
{
}
{
#ifdef DEBUG_andy
#endif
return rc;
}
{
/** @todo Support locking more than one file at a time! We
* might want to have a table in DnDGuestImpl which
* keeps those file pointers around, or extend the
* actual protocol for explicit open calls.
*
* For now we only keep one file open at a time, so if
* a client does alternating writes to different files
* this function will close the old and re-open the new
* file on every call. */
int rc;
if ( m_hFile == NIL_RTFILE
{
if (pszFile)
{
/** @todo Respect fMode! */
if (RT_SUCCESS(rc))
{
LogFlowFunc(("Opening \"%s\" (fMode=0x%x) for writing ...\n",
}
}
else
rc = VERR_NO_MEMORY;
}
else
rc = VINF_SUCCESS;
if (RT_SUCCESS(rc))
{
NULL /* No partial writes */);
if (RT_SUCCESS(rc))
}
return rc;
}
///////////////////////////////////////////////////////////////////////////////
{
/* For multi-monitor support we need to add shift values to the coordinates
* (depending on the screen number). */
return hr;
return hr;
return hr;
}
PVBOXHGCMSVCPARM paParms) const
{
{
/* Make sure mParent is valid, so set the read lock while using.
* Do not keep this lock while doing the actual call, because in the meanwhile
* another thread could request a write lock which would be a bad idea ... */
/* Forward the information to the VMM device. */
}
if (!pVMMDev)
throw p->setError(VBOX_E_VM_ERROR,
p->tr("VMM device is not available (is the VM running?)"));
if (RT_FAILURE(rc))
{
throw p->setError(VBOX_E_IPRT_ERROR,
}
return rc;
}
/* static */
{
{
/* Only keep allowed format types. */
if (m_sstrAllowedMimeTypes.contains(f))
strFormat += f + "\r\n";
}
return strFormat;
}
/* static */
void GuestDnDPrivate::toFormatSafeArray(const RTCString &strFormats, ComSafeArrayOut(BSTR, formats))
{
size_t i = 0;
{
/* Only keep allowed format types. */
else
++i;
}
/* Create a safe array out of the cleaned list. */
{
if (m_sstrAllowedMimeTypes.contains(f))
{
}
}
}
/* static */
{
switch (action)
{
case DragAndDropAction_Copy: a = DND_COPY_ACTION; break;
case DragAndDropAction_Move: a = DND_MOVE_ACTION; break;
case DragAndDropAction_Link: /* For now it doesn't seems useful to allow a link action between host & guest. Maybe later! */
case DragAndDropAction_Ignore: /* Ignored */ break;
}
return a;
}
/* static */
{
/* Defaults */
/* First convert the allowed actions to a bit array. */
/* Second check if the default action is a valid action and if not so try
* to find an allowed action. */
if (isDnDIgnoreAction(*pOutDefAction))
{
else if (hasDnDMoveAction(*pOutAllowedActions))
}
}
/* static */
{
/* For now it doesn't seems useful to allow a
* link action between host & guest. Maybe later! */
}
/* static */
{
/* For now it doesn't seems useful to allow a
* link action between host & guest. Maybe later! */
if (hasDnDCopyAction(uActions))
if (hasDnDMoveAction(uActions))
}
{
}
{
delete d_ptr;
}
{
/* Default is ignoring */
/* Check & convert the drag & drop actions */
uint32_t uDefAction = 0;
uint32_t uAllowedActions = 0;
/* If there is no usable action, ignore this request. */
if (isDnDIgnoreAction(uDefAction))
return S_OK;
/* Make a flat data string out of the mime-type list. */
/* If there is no valid mime-type, ignore this request. */
if (strFormats.isEmpty())
return S_OK;
try
{
/* Adjust the coordinates in a multi-monitor setup. */
int i = 0;
i,
paParms);
return S_OK;
/* Copy the response info */
}
{
}
return hr;
}
{
/* Default is ignoring */
/* Check & convert the drag & drop actions */
uint32_t uDefAction = 0;
uint32_t uAllowedActions = 0;
/* If there is no usable action, ignore this request. */
if (isDnDIgnoreAction(uDefAction))
return S_OK;
/* Make a flat data string out of the mime-type list. */
/* If there is no valid mime-type, ignore this request. */
if (strFormats.isEmpty())
return S_OK;
try
{
/* Adjust the coordinates in a multi-monitor setup. */
int i = 0;
i,
paParms);
return S_OK;
/* Copy the response info */
}
{
}
return hr;
}
{
try
{
0,
NULL);
}
{
}
return hr;
}
{
/* Default is ignoring */
/* Check & convert the drag & drop actions */
uint32_t uDefAction = 0;
uint32_t uAllowedActions = 0;
/* If there is no usable action, ignore this request. */
if (isDnDIgnoreAction(uDefAction))
return S_OK;
/* Make a flat data string out of the mime-type list. */
/* If there is no valid mime-type, ignore this request. */
if (strFormats.isEmpty())
return S_OK;
try
{
/* Adjust the coordinates in a multi-monitor setup. */
int i = 0;
i,
paParms);
return S_OK;
/* Get the resulting action from the guest. */
LogFlowFunc(("resFormat=%s, resAction=%RU32\n",
}
{
}
return hr;
}
{
try
{
int i = 0;
/* Reset any old progress status. */
pResp->resetProgress(p);
/* Note: The actual data transfer of files/directoies is performed by the
* DnD host service. */
i,
paParms);
/* Query the progress object to the caller. */
}
{
}
return hr;
}
#ifdef VBOX_WITH_DRAG_AND_DROP_GH
{
/* Default is ignoring */
try
{
int i = 0;
i,
paParms);
return S_OK;
return S_OK;
/* Fetch the default action to use. */
}
{
}
return hr;
}
{
/* If there is no usable action, ignore this request. */
if (isDnDIgnoreAction(uAction))
return S_OK;
LogFlowFunc(("strFormat=%s, uAction=0x%x, fNeedsDropDir=%RTbool\n",
/* Reset any old data. */
pResp->resetProgress(p);
/* Set the format we are going to retrieve to have it around
* when retrieving the data later. */
if (fNeedsDropDir)
{
char szDropDir[RTPATH_MAX];
if (RT_FAILURE(rc))
return p->setError(VBOX_E_IPRT_ERROR,
p->tr("Unable to create the temporary drag'n drop directory \"%s\" (%Rrc)\n"),
}
try
{
int i = 0;
i,
paParms);
/* Query the progress object to the caller. */
}
{
}
return hr;
}
{
if (pResp)
{
if (cbData)
{
LogFlowFunc(("strFormat=%s, cbData=%zu, pvData=0x%p\n",
{
if (RT_SUCCESS(rc2))
{
else
hr = E_OUTOFMEMORY;
}
else
}
else
{
/* Copy the data into a safe array of bytes. */
else
hr = E_OUTOFMEMORY;
}
}
/* Detach in any case, regardless of data size. */
/* Delete the data. */
}
else
return hr;
}
{
if (RT_SUCCESS(rc))
return rc;
}
{
LogFlowFunc(("pszPath=%s, cbPath=%zu, fMode=0x%x\n",
int rc;
if (pszDir)
{
}
else
rc = VERR_NO_MEMORY;
if (RT_SUCCESS(rc))
return rc;
}
{
LogFlowFunc(("pszPath=%s, cbPath=%zu, fMode=0x%x\n",
return rc;
}
#endif /* VBOX_WITH_DRAG_AND_DROP_GH */
/* static */
{
LogFlowFunc(("pvExtension=%p, u32Function=%RU32, pvParms=%p, cbParms=%RU32\n",
if (!pGuest->m_pGuestDnD)
return VINF_SUCCESS;
return VERR_INVALID_PARAMETER;
int rc = VINF_SUCCESS;
switch (u32Function)
{
{
DragAndDropSvc::PVBOXDNDCBHGACKOPDATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBHGACKOPDATA>(pvParms);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_HG_ACK_OP == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
break;
}
{
DragAndDropSvc::PVBOXDNDCBHGREQDATADATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBHGREQDATADATA>(pvParms);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_HG_REQ_DATA == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
break;
}
{
DragAndDropSvc::PVBOXDNDCBHGEVTPROGRESSDATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBHGEVTPROGRESSDATA>(pvParms);
AssertReturn(sizeof(DragAndDropSvc::VBOXDNDCBHGEVTPROGRESSDATA) == cbParms, VERR_INVALID_PARAMETER);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_HG_EVT_PROGRESS == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
break;
}
#ifdef VBOX_WITH_DRAG_AND_DROP_GH
{
DragAndDropSvc::PVBOXDNDCBGHACKPENDINGDATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBGHACKPENDINGDATA>(pvParms);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_GH_ACK_PENDING == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
break;
}
{
DragAndDropSvc::PVBOXDNDCBSNDDATADATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBSNDDATADATA>(pvParms);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_GH_SND_DATA == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
break;
}
{
DragAndDropSvc::PVBOXDNDCBSNDDIRDATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBSNDDIRDATA>(pvParms);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_GH_SND_DIR == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
break;
}
{
DragAndDropSvc::PVBOXDNDCBSNDFILEDATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBSNDFILEDATA>(pvParms);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_GH_SND_FILE == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
break;
}
{
DragAndDropSvc::PVBOXDNDCBEVTERRORDATA pCBData = reinterpret_cast<DragAndDropSvc::PVBOXDNDCBEVTERRORDATA>(pvParms);
AssertReturn(DragAndDropSvc::CB_MAGIC_DND_GH_EVT_ERROR == pCBData->hdr.u32Magic, VERR_INVALID_PARAMETER);
/* Cleanup. */
break;
}
#endif /* VBOX_WITH_DRAG_AND_DROP_GH */
default:
break;
}
return rc;
}
#endif /* VBOX_WITH_DRAG_AND_DROP */