VirtualBox

source: vbox/trunk/src/VBox/Additions/WINNT/VBoxTray/VBoxClipboard.cpp@ 103450

Last change on this file since 103450 was 103450, checked in by vboxsync, 3 months ago

Shared Clipboard: Condensed more code by adding a new Windows-specific function SharedClipboardWinTransferHandOffToDataObject(). bugref:9437

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 40.6 KB
Line 
1/* $Id: VBoxClipboard.cpp 103450 2024-02-19 14:55:20Z vboxsync $ */
2/** @file
3 * VBoxClipboard - Shared clipboard, Windows Guest Implementation.
4 */
5
6/*
7 * Copyright (C) 2006-2023 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28
29/*********************************************************************************************************************************
30* Header Files *
31*********************************************************************************************************************************/
32#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD
33#include <VBox/log.h>
34
35#include "VBoxTray.h"
36#include "VBoxHelpers.h"
37
38#include <iprt/asm.h>
39#include <iprt/errcore.h>
40#include <iprt/ldr.h>
41#include <iprt/mem.h>
42#include <iprt/utf16.h>
43
44#include <VBox/GuestHost/SharedClipboard.h>
45#include <VBox/GuestHost/SharedClipboard-win.h>
46#include <VBox/GuestHost/clipboard-helper.h>
47#include <VBox/HostServices/VBoxClipboardSvc.h> /* Temp, remove. */
48#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
49# include <VBox/GuestHost/SharedClipboard-transfers.h>
50#endif
51
52#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
53# include <iprt/win/shlobj.h>
54# include <iprt/win/shlwapi.h>
55#endif
56
57
58/*********************************************************************************************************************************
59* Structures and Typedefs *
60*********************************************************************************************************************************/
61struct SHCLCONTEXT
62{
63 /** Pointer to the VBoxClient service environment. */
64 const VBOXSERVICEENV *pEnv;
65 /** Command context. */
66 VBGLR3SHCLCMDCTX CmdCtx;
67 /** Windows-specific context data. */
68 SHCLWINCTX Win;
69 /** Thread handle for window thread. */
70 RTTHREAD hThread;
71 /** Start indicator flag. */
72 bool fStarted;
73 /** Shutdown indicator flag. */
74 bool fShutdown;
75#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
76 /** Associated transfer data. */
77 SHCLTRANSFERCTX TransferCtx;
78#endif
79};
80
81
82/*********************************************************************************************************************************
83* Static variables *
84*********************************************************************************************************************************/
85/** Static clipboard context (since it is the single instance). Directly used in the windows proc. */
86static SHCLCONTEXT g_Ctx = { NULL };
87/** Static window class name. */
88static char s_szClipWndClassName[] = SHCL_WIN_WNDCLASS_NAME;
89
90
91/*********************************************************************************************************************************
92* Prototypes *
93*********************************************************************************************************************************/
94#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
95static DECLCALLBACK(void) vbtrShClTransferCreatedCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx);
96static DECLCALLBACK(void) vbtrShClTransferDestroyCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx);
97static DECLCALLBACK(void) vbtrShClTransferInitializedCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx);
98static DECLCALLBACK(void) vbtrShClTransferStartedCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx);
99static DECLCALLBACK(void) vbtrShClTransferErrorCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx, int rc);
100#endif
101
102
103#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
104/**
105 * @copydoc SharedClipboardWinDataObject::CALLBACKS::pfnTransferBegin
106 *
107 * Called by SharedClipboardWinDataObject::GetData() when the user wants to paste data.
108 * This then requests a new transfer on the host.
109 *
110 * @thread Clipboard main thread.
111 */
112static DECLCALLBACK(int) vbtrShClDataObjectTransferBeginCallback(SharedClipboardWinDataObject::PCALLBACKCTX pCbCtx)
113{
114 LogFlowFuncEnter();
115
116 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser;
117 AssertPtr(pCtx);
118
119 int rc = VbglR3ClipboardTransferRequest(&pCtx->CmdCtx);
120
121 LogFlowFuncLeaveRC(rc);
122 return rc;
123}
124
125/**
126 * @copydoc SHCLTRANSFERCALLBACKS::pfnOnCreated
127 *
128 * Called by ShClTransferCreate via VbglR3.
129 *
130 * @thread Clipboard main thread.
131 */
132static DECLCALLBACK(void) vbtrShClTransferCreatedCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx)
133{
134 LogFlowFuncEnter();
135
136 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser;
137 AssertPtr(pCtx);
138
139 int rc = SharedClipboardWinTransferCreate(&pCtx->Win, pCbCtx->pTransfer);
140
141 LogFlowFuncLeaveRC(rc);
142}
143
144/**
145 * @copydoc SHCLTRANSFERCALLBACKS::pfnOnDestroy
146 *
147 * Called by ShClTransferDestroy via VbglR3.
148 *
149 * @thread Clipboard main thread.
150 */
151static DECLCALLBACK(void) vbtrShClTransferDestroyCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx)
152{
153 LogFlowFuncEnter();
154
155 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser;
156 AssertPtr(pCtx);
157
158 SharedClipboardWinTransferDestroy(&pCtx->Win, pCbCtx->pTransfer);
159
160 LogFlowFuncLeave();
161}
162
163/**
164 * @copydoc SHCLTRANSFERCALLBACKS::pfnOnInitialize
165 *
166 * Called by ShClTransferInit via VbglR3.
167 * For G->H: Called on transfer intialization to notify the "in-flight" IDataObject about a data transfer.
168 * For H->G: Called on transfer intialization to populate the transfer's root list.
169 *
170 * @thread Clipboard main thread.
171 */
172static DECLCALLBACK(int) vbtrShClTransferInitializeCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx)
173{
174 LogFlowFuncEnter();
175
176 int rc = VINF_SUCCESS;
177
178 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser;
179 AssertPtr(pCtx);
180
181 PSHCLTRANSFER pTransfer = pCbCtx->pTransfer;
182 AssertPtr(pTransfer);
183
184 switch(ShClTransferGetDir(pTransfer))
185 {
186 case SHCLTRANSFERDIR_FROM_REMOTE: /* G->H */
187 {
188 rc = SharedClipboardWinTransferHandOffToDataObject(&pCtx->Win, pTransfer);
189 break;
190 }
191
192 case SHCLTRANSFERDIR_TO_REMOTE: /* H->G */
193 {
194 rc = SharedClipboardWinTransferGetRootsFromClipboard(&pCtx->Win, pTransfer);
195 break;
196 }
197
198 default:
199 break;
200 }
201
202 LogFlowFuncLeaveRC(rc);
203 return rc;
204}
205
206/**
207 * Worker for a reading clipboard from the host.
208 *
209 * @returns VBox status code.
210 * @retval VERR_SHCLPB_NO_DATA if no clipboard data is available.
211 * @param pCtx Shared Clipbaord context to use.
212 * @param uFmt The format to read clipboard data in.
213 * @param ppvData Where to return the allocated data read.
214 * Must be free'd by the caller.
215 * @param pcbData Where to return number of bytes read.
216 * @param pvUser User-supplied context.
217 *
218 * @thread Clipboard main thread.
219 *
220 */
221static DECLCALLBACK(int) vbtrShClRequestDataFromSourceCallbackWorker(PSHCLCONTEXT pCtx,
222 SHCLFORMAT uFmt, void **ppvData, uint32_t *pcbData, void *pvUser)
223{
224 RT_NOREF(pvUser);
225
226 return VbglR3ClipboardReadDataEx(&pCtx->CmdCtx, uFmt, ppvData, pcbData);
227}
228
229/**
230 * @copydoc SHCLCALLBACKS::pfnOnRequestDataFromSource
231 *
232 * Called from the IDataObject implementation to request data from the host.
233 *
234 * @thread shclwnd thread.
235 */
236DECLCALLBACK(int) vbtrShClRequestDataFromSourceCallback(PSHCLCONTEXT pCtx,
237 SHCLFORMAT uFmt, void **ppv, uint32_t *pcb, void *pvUser)
238{
239 PRTREQ pReq = NULL;
240 int rc = RTReqQueueCallEx(pCtx->Win.hReqQ, &pReq, SHCL_TIMEOUT_DEFAULT_MS, RTREQFLAGS_IPRT_STATUS,
241 (PFNRT)vbtrShClRequestDataFromSourceCallbackWorker, 5, pCtx, uFmt, ppv, pcb, pvUser);
242 RTReqRelease(pReq);
243 return rc;
244}
245
246/**
247 * @copydoc SHCLTRANSFERCALLBACKS::pfnOnStart
248 *
249 * Called from VbglR3 (main thread) to notify the IDataObject.
250 *
251 * @thread Clipboard main thread.
252 */
253static DECLCALLBACK(void) vbtrShClTransferStartedCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx)
254{
255 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser;
256 AssertPtr(pCtx);
257
258 PSHCLTRANSFER pTransfer = pCbCtx->pTransfer;
259 AssertPtr(pTransfer);
260
261 SHCLTRANSFERDIR const enmDir = ShClTransferGetDir(pTransfer);
262
263 int rc = VINF_SUCCESS;
264
265 /* The guest wants to transfer data to the host. */
266 if (enmDir == SHCLTRANSFERDIR_TO_REMOTE) /* G->H */
267 {
268 rc = SharedClipboardWinTransferGetRootsFromClipboard(&pCtx->Win, pTransfer);
269 }
270 else if (enmDir == SHCLTRANSFERDIR_FROM_REMOTE) /* H->G */
271 {
272 /* Nothing to do here. */
273 }
274 else
275 AssertFailedStmt(rc = VERR_NOT_SUPPORTED);
276
277 if (RT_FAILURE(rc))
278 LogRel(("Shared Clipboard: Starting transfer failed, rc=%Rrc\n", rc));
279}
280
281/** @copydoc SHCLTRANSFERCALLBACKS::pfnOnCompleted */
282static DECLCALLBACK(void) vbtrShClTransferCompletedCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx, int rcCompletion)
283{
284 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser;
285 AssertPtr(pCtx);
286
287 LogFlowFunc(("rcCompletion=%Rrc\n", rcCompletion));
288
289 LogRel2(("Shared Clipboard: Transfer %RU16 %s\n",
290 ShClTransferGetID(pCbCtx->pTransfer), rcCompletion == VERR_CANCELLED ? "canceled" : "complete"));
291
292 SHCLTRANSFERSTATUS enmSts;
293
294 switch (rcCompletion)
295 {
296 case VERR_CANCELLED:
297 enmSts = SHCLTRANSFERSTATUS_CANCELED;
298 break;
299
300 case VINF_SUCCESS:
301 enmSts = SHCLTRANSFERSTATUS_COMPLETED;
302 break;
303
304 default:
305 AssertFailedStmt(enmSts = SHCLTRANSFERSTATUS_ERROR);
306 break;
307 }
308
309 int rc = VbglR3ClipboardTransferSendStatus(&pCtx->CmdCtx, pCbCtx->pTransfer, enmSts, rcCompletion);
310 LogFlowFuncLeaveRC(rc);
311}
312
313/** @copydoc SHCLTRANSFERCALLBACKS::pfnOnError */
314static DECLCALLBACK(void) vbtrShClTransferErrorCallback(PSHCLTRANSFERCALLBACKCTX pCbCtx, int rcError)
315{
316 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pCbCtx->pvUser;
317 AssertPtr(pCtx);
318
319 LogRel(("Shared Clipboard: Transfer %RU16 failed with %Rrc\n", ShClTransferGetID(pCbCtx->pTransfer), rcError));
320
321 if (g_cVerbosity) /* Only show this in verbose mode. */
322 {
323 char szMsg [256]; /* Sizes according to MSDN. */
324 char szTitle[64];
325
326 /** @todo Add some translation macros here. */
327 RTStrPrintf(szTitle, sizeof(szTitle), "VirtualBox Shared Clipboard");
328 RTStrPrintf(szMsg, sizeof(szMsg),
329 "Transfer %RU16 failed with %Rrc", ShClTransferGetID(pCbCtx->pTransfer), rcError);
330
331 hlpShowBalloonTip(g_hInstance, g_hwndToolWindow, ID_TRAYICON,
332 szMsg, szTitle,
333 5000 /* Time to display in msec */, NIIF_INFO);
334 }
335
336 int rc = VbglR3ClipboardTransferSendStatus(&pCtx->CmdCtx, pCbCtx->pTransfer, SHCLTRANSFERSTATUS_ERROR, rcError);
337 LogFlowFuncLeaveRC(rc);
338}
339#endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS */
340
341static LRESULT vbtrShClWndProcWorker(PSHCLCONTEXT pCtx, HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
342{
343 AssertPtr(pCtx);
344
345 const PSHCLWINCTX pWinCtx = &pCtx->Win;
346
347 LRESULT lresultRc = 0;
348
349 switch (msg)
350 {
351 case WM_CLIPBOARDUPDATE:
352 {
353 LogFunc(("WM_CLIPBOARDUPDATE: pWinCtx=%p\n", pWinCtx));
354
355 if (pCtx->fShutdown) /* If we're about to shut down, skip handling stuff here. */
356 break;
357
358 int rc = RTCritSectEnter(&pWinCtx->CritSect);
359 if (RT_SUCCESS(rc))
360 {
361 const HWND hWndClipboardOwner = GetClipboardOwner();
362
363 LogFunc(("WM_CLIPBOARDUPDATE: hWndOldClipboardOwner=%p, hWndNewClipboardOwner=%p\n",
364 pWinCtx->hWndClipboardOwnerUs, hWndClipboardOwner));
365
366 if (pWinCtx->hWndClipboardOwnerUs != hWndClipboardOwner)
367 {
368 int rc2 = RTCritSectLeave(&pWinCtx->CritSect);
369 AssertRC(rc2);
370
371 /* Clipboard was updated by another application.
372 * Report available formats to the host. */
373 SHCLFORMATS fFormats;
374 rc = SharedClipboardWinGetFormats(pWinCtx, &fFormats);
375 if (RT_SUCCESS(rc))
376 {
377 LogFunc(("WM_CLIPBOARDUPDATE: Reporting formats %#x\n", fFormats));
378 rc = VbglR3ClipboardReportFormats(pCtx->CmdCtx.idClient, fFormats);
379 }
380 }
381 else
382 {
383 int rc2 = RTCritSectLeave(&pWinCtx->CritSect);
384 AssertRC(rc2);
385 }
386 }
387
388 if (RT_FAILURE(rc))
389 LogRel(("Shared Clipboard: WM_CLIPBOARDUPDATE failed with %Rrc\n", rc));
390
391 break;
392 }
393
394 case WM_CHANGECBCHAIN:
395 {
396 LogFunc(("WM_CHANGECBCHAIN\n"));
397 lresultRc = SharedClipboardWinHandleWMChangeCBChain(pWinCtx, hwnd, msg, wParam, lParam);
398 break;
399 }
400
401 case WM_DRAWCLIPBOARD:
402 {
403 LogFlowFunc(("WM_DRAWCLIPBOARD: pWinCtx=%p\n", pWinCtx));
404
405 int rc = RTCritSectEnter(&pWinCtx->CritSect);
406 if (RT_SUCCESS(rc))
407 {
408 const HWND hWndClipboardOwner = GetClipboardOwner();
409
410 LogFunc(("WM_DRAWCLIPBOARD: hWndClipboardOwnerUs=%p, hWndNewClipboardOwner=%p\n",
411 pWinCtx->hWndClipboardOwnerUs, hWndClipboardOwner));
412
413 if (pWinCtx->hWndClipboardOwnerUs != hWndClipboardOwner)
414 {
415 int rc2 = RTCritSectLeave(&pWinCtx->CritSect);
416 AssertRC(rc2);
417
418 /* Clipboard was updated by another application. */
419 /* WM_DRAWCLIPBOARD always expects a return code of 0, so don't change "rc" here. */
420 SHCLFORMATS fFormats;
421 rc = SharedClipboardWinGetFormats(pWinCtx, &fFormats);
422 if ( RT_SUCCESS(rc)
423 && fFormats != VBOX_SHCL_FMT_NONE)
424 rc = VbglR3ClipboardReportFormats(pCtx->CmdCtx.idClient, fFormats);
425 }
426 else
427 {
428 int rc2 = RTCritSectLeave(&pWinCtx->CritSect);
429 AssertRC(rc2);
430 }
431 }
432
433 lresultRc = SharedClipboardWinChainPassToNext(pWinCtx, msg, wParam, lParam);
434 break;
435 }
436
437 case WM_TIMER:
438 {
439 int rc = SharedClipboardWinHandleWMTimer(pWinCtx);
440 AssertRC(rc);
441
442 break;
443 }
444
445 case WM_CLOSE:
446 {
447 /* Do nothing. Ignore the message. */
448 break;
449 }
450
451 case WM_RENDERFORMAT: /* Guest wants to render the clipboard data. */
452 {
453 /* Insert the requested clipboard format data into the clipboard. */
454 const UINT uFmtWin = (UINT)wParam;
455 const SHCLFORMAT uFmtVBox = SharedClipboardWinClipboardFormatToVBox(uFmtWin);
456
457 LogFunc(("WM_RENDERFORMAT: uFmtWin=%u -> uFmtVBox=0x%x\n", uFmtWin, uFmtVBox));
458#ifdef LOG_ENABLED
459 char *pszFmts = ShClFormatsToStrA(uFmtVBox);
460 AssertPtrReturn(pszFmts, 0);
461 LogRel(("Shared Clipboard: Rendering Windows format %#x as VBox format '%s'\n", uFmtWin, pszFmts));
462 RTStrFree(pszFmts);
463#endif
464 if (uFmtVBox == VBOX_SHCL_FMT_NONE)
465 {
466 LogRel(("Shared Clipboard: Unsupported format (%#x) requested\n", uFmtWin));
467 SharedClipboardWinClear();
468 }
469 else
470 {
471 void *pvData = NULL;
472 uint32_t cbData;
473
474 HANDLE hMem;
475 int rc = VbglR3ClipboardReadDataEx(&pCtx->CmdCtx, uFmtVBox, &pvData, &cbData);
476 if (RT_SUCCESS(rc))
477 {
478 /* Verify the size of returned text, the memory block for clipboard must have the exact string size. */
479 if (uFmtVBox == VBOX_SHCL_FMT_UNICODETEXT)
480 {
481 size_t cwcActual = 0;
482 rc = RTUtf16NLenEx((PCRTUTF16)pvData, cbData / sizeof(RTUTF16), &cwcActual);
483 if (RT_SUCCESS(rc))
484 cbData = (uint32_t)((cwcActual + 1 /* '\0' */) * sizeof(RTUTF16));
485 else
486 LogRel(("Shared Clipboard: Invalid UTF16 string from host: cb=%RU32, cwcActual=%zu, rc=%Rrc\n",
487 cbData, cwcActual, rc));
488 }
489 else if (uFmtVBox == VBOX_SHCL_FMT_HTML)
490 {
491 /* Wrap content into CF_HTML clipboard format if needed. */
492 if (!SharedClipboardWinIsCFHTML((const char *)pvData))
493 {
494 char *pszWrapped = NULL;
495 uint32_t cbWrapped = 0;
496 rc = SharedClipboardWinConvertMIMEToCFHTML((const char *)pvData, cbData, &pszWrapped, &cbWrapped);
497 if (RT_SUCCESS(rc))
498 {
499 AssertBreakStmt(cbWrapped, rc = VERR_INVALID_PARAMETER);
500 pvData = RTMemRealloc(pvData, cbWrapped);
501 if (pvData)
502 {
503 memcpy(pvData, pszWrapped, cbWrapped);
504 cbData = cbWrapped;
505 }
506 else
507 rc = VERR_NO_MEMORY;
508 RTMemFree(pszWrapped);
509 }
510
511 if (RT_FAILURE(rc))
512 LogRel(("Shared Clipboard: Cannot convert HTML clipboard data into CF_HTML clipboard format, rc=%Rrc\n", rc));
513 }
514 }
515
516 hMem = GlobalAlloc(GMEM_DDESHARE | GMEM_MOVEABLE, cbData);
517 if (hMem)
518 {
519 void *pvMem = GlobalLock(hMem);
520 if (pvMem)
521 {
522 memcpy(pvMem, pvData, cbData);
523 GlobalUnlock(hMem);
524
525 HANDLE hClip = SetClipboardData(uFmtWin, hMem);
526 if (!hClip)
527 {
528 /* The hMem ownership has gone to the system. Finish the processing. */
529 break;
530 }
531 else
532 LogRel(("Shared Clipboard: Setting host data buffer to clipboard failed with %Rrc\n",
533 RTErrConvertFromWin32(GetLastError())));
534 }
535 else
536 LogRel(("Shared Clipboard: Failed to lock memory (%Rrc)\n", RTErrConvertFromWin32(GetLastError())));
537 GlobalFree(hMem);
538 }
539 else
540 LogRel(("Shared Clipboard: No memory for allocating host data buffer\n"));
541 }
542 }
543
544 break;
545 }
546
547 case WM_RENDERALLFORMATS:
548 {
549 LogFunc(("WM_RENDERALLFORMATS\n"));
550
551 int rc = SharedClipboardWinHandleWMRenderAllFormats(pWinCtx, hwnd);
552 AssertRC(rc);
553
554 break;
555 }
556
557 case SHCL_WIN_WM_REPORT_FORMATS: /* Host reported clipboard formats. */
558 {
559 LogFunc(("SHCL_WIN_WM_REPORT_FORMATS\n"));
560
561 /* Announce available formats. Do not insert data -- will be inserted in WM_RENDERFORMAT. */
562 PVBGLR3CLIPBOARDEVENT pEvent = (PVBGLR3CLIPBOARDEVENT)lParam;
563 AssertPtr(pEvent);
564 Assert(pEvent->enmType == VBGLR3CLIPBOARDEVENTTYPE_REPORT_FORMATS);
565
566 const SHCLFORMATS fFormats = pEvent->u.fReportedFormats;
567
568#ifdef LOG_ENABLED
569 char *pszFmts = ShClFormatsToStrA(fFormats);
570 AssertPtrReturn(pszFmts, 0);
571 LogRel(("Shared Clipboard: Host reported formats '%s'\n", pszFmts));
572 RTStrFree(pszFmts);
573#endif
574 if (fFormats != VBOX_SHCL_FMT_NONE) /* Could arrive with some older GA versions. */
575 {
576 int rc = SharedClipboardWinClearAndAnnounceFormats(pWinCtx, fFormats, hwnd);
577#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
578 if ( RT_SUCCESS(rc)
579 && fFormats & VBOX_SHCL_FMT_URI_LIST)
580 {
581 /*
582 * Create our IDataObject implementation and push it to the Windows clibpoard.
583 * That way Windows will recognize that there is a data transfer available.
584 */
585 SharedClipboardWinDataObject::CALLBACKS Callbacks;
586 RT_ZERO(Callbacks);
587 Callbacks.pfnTransferBegin = vbtrShClDataObjectTransferBeginCallback;
588
589 rc = SharedClipboardWinTransferCreateAndSetDataObject(pWinCtx, pCtx, &Callbacks);
590 }
591#else
592 RT_NOREF(rc);
593#endif
594 }
595
596 LogFunc(("SHCL_WIN_WM_REPORT_FORMATS: fFormats=0x%x, lastErr=%ld\n", fFormats, GetLastError()));
597 break;
598 }
599
600 case SHCL_WIN_WM_READ_DATA: /* Host wants to read clipboard data from the guest. */
601 {
602 /* Send data in the specified format to the host. */
603 PVBGLR3CLIPBOARDEVENT pEvent = (PVBGLR3CLIPBOARDEVENT)lParam;
604 AssertPtr(pEvent);
605 Assert(pEvent->enmType == VBGLR3CLIPBOARDEVENTTYPE_READ_DATA);
606
607 const SHCLFORMAT fFormat = (uint32_t)pEvent->u.fReadData;
608
609 LogFlowFunc(("SHCL_WIN_WM_READ_DATA: fFormat=%#x\n", fFormat));
610#ifdef LOG_ENABLED
611 char *pszFmts = ShClFormatsToStrA(fFormat);
612 AssertPtrReturn(pszFmts, 0);
613 LogRel(("Shared Clipboard: Sending data to host as '%s'\n", pszFmts));
614 RTStrFree(pszFmts);
615#endif
616 int rc = SharedClipboardWinOpen(hwnd);
617 HANDLE hClip = NULL;
618 if (RT_SUCCESS(rc))
619 {
620 if (fFormat & VBOX_SHCL_FMT_BITMAP)
621 {
622 hClip = GetClipboardData(CF_DIB);
623 if (hClip != NULL)
624 {
625 LPVOID lp = GlobalLock(hClip);
626 if (lp != NULL)
627 {
628 rc = VbglR3ClipboardWriteDataEx(&pEvent->cmdCtx, fFormat, lp, (uint32_t)GlobalSize(hClip));
629
630 GlobalUnlock(hClip);
631 }
632 else
633 hClip = NULL;
634 }
635 }
636 else if (fFormat & VBOX_SHCL_FMT_UNICODETEXT)
637 {
638 hClip = GetClipboardData(CF_UNICODETEXT);
639 if (hClip != NULL)
640 {
641 LPWSTR uniString = (LPWSTR)GlobalLock(hClip);
642 if (uniString != NULL)
643 {
644 rc = VbglR3ClipboardWriteDataEx(&pEvent->cmdCtx,
645 fFormat, uniString, ((uint32_t)lstrlenW(uniString) + 1) * 2);
646
647 GlobalUnlock(hClip);
648 }
649 else
650 hClip = NULL;
651 }
652 }
653 else if (fFormat & VBOX_SHCL_FMT_HTML)
654 {
655 UINT format = RegisterClipboardFormat(SHCL_WIN_REGFMT_HTML);
656 if (format != 0)
657 {
658 hClip = GetClipboardData(format);
659 if (hClip != NULL)
660 {
661 LPVOID const pvClip = GlobalLock(hClip);
662 if (pvClip != NULL)
663 {
664 uint32_t const cbClip = (uint32_t)GlobalSize(hClip);
665
666 /* Unwrap clipboard content from CF_HTML format if needed. */
667 if (SharedClipboardWinIsCFHTML((const char *)pvClip))
668 {
669 char *pszBuf = NULL;
670 uint32_t cbBuf = 0;
671 rc = SharedClipboardWinConvertCFHTMLToMIME((const char *)pvClip, cbClip, &pszBuf, &cbBuf);
672 if (RT_SUCCESS(rc))
673 {
674 rc = VbglR3ClipboardWriteDataEx(&pEvent->cmdCtx, fFormat, pszBuf, cbBuf);
675 RTMemFree(pszBuf);
676 }
677 else
678 rc = VbglR3ClipboardWriteDataEx(&pEvent->cmdCtx, fFormat, pvClip, cbClip);
679 }
680 else
681 rc = VbglR3ClipboardWriteDataEx(&pEvent->cmdCtx, fFormat, pvClip, cbClip);
682
683 GlobalUnlock(hClip);
684 }
685 else
686 hClip = NULL;
687 }
688 }
689 }
690
691 if (hClip == NULL)
692 LogFunc(("SHCL_WIN_WM_READ_DATA: hClip=NULL, lastError=%ld\n", GetLastError()));
693
694 SharedClipboardWinClose();
695 }
696
697 /* If the requested clipboard format is not available, we must send empty data. */
698 if (hClip == NULL)
699 VbglR3ClipboardWriteDataEx(&pEvent->cmdCtx, VBOX_SHCL_FMT_NONE, NULL, 0);
700 break;
701 }
702
703 case WM_DESTROY:
704 {
705 LogFunc(("WM_DESTROY\n"));
706
707 int rc = SharedClipboardWinHandleWMDestroy(pWinCtx);
708 AssertRC(rc);
709
710 /*
711 * Don't need to call PostQuitMessage cause
712 * the VBoxTray already finished a message loop.
713 */
714
715 break;
716 }
717
718 default:
719 {
720 LogFunc(("WM_ %p\n", msg));
721 lresultRc = DefWindowProc(hwnd, msg, wParam, lParam);
722 break;
723 }
724 }
725
726 LogFunc(("WM_ rc %d\n", lresultRc));
727 return lresultRc;
728}
729
730static LRESULT CALLBACK vbtrShClWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
731
732static int vbtrShClCreateWindow(PSHCLCONTEXT pCtx)
733{
734 AssertPtrReturn(pCtx, VERR_INVALID_POINTER);
735
736 int rc = VINF_SUCCESS;
737
738 AssertPtr(pCtx->pEnv);
739 HINSTANCE hInstance = pCtx->pEnv->hInstance;
740 Assert(hInstance != 0);
741
742 /* Register the Window Class. */
743 WNDCLASSEX wc;
744 RT_ZERO(wc);
745
746 wc.cbSize = sizeof(WNDCLASSEX);
747
748 if (!GetClassInfoEx(hInstance, s_szClipWndClassName, &wc))
749 {
750 wc.style = CS_NOCLOSE;
751 wc.lpfnWndProc = vbtrShClWndProc;
752 wc.hInstance = pCtx->pEnv->hInstance;
753 wc.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1);
754 wc.lpszClassName = s_szClipWndClassName;
755
756 ATOM wndClass = RegisterClassEx(&wc);
757 if (wndClass == 0)
758 rc = RTErrConvertFromWin32(GetLastError());
759 }
760
761 if (RT_SUCCESS(rc))
762 {
763 const PSHCLWINCTX pWinCtx = &pCtx->Win;
764
765 /* Create the window. */
766 pWinCtx->hWnd = CreateWindowEx(WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT | WS_EX_TOPMOST,
767 s_szClipWndClassName, s_szClipWndClassName,
768 WS_POPUPWINDOW,
769 -200, -200, 100, 100, NULL, NULL, hInstance, NULL);
770 if (pWinCtx->hWnd == NULL)
771 {
772 rc = VERR_NOT_SUPPORTED;
773 }
774 else
775 {
776 SetWindowPos(pWinCtx->hWnd, HWND_TOPMOST, -200, -200, 0, 0,
777 SWP_NOACTIVATE | SWP_HIDEWINDOW | SWP_NOCOPYBITS | SWP_NOREDRAW | SWP_NOSIZE);
778
779 rc = SharedClipboardWinChainAdd(pWinCtx);
780 if (RT_SUCCESS(rc))
781 {
782 if (!SharedClipboardWinIsNewAPI(&pWinCtx->newAPI))
783 pWinCtx->oldAPI.timerRefresh = SetTimer(pWinCtx->hWnd, 0, 10 * 1000 /* 10s */, NULL);
784 }
785 }
786 }
787
788 LogFlowFuncLeaveRC(rc);
789 return rc;
790}
791
792static DECLCALLBACK(int) vbtrShClWindowThread(RTTHREAD hThread, void *pvUser)
793{
794 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pvUser;
795 AssertPtr(pCtx);
796
797#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
798 HRESULT hr = OleInitialize(NULL);
799 if (FAILED(hr))
800 {
801 LogRel(("Shared Clipboard: Initializing OLE in window thread failed (%Rhrc) -- file transfers unavailable\n", hr));
802 /* Not critical, the rest of the clipboard might work. */
803 }
804 else
805 LogRel(("Shared Clipboard: Initialized OLE in window thread\n"));
806#endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS */
807
808 int rc = vbtrShClCreateWindow(pCtx);
809 if (RT_FAILURE(rc))
810 {
811 LogRel(("Shared Clipboard: Unable to create window, rc=%Rrc\n", rc));
812 return rc;
813 }
814
815 pCtx->fStarted = true; /* Set started indicator. */
816
817 int rc2 = RTThreadUserSignal(hThread);
818 bool fSignalled = RT_SUCCESS(rc2);
819
820 LogRel2(("Shared Clipboard: Window thread running\n"));
821
822 if (RT_SUCCESS(rc))
823 {
824 for (;;)
825 {
826 MSG uMsg;
827 BOOL fRet;
828 while ((fRet = GetMessage(&uMsg, 0, 0, 0)) > 0)
829 {
830 TranslateMessage(&uMsg);
831 DispatchMessage(&uMsg);
832 }
833 Assert(fRet >= 0);
834
835 if (ASMAtomicReadBool(&pCtx->fShutdown))
836 break;
837
838 /** @todo Immediately drop on failure? */
839 }
840 }
841
842 if (!fSignalled)
843 {
844 rc2 = RTThreadUserSignal(hThread);
845 AssertRC(rc2);
846 }
847
848#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
849 OleSetClipboard(NULL); /* Make sure to flush the clipboard on destruction. */
850 OleUninitialize();
851#endif
852
853 LogRel(("Shared Clipboard: Window thread ended\n"));
854
855 LogFlowFuncLeaveRC(rc);
856 return rc;
857}
858
859static LRESULT CALLBACK vbtrShClWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
860{
861 PSHCLCONTEXT pCtx = &g_Ctx; /** @todo r=andy Make pCtx available through SetWindowLongPtr() / GWL_USERDATA. */
862 AssertPtr(pCtx);
863
864 /* Forward with proper context. */
865 return vbtrShClWndProcWorker(pCtx, hWnd, uMsg, wParam, lParam);
866}
867
868DECLCALLBACK(int) vbtrShClInit(const PVBOXSERVICEENV pEnv, void **ppInstance)
869{
870 LogFlowFuncEnter();
871
872 PSHCLCONTEXT pCtx = &g_Ctx; /* Only one instance for now. */
873 AssertPtr(pCtx);
874
875 if (pCtx->pEnv)
876 {
877 /* Clipboard was already initialized. 2 or more instances are not supported. */
878 return VERR_NOT_SUPPORTED;
879 }
880
881 if (VbglR3AutoLogonIsRemoteSession())
882 {
883 /* Do not use clipboard for remote sessions. */
884 LogRel(("Shared Clipboard: Clipboard has been disabled for a remote session\n"));
885 return VERR_NOT_SUPPORTED;
886 }
887
888 pCtx->pEnv = pEnv;
889 pCtx->hThread = NIL_RTTHREAD;
890 pCtx->fStarted = false;
891 pCtx->fShutdown = false;
892
893 int rc = RTReqQueueCreate(&pCtx->Win.hReqQ);
894 AssertRCReturn(rc, rc);
895
896 rc = SharedClipboardWinCtxInit(&pCtx->Win);
897 if (RT_SUCCESS(rc))
898 rc = VbglR3ClipboardConnectEx(&pCtx->CmdCtx, VBOX_SHCL_GF_0_CONTEXT_ID);
899 if (RT_SUCCESS(rc))
900 {
901#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
902 rc = ShClTransferCtxInit(&pCtx->TransferCtx);
903#endif
904 if (RT_SUCCESS(rc))
905 {
906 /* Message pump thread for our proxy window. */
907 rc = RTThreadCreate(&pCtx->hThread, vbtrShClWindowThread, pCtx /* pvUser */,
908 0, RTTHREADTYPE_MSG_PUMP, RTTHREADFLAGS_WAITABLE,
909 "shclwnd");
910 if (RT_SUCCESS(rc))
911 {
912 int rc2 = RTThreadUserWait(pCtx->hThread, RT_MS_30SEC /* Timeout in ms */);
913 AssertRC(rc2);
914
915 if (!pCtx->fStarted) /* Did the thread fail to start? */
916 rc = VERR_NOT_SUPPORTED; /* Report back Shared Clipboard as not being supported. */
917 }
918 }
919
920 if (RT_SUCCESS(rc))
921 {
922 *ppInstance = pCtx;
923 }
924 else
925 VbglR3ClipboardDisconnectEx(&pCtx->CmdCtx);
926 }
927
928 if (RT_FAILURE(rc))
929 LogRel(("Shared Clipboard: Unable to initialize, rc=%Rrc\n", rc));
930
931 LogFlowFuncLeaveRC(rc);
932 return rc;
933}
934
935DECLCALLBACK(int) vbtrShClWorker(void *pInstance, bool volatile *pfShutdown)
936{
937 AssertPtr(pInstance);
938 LogFlowFunc(("pInstance=%p\n", pInstance));
939
940 /*
941 * Tell the control thread that it can continue
942 * spawning services.
943 */
944 RTThreadUserSignal(RTThreadSelf());
945
946 const PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pInstance;
947 AssertPtr(pCtx);
948
949 const PSHCLWINCTX pWinCtx = &pCtx->Win;
950
951 LogRel2(("Shared Clipboard: Worker loop running\n"));
952
953#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
954 HRESULT hr = OleInitialize(NULL);
955 if (FAILED(hr))
956 {
957 LogRel(("Shared Clipboard: Initializing OLE in worker thread failed (%Rhrc) -- file transfers unavailable\n", hr));
958 /* Not critical, the rest of the clipboard might work. */
959 }
960 else
961 LogRel(("Shared Clipboard: Initialized OLE in worker thread\n"));
962
963 /*
964 * Init callbacks.
965 * Those will be registered within VbglR3 when a new transfer gets initialized.
966 */
967 RT_ZERO(pCtx->CmdCtx.Transfers.Callbacks);
968
969 pCtx->CmdCtx.Transfers.Callbacks.pvUser = pCtx; /* Assign context as user-provided callback data. */
970 pCtx->CmdCtx.Transfers.Callbacks.cbUser = sizeof(SHCLCONTEXT);
971
972 pCtx->CmdCtx.Transfers.Callbacks.pfnOnCreated = vbtrShClTransferCreatedCallback;
973 pCtx->CmdCtx.Transfers.Callbacks.pfnOnDestroy = vbtrShClTransferDestroyCallback;
974 pCtx->CmdCtx.Transfers.Callbacks.pfnOnInitialize = vbtrShClTransferInitializeCallback;
975 pCtx->CmdCtx.Transfers.Callbacks.pfnOnStarted = vbtrShClTransferStartedCallback;
976 pCtx->CmdCtx.Transfers.Callbacks.pfnOnCompleted = vbtrShClTransferCompletedCallback;
977 pCtx->CmdCtx.Transfers.Callbacks.pfnOnError = vbtrShClTransferErrorCallback;
978#endif /* VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS */
979
980 int rc;
981
982 /* The thread waits for incoming messages from the host. */
983 PVBGLR3CLIPBOARDEVENT pEvent = NULL;
984 for (;;)
985 {
986 LogFlowFunc(("Waiting for host message (fUseLegacyProtocol=%RTbool, fHostFeatures=%#RX64) ...\n",
987 pCtx->CmdCtx.fUseLegacyProtocol, pCtx->CmdCtx.fHostFeatures));
988
989 if (!pEvent)
990 pEvent = (PVBGLR3CLIPBOARDEVENT)RTMemAllocZ(sizeof(VBGLR3CLIPBOARDEVENT));
991 AssertPtrBreakStmt(pEvent, rc = VERR_NO_MEMORY);
992
993 uint32_t idMsg = 0;
994 uint32_t cParms = 0;
995 rc = VbglR3ClipboardMsgPeekWait(&pCtx->CmdCtx, &idMsg, &cParms, NULL /* pidRestoreCheck */);
996 if (RT_SUCCESS(rc))
997 {
998#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
999 rc = VbglR3ClipboardEventGetNextEx(idMsg, cParms, &pCtx->CmdCtx, &pCtx->TransferCtx, pEvent);
1000#else
1001 rc = VbglR3ClipboardEventGetNext(idMsg, cParms, &pCtx->CmdCtx, pEvent);
1002#endif
1003 }
1004 else if (rc == VERR_TRY_AGAIN) /* No new message (yet). */
1005 {
1006 RTReqQueueProcess(pCtx->Win.hReqQ, RT_MS_1SEC);
1007 continue;
1008 }
1009
1010 if (RT_FAILURE(rc))
1011 {
1012 LogFlowFunc(("Getting next event failed with %Rrc\n", rc));
1013
1014 VbglR3ClipboardEventFree(pEvent);
1015 pEvent = NULL;
1016
1017 if (*pfShutdown)
1018 break;
1019
1020 /* Wait a bit before retrying. */
1021 RTThreadSleep(1000);
1022 continue;
1023 }
1024 else
1025 {
1026 AssertPtr(pEvent);
1027 LogFlowFunc(("Event uType=%RU32\n", pEvent->enmType));
1028
1029 switch (pEvent->enmType)
1030 {
1031 case VBGLR3CLIPBOARDEVENTTYPE_REPORT_FORMATS:
1032 {
1033 /* The host has announced available clipboard formats.
1034 * Forward the information to the window, so it can later
1035 * respond to WM_RENDERFORMAT message. */
1036 ::PostMessage(pWinCtx->hWnd, SHCL_WIN_WM_REPORT_FORMATS,
1037 0 /* wParam */, (LPARAM)pEvent /* lParam */);
1038
1039 pEvent = NULL; /* Consume pointer. */
1040 break;
1041 }
1042
1043 case VBGLR3CLIPBOARDEVENTTYPE_READ_DATA:
1044 {
1045 /* The host needs data in the specified format. */
1046 ::PostMessage(pWinCtx->hWnd, SHCL_WIN_WM_READ_DATA,
1047 0 /* wParam */, (LPARAM)pEvent /* lParam */);
1048
1049 pEvent = NULL; /* Consume pointer. */
1050 break;
1051 }
1052
1053 case VBGLR3CLIPBOARDEVENTTYPE_QUIT:
1054 {
1055 LogRel2(("Shared Clipboard: Host requested termination\n"));
1056 ASMAtomicXchgBool(pfShutdown, true);
1057 break;
1058 }
1059
1060#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
1061 case VBGLR3CLIPBOARDEVENTTYPE_TRANSFER_STATUS:
1062 {
1063 /* Nothing to do here. */
1064 rc = VINF_SUCCESS;
1065 break;
1066 }
1067#endif
1068 case VBGLR3CLIPBOARDEVENTTYPE_NONE:
1069 {
1070 /* Nothing to do here. */
1071 rc = VINF_SUCCESS;
1072 break;
1073 }
1074
1075 default:
1076 {
1077 AssertMsgFailedBreakStmt(("Event type %RU32 not implemented\n", pEvent->enmType), rc = VERR_NOT_SUPPORTED);
1078 }
1079 }
1080
1081 if (pEvent)
1082 {
1083 VbglR3ClipboardEventFree(pEvent);
1084 pEvent = NULL;
1085 }
1086 }
1087
1088 if (*pfShutdown)
1089 break;
1090 }
1091
1092 LogRel2(("Shared Clipboard: Worker loop ended\n"));
1093
1094#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
1095 OleSetClipboard(NULL); /* Make sure to flush the clipboard on destruction. */
1096 OleUninitialize();
1097#endif
1098
1099 LogFlowFuncLeaveRC(rc);
1100 return rc;
1101}
1102
1103DECLCALLBACK(int) vbtrShClStop(void *pInstance)
1104{
1105 AssertPtrReturn(pInstance, VERR_INVALID_POINTER);
1106
1107 LogFunc(("Stopping pInstance=%p\n", pInstance));
1108
1109 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pInstance;
1110 AssertPtr(pCtx);
1111
1112 /* Set shutdown indicator. */
1113 ASMAtomicWriteBool(&pCtx->fShutdown, true);
1114
1115 /* Let our clipboard know that we're going to shut down. */
1116 PostMessage(pCtx->Win.hWnd, WM_QUIT, 0, 0);
1117
1118 /* Disconnect from the host service.
1119 * This will also send a VBOX_SHCL_HOST_MSG_QUIT from the host so that we can break out from our message worker. */
1120 VbglR3ClipboardDisconnect(pCtx->CmdCtx.idClient);
1121 pCtx->CmdCtx.idClient = 0;
1122
1123 LogFlowFuncLeaveRC(VINF_SUCCESS);
1124 return VINF_SUCCESS;
1125}
1126
1127DECLCALLBACK(void) vbtrShClDestroy(void *pInstance)
1128{
1129 AssertPtrReturnVoid(pInstance);
1130
1131 PSHCLCONTEXT pCtx = (PSHCLCONTEXT)pInstance;
1132 AssertPtrReturnVoid(pCtx);
1133
1134 /* Make sure that we are disconnected. */
1135 Assert(pCtx->CmdCtx.idClient == 0);
1136
1137 LogFlowFunc(("pCtx=%p\n", pCtx));
1138
1139 LogRel2(("Shared Clipboard: Destroying ...\n"));
1140
1141 const PSHCLWINCTX pWinCtx = &pCtx->Win;
1142
1143 if (pCtx->hThread != NIL_RTTHREAD)
1144 {
1145 int rcThread = VERR_WRONG_ORDER;
1146 int rc = RTThreadWait(pCtx->hThread, 60 * 1000 /* Timeout in ms */, &rcThread);
1147 LogFlowFunc(("Waiting for thread resulted in %Rrc (thread exited with %Rrc)\n",
1148 rc, rcThread));
1149 RT_NOREF(rc);
1150 }
1151
1152 if (pWinCtx->hWnd)
1153 {
1154 DestroyWindow(pWinCtx->hWnd);
1155 pWinCtx->hWnd = NULL;
1156 }
1157
1158 UnregisterClass(s_szClipWndClassName, pCtx->pEnv->hInstance);
1159
1160 SharedClipboardWinCtxDestroy(&pCtx->Win);
1161
1162#ifdef VBOX_WITH_SHARED_CLIPBOARD_TRANSFERS
1163 ShClTransferCtxDestroy(&pCtx->TransferCtx);
1164#endif
1165
1166 RTReqQueueDestroy(pCtx->Win.hReqQ);
1167
1168 LogRel2(("Shared Clipboard: Destroyed\n"));
1169
1170 return;
1171}
1172
1173/**
1174 * The service description.
1175 */
1176VBOXSERVICEDESC g_SvcDescClipboard =
1177{
1178 /* pszName. */
1179 "clipboard",
1180 /* pszDescription. */
1181 "Shared Clipboard",
1182 /* methods */
1183 vbtrShClInit,
1184 vbtrShClWorker,
1185 vbtrShClStop,
1186 vbtrShClDestroy
1187};
Note: See TracBrowser for help on using the repository browser.

© 2023 Oracle
ContactPrivacy policyTerms of Use