/** $Id: clipboard-x11.cpp 100286 2023-06-26 07:51:54Z vboxsync $ */ /** @file * Guest Additions - X11 Shared Clipboard implementation. */ /* * Copyright (C) 2007-2023 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * SPDX-License-Identifier: GPL-3.0-only */ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "VBoxClient.h" #include "clipboard.h" #ifdef LOG_GROUP # undef LOG_GROUP #endif #define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD #include #ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP /** * Worker for waiting for a transfer status change. */ static DECLCALLBACK(int) vbclX11TransferWaitForStatusWorker(PSHCLCONTEXT pCtx, PSHCLTRANSFER pTransfer, SHCLTRANSFERSTATUS enmSts, RTMSINTERVAL msTimeout) { RT_NOREF(pCtx); LogFlowFuncEnter(); int rc = VERR_TIMEOUT; ShClTransferAcquire(pTransfer); uint64_t const tsStartMs = RTTimeMilliTS(); while (RTTimeMilliTS() - tsStartMs <= msTimeout) { if (ShClTransferGetStatus(pTransfer) == enmSts) /* Currently we only have busy waiting here. */ { rc = VINF_SUCCESS; break; } RTThreadSleep(100); } ShClTransferRelease(pTransfer); return rc; } /** * @copydoc SHCLTRANSFERCALLBACKS::pfnOnRegistered * * This starts the HTTP server if not done yet and registers the transfer with it. * * @thread Clipboard main thread. */ static DECLCALLBACK(void) vbclX11OnHttpTransferRegisteredCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx, PSHCLTRANSFERCTX pTransferCtx) { RT_NOREF(pTransferCtx); LogFlowFuncEnter(); PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser; AssertPtr(pCtx); PSHCLTRANSFER pTransfer = pCbCtx->pTransfer; AssertPtr(pTransfer); ShClTransferAcquire(pTransfer); /* We only need to start the HTTP server when we actually receive data from the remote (host). */ if (ShClTransferGetDir(pTransfer) == SHCLTRANSFERDIR_FROM_REMOTE) { /* Retrieve the root entries as a first action, so that the transfer is ready to go * once it gets registered to HTTP server below. */ int rc2 = ShClTransferRootListRead(pTransfer); if (RT_SUCCESS(rc2)) { ShClTransferHttpServerMaybeStart(&pCtx->X11.HttpCtx); rc2 = ShClTransferHttpServerRegisterTransfer(&pCtx->X11.HttpCtx.HttpServer, pTransfer); } if (RT_FAILURE(rc2)) LogRel(("Shared Clipboard: Registering HTTP transfer failed: %Rrc\n", rc2)); } LogFlowFuncLeave(); } /** * Unregisters a transfer from a HTTP server. * * This also stops the HTTP server if no active transfers are found anymore. * * @param pCtx Shared clipboard context to unregister transfer for. * @param pTransfer Transfer to unregister. * * @thread Clipboard main thread. */ static void vbclX11HttpTransferUnregister(PSHCLCONTEXT pCtx, PSHCLTRANSFER pTransfer) { if (ShClTransferGetDir(pTransfer) == SHCLTRANSFERDIR_FROM_REMOTE) { ShClTransferHttpServerUnregisterTransfer(&pCtx->X11.HttpCtx.HttpServer, pTransfer); ShClTransferHttpServerMaybeStop(&pCtx->X11.HttpCtx); } ShClTransferRelease(pTransfer); } /** * @copydoc SHCLTRANSFERCALLBACKS::pfnOnUnregistered * * Unregisters a (now) unregistered transfer from the HTTP server. * * @thread Clipboard main thread. */ static DECLCALLBACK(void) vbclX11OnHttpTransferUnregisteredCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx, PSHCLTRANSFERCTX pTransferCtx) { RT_NOREF(pTransferCtx); vbclX11HttpTransferUnregister((PSHCLCONTEXT)pCbCtx->pvUser, pCbCtx->pTransfer); } /** * @copydoc SHCLTRANSFERCALLBACKS::pfnOnCompleted * * Unregisters a complete transfer from the HTTP server. * * @thread Clipboard main thread. */ static DECLCALLBACK(void) vbclX11OnHttpTransferCompletedCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx, int rc) { RT_NOREF(rc); vbclX11HttpTransferUnregister((PSHCLCONTEXT)pCbCtx->pvUser, pCbCtx->pTransfer); } /** @copydoc SHCLTRANSFERCALLBACKS::pfnOnError * * Unregisters a failed transfer from the HTTP server. * * @thread Clipboard main thread. */ static DECLCALLBACK(void) vbclX11OnHttpTransferErrorCallback(PSHCLTRANSFERCALLBACKCTX pCtx, int rc) { return vbclX11OnHttpTransferCompletedCallback(pCtx, rc); } #endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP */ /** * Worker for a reading clipboard from the host. */ static DECLCALLBACK(int) vbclX11ReadDataWorker(PSHCLCONTEXT pCtx, SHCLFORMAT uFmt, void **ppv, uint32_t *pcb, void *pvUser) { RT_NOREF(pvUser); LogFlowFuncEnter(); int rc = VERR_NO_DATA; /* Play safe. */ uint32_t cbRead = 0; uint32_t cbData = _4K; /** @todo Make this dynamic. */ void *pvData = RTMemAlloc(cbData); if (pvData) { rc = VbglR3ClipboardReadDataEx(&pCtx->CmdCtx, uFmt, 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, uFmt, 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)) { if (ppv) *ppv = pvData; if (pcb) *pcb = cbRead; /* Actual bytes read. */ } 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; } /** * @copydoc SHCLCALLBACKS::pfnOnRequestDataFromSource * * Requests URI data from the host. * This initiates a transfer on the host. Most of the handling will be done VbglR3 then. * * @thread X11 event thread. */ static DECLCALLBACK(int) vbclX11OnRequestDataFromSourceCallback(PSHCLCONTEXT pCtx, SHCLFORMAT uFmt, void **ppv, uint32_t *pcb, void *pvUser) { RT_NOREF(pvUser); LogFlowFunc(("pCtx=%p, uFmt=%#x\n", pCtx, uFmt)); int rc; #ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP if (uFmt == VBOX_SHCL_FMT_URI_LIST) { PSHCLHTTPSERVER pSrv = &pCtx->X11.HttpCtx.HttpServer; rc = vbclX11ReadDataWorker(pCtx, uFmt, ppv, pcb, pvUser); if (RT_SUCCESS(rc)) rc = ShClTransferHttpServerWaitForStatusChange(pSrv, SHCLHTTPSERVERSTATUS_TRANSFER_REGISTERED, 5000 /* SHCL_TIMEOUT_DEFAULT_MS */); if (RT_SUCCESS(rc)) { PSHCLTRANSFER pTransfer = ShClTransferHttpServerGetTransferLast(pSrv); if (pTransfer) { rc = vbclX11TransferWaitForStatusWorker(pCtx, pTransfer, SHCLTRANSFERSTATUS_STARTED, SHCL_TIMEOUT_DEFAULT_MS); if (RT_SUCCESS(rc)) { char *pszURL = ShClTransferHttpServerGetUrlA(pSrv, pTransfer->State.uID); char *pszData = NULL; RTStrAPrintf(&pszData, "copy\n%s", pszURL); *ppv = pszData; *pcb = strlen(pszData) + 1 /* Include terminator */; LogFlowFunc(("pszURL=%s\n", pszURL)); RTStrFree(pszURL); rc = VINF_SUCCESS; } } else AssertMsgFailed(("No registered transfer found for HTTP server\n")); } else LogRel(("Shared Clipboard: Could not start transfer, as the HTTP server is not running\n")); } else /* Anything else */ #endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP */ { rc = vbclX11ReadDataWorker(pCtx, uFmt, ppv, pcb, pvUser); } if (RT_FAILURE(rc)) LogRel(("Requesting data in format %#x from host failed with %Rrc\n", uFmt, rc)); LogFlowFuncLeaveRC(rc); return rc; } /** * @copydoc SHCLCALLBACKS::pfnReportFormats * * Reports clipboard formats to the host. * * @thread X11 event thread. */ static DECLCALLBACK(int) vbclX11ReportFormatsCallback(PSHCLCONTEXT pCtx, uint32_t fFormats, void *pvUser) { RT_NOREF(pvUser); LogFlowFunc(("fFormats=%#x\n", fFormats)); int rc = VbglR3ClipboardReportFormats(pCtx->CmdCtx.idClient, fFormats); LogFlowFuncLeaveRC(rc); return rc; } /** * Initializes the X11-specifc Shared Clipboard code. * * @returns VBox status code. */ int VBClX11ClipboardInit(void) { LogFlowFuncEnter(); int rc = ShClEventSourceCreate(&g_Ctx.EventSrc, 0 /* uID */); AssertRCReturn(rc, rc); SHCLCALLBACKS Callbacks; RT_ZERO(Callbacks); Callbacks.pfnReportFormats = vbclX11ReportFormatsCallback; Callbacks.pfnOnRequestDataFromSource = vbclX11OnRequestDataFromSourceCallback; rc = ShClX11Init(&g_Ctx.X11, &Callbacks, &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; } /** * Destroys the X11-specifc Shared Clipboard code. * * @returns VBox status code. */ int VBClX11ClipboardDestroy(void) { return ShClEventSourceDestroy(&g_Ctx.EventSrc); } /** * The main loop of the X11-specifc Shared Clipboard code. * * @returns VBox status code. * * @thread Clipboard service worker thread. */ int VBClX11ClipboardMain(void) { PSHCLCONTEXT pCtx = &g_Ctx; bool fShutdown = false; #ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS # ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP /* * Set callbacks. * Those will be registered within VbglR3 when a new transfer gets initialized. * * Used for starting / stopping the HTTP server. */ RT_ZERO(pCtx->CmdCtx.Transfers.Callbacks); pCtx->CmdCtx.Transfers.Callbacks.pvUser = pCtx; /* Assign context as user-provided callback data. */ pCtx->CmdCtx.Transfers.Callbacks.cbUser = sizeof(SHCLCONTEXT); pCtx->CmdCtx.Transfers.Callbacks.pfnOnRegistered = vbclX11OnHttpTransferRegisteredCallback; pCtx->CmdCtx.Transfers.Callbacks.pfnOnUnregistered = vbclX11OnHttpTransferUnregisteredCallback; pCtx->CmdCtx.Transfers.Callbacks.pfnOnCompleted = vbclX11OnHttpTransferCompletedCallback; pCtx->CmdCtx.Transfers.Callbacks.pfnOnError = vbclX11OnHttpTransferErrorCallback; # endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS_HTTP */ #endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS */ LogFlowFunc(("fUseLegacyProtocol=%RTbool, fHostFeatures=%#RX64 ...\n", pCtx->CmdCtx.fUseLegacyProtocol, pCtx->CmdCtx.fHostFeatures)); int rc; /* The thread waits for incoming messages from the host. */ for (;;) { PVBGLR3CLIPBOARDEVENT pEvent = (PVBGLR3CLIPBOARDEVENT)RTMemAllocZ(sizeof(VBGLR3CLIPBOARDEVENT)); AssertPtrBreakStmt(pEvent, rc = VERR_NO_MEMORY); 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(RT_MS_1SEC); continue; } else { AssertPtr(pEvent); LogFlowFunc(("Event uType=%RU32\n", pEvent->enmType)); switch (pEvent->enmType) { case VBGLR3CLIPBOARDEVENTTYPE_REPORT_FORMATS: { ShClX11ReportFormatsToX11Async(&g_Ctx.X11, pEvent->u.fReportedFormats); break; } case VBGLR3CLIPBOARDEVENTTYPE_READ_DATA: { PSHCLEVENT pReadDataEvent; rc = ShClEventSourceGenerateAndRegisterEvent(&pCtx->EventSrc, &pReadDataEvent); if (RT_SUCCESS(rc)) { rc = ShClX11ReadDataFromX11Async(&g_Ctx.X11, pEvent->u.fReadData, UINT32_MAX, pReadDataEvent); if (RT_SUCCESS(rc)) { PSHCLEVENTPAYLOAD pPayload; rc = ShClEventWait(pReadDataEvent, SHCL_TIMEOUT_DEFAULT_MS, &pPayload); if (RT_SUCCESS(rc)) { if (pPayload) { Assert(pPayload->cbData == sizeof(SHCLX11RESPONSE)); PSHCLX11RESPONSE pResp = (PSHCLX11RESPONSE)pPayload->pvData; rc = VbglR3ClipboardWriteDataEx(&pCtx->CmdCtx, pEvent->u.fReadData, pResp->Read.pvData, pResp->Read.cbData); RTMemFree(pResp->Read.pvData); pResp->Read.cbData = 0; ShClPayloadFree(pPayload); } } } ShClEventRelease(pReadDataEvent); pReadDataEvent = NULL; } if (RT_FAILURE(rc)) VbglR3ClipboardWriteDataEx(&pCtx->CmdCtx, pEvent->u.fReadData, NULL, 0); break; } case VBGLR3CLIPBOARDEVENTTYPE_QUIT: { VBClLogVerbose(2, "Host requested termination\n"); fShutdown = true; break; } #ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS case VBGLR3CLIPBOARDEVENTTYPE_TRANSFER_STATUS: { if (pEvent->u.TransferStatus.Report.uStatus == SHCLTRANSFERSTATUS_STARTED) { } 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; } LogFlowFuncLeaveRC(rc); return rc; }