VirtualBox

source: vbox/trunk/src/VBox/HostServices/SharedClipboard/x11-clipboard.cpp@ 23133

Last change on this file since 23133 was 22181, checked in by vboxsync, 15 years ago

GuestHost/SharedClipboard: use the XFIXES notification extension in the X11 shared clipboard instead of polling

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 19.9 KB
Line 
1/** @file
2 *
3 * Shared Clipboard:
4 * Linux host.
5 */
6
7/*
8 * Copyright (C) 2006-2007 Sun Microsystems, Inc.
9 *
10 * This file is part of VirtualBox Open Source Edition (OSE), as
11 * available from http://www.virtualbox.org. This file is free software;
12 * you can redistribute it and/or modify it under the terms of the GNU
13 * General Public License (GPL) as published by the Free Software
14 * Foundation, in version 2 as it comes in the "COPYING" file of the
15 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
16 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
17 *
18 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
19 * Clara, CA 95054 USA or visit http://www.sun.com if you need
20 * additional information or have any questions.
21 */
22
23#define LOG_GROUP LOG_GROUP_SHARED_CLIPBOARD
24
25#include <string.h>
26
27#include <iprt/assert.h>
28#include <iprt/critsect.h>
29#include <iprt/mem.h>
30#include <iprt/semaphore.h>
31
32#include <VBox/GuestHost/SharedClipboard.h>
33#include <VBox/HostServices/VBoxClipboardSvc.h>
34
35#include "VBoxClipboard.h"
36
37struct _VBOXCLIPBOARDREQFROMVBOX;
38typedef struct _VBOXCLIPBOARDREQFROMVBOX VBOXCLIPBOARDREQFROMVBOX;
39
40/** Global context information used by the host glue for the X11 clipboard
41 * backend */
42struct _VBOXCLIPBOARDCONTEXT
43{
44 /** This mutex is grabbed during any critical operations on the clipboard
45 * which might clash with others. */
46 RTCRITSECT clipboardMutex;
47 /** The currently pending request for data from VBox. NULL if there is
48 * no request pending. The protocol for completing a request is to grab
49 * the critical section, check that @a pReq is not NULL, fill in the data
50 * fields and set @a pReq to NULL. The protocol for cancelling a pending
51 * request is to grab the critical section and set pReq to NULL.
52 * It is an error if a request arrives while another one is pending, and
53 * the backend is responsible for ensuring that this does not happen. */
54 VBOXCLIPBOARDREQFROMVBOX *pReq;
55
56 /** Pointer to the opaque X11 backend structure */
57 CLIPBACKEND *pBackend;
58 /** Pointer to the VBox host client data structure. */
59 VBOXCLIPBOARDCLIENTDATA *pClient;
60 /** We set this when we start shutting down as a hint not to post any new
61 * requests. */
62 bool fShuttingDown;
63};
64
65/**
66 * Report formats available in the X11 clipboard to VBox.
67 * @param pCtx Opaque context pointer for the glue code
68 * @param u32Formats The formats available
69 * @note Host glue code
70 */
71void ClipReportX11Formats(VBOXCLIPBOARDCONTEXT *pCtx,
72 uint32_t u32Formats)
73{
74 LogRelFlowFunc(("called. pCtx=%p, u32Formats=%02X\n", pCtx, u32Formats));
75 vboxSvcClipboardReportMsg(pCtx->pClient,
76 VBOX_SHARED_CLIPBOARD_HOST_MSG_FORMATS,
77 u32Formats);
78}
79
80/**
81 * Initialise the host side of the shared clipboard.
82 * @note Host glue code
83 */
84int vboxClipboardInit (void)
85{
86 return VINF_SUCCESS;
87}
88
89/**
90 * Terminate the host side of the shared clipboard.
91 * @note host glue code
92 */
93void vboxClipboardDestroy (void)
94{
95
96}
97
98/**
99 * Connect a guest to the shared clipboard.
100 * @note host glue code
101 * @note on the host, we assume that some other application already owns
102 * the clipboard and leave ownership to X11.
103 */
104int vboxClipboardConnect (VBOXCLIPBOARDCLIENTDATA *pClient)
105{
106 int rc = VINF_SUCCESS;
107 CLIPBACKEND *pBackend = NULL;
108
109 LogRel(("Starting host clipboard service\n"));
110 VBOXCLIPBOARDCONTEXT *pCtx =
111 (VBOXCLIPBOARDCONTEXT *) RTMemAllocZ(sizeof(VBOXCLIPBOARDCONTEXT));
112 if (!pCtx)
113 rc = VERR_NO_MEMORY;
114 else
115 {
116 RTCritSectInit(&pCtx->clipboardMutex);
117 pBackend = ClipConstructX11(pCtx);
118 if (pBackend == NULL)
119 rc = VERR_NO_MEMORY;
120 else
121 {
122 pCtx->pBackend = pBackend;
123 pClient->pCtx = pCtx;
124 pCtx->pClient = pClient;
125 rc = ClipStartX11(pBackend, true /* grab shared clipboard */);
126 }
127 if (RT_FAILURE(rc) && pBackend)
128 ClipStopX11(pCtx->pBackend);
129 if (RT_FAILURE(rc))
130 RTCritSectDelete(&pCtx->clipboardMutex);
131 }
132 if (RT_FAILURE(rc))
133 {
134 RTMemFree(pCtx);
135 LogRel(("Failed to initialise the shared clipboard\n"));
136 }
137 LogRelFlowFunc(("returning %Rrc\n", rc));
138 return rc;
139}
140
141/**
142 * Synchronise the contents of the host clipboard with the guest, called
143 * after a save and restore of the guest.
144 * @note Host glue code
145 */
146int vboxClipboardSync (VBOXCLIPBOARDCLIENTDATA *pClient)
147{
148 /* Tell the guest we have no data in case X11 is not available. If
149 * there is data in the host clipboard it will automatically be sent to
150 * the guest when the clipboard starts up. */
151 vboxSvcClipboardReportMsg (pClient,
152 VBOX_SHARED_CLIPBOARD_HOST_MSG_FORMATS, 0);
153 return VINF_SUCCESS;
154}
155
156/**
157 * Shut down the shared clipboard service and "disconnect" the guest.
158 * @note Host glue code
159 */
160void vboxClipboardDisconnect (VBOXCLIPBOARDCLIENTDATA *pClient)
161{
162 LogRelFlow(("vboxClipboardDisconnect\n"));
163
164 LogRel(("Stopping the host clipboard service\n"));
165 VBOXCLIPBOARDCONTEXT *pCtx = pClient->pCtx;
166 /* Drop the reference to the client, in case it is still there. This
167 * will cause any outstanding clipboard data requests from X11 to fail
168 * immediately. */
169 pCtx->fShuttingDown = true;
170 /* If there is a currently pending request, release it immediately. */
171 vboxClipboardWriteData(pClient, NULL, 0, 0);
172 int rc = ClipStopX11(pCtx->pBackend);
173 /** @todo handle this slightly more reasonably, or be really sure
174 * it won't go wrong. */
175 AssertRC(rc);
176 if (RT_SUCCESS(rc)) /* And if not? */
177 {
178 ClipDestructX11(pCtx->pBackend);
179 RTCritSectDelete(&pCtx->clipboardMutex);
180 RTMemFree(pCtx);
181 }
182}
183
184/**
185 * VBox is taking possession of the shared clipboard.
186 *
187 * @param pClient Context data for the guest system
188 * @param u32Formats Clipboard formats the guest is offering
189 * @note Host glue code
190 */
191void vboxClipboardFormatAnnounce (VBOXCLIPBOARDCLIENTDATA *pClient,
192 uint32_t u32Formats)
193{
194 LogRelFlowFunc(("called. pClient=%p, u32Formats=%02X\n", pClient,
195 u32Formats));
196 ClipAnnounceFormatToX11 (pClient->pCtx->pBackend, u32Formats);
197}
198
199/** Structure describing a request for clipoard data from the guest. */
200struct _CLIPREADCBREQ
201{
202 /** Where to write the returned data to. */
203 void *pv;
204 /** The size of the buffer in pv */
205 uint32_t cb;
206 /** The actual size of the data written */
207 uint32_t *pcbActual;
208};
209
210/**
211 * Called when VBox wants to read the X11 clipboard.
212 *
213 * @returns VINF_SUCCESS on successful completion
214 * @returns VINF_HGCM_ASYNC_EXECUTE if the operation will complete
215 * asynchronously
216 * @returns iprt status code on failure
217 * @param pClient Context information about the guest VM
218 * @param u32Format The format that the guest would like to receive the data in
219 * @param pv Where to write the data to
220 * @param cb The size of the buffer to write the data to
221 * @param pcbActual Where to write the actual size of the written data
222 * @note We always fail or complete asynchronously
223 * @note On success allocates a CLIPREADCBREQ structure which must be
224 * freed in ClipCompleteDataRequestFromX11 when it is called back from
225 * the backend code.
226 *
227 */
228int vboxClipboardReadData (VBOXCLIPBOARDCLIENTDATA *pClient,
229 uint32_t u32Format, void *pv, uint32_t cb,
230 uint32_t *pcbActual)
231{
232 LogRelFlowFunc(("pClient=%p, u32Format=%02X, pv=%p, cb=%u, pcbActual=%p",
233 pClient, u32Format, pv, cb, pcbActual));
234
235 int rc = VINF_SUCCESS;
236 CLIPREADCBREQ *pReq = (CLIPREADCBREQ *) RTMemAlloc(sizeof(CLIPREADCBREQ));
237 if (!pReq)
238 rc = VERR_NO_MEMORY;
239 else
240 {
241 pReq->pv = pv;
242 pReq->cb = cb;
243 pReq->pcbActual = pcbActual;
244 rc = ClipRequestDataFromX11(pClient->pCtx->pBackend, u32Format, pReq);
245 if (RT_SUCCESS(rc))
246 rc = VINF_HGCM_ASYNC_EXECUTE;
247 }
248 LogRelFlowFunc(("returning %Rrc\n", rc));
249 return rc;
250}
251
252/**
253 * Complete a request from VBox for the X11 clipboard data. The data should
254 * be written to the buffer provided in the initial request.
255 * @param pCtx request context information
256 * @param rc the completion status of the request
257 * @param cbActual on successful completion, the number of bytes of data
258 * actually written, on buffer overflow the size of the
259 * buffer needed, ignored otherwise
260 * @todo change this to deal with the buffer issues rather than offloading
261 * them onto the caller
262 */
263void ClipCompleteDataRequestFromX11(VBOXCLIPBOARDCONTEXT *pCtx, int rc,
264 CLIPREADCBREQ *pReq, void *pv,
265 uint32_t cb)
266{
267 if (cb <= pReq->cb)
268 memcpy(pReq->pv, pv, cb);
269 RTMemFree(pReq);
270 vboxSvcClipboardCompleteReadData(pCtx->pClient, rc, cb);
271}
272
273/** A request for clipboard data from VBox */
274struct _VBOXCLIPBOARDREQFROMVBOX
275{
276 /** Data received */
277 void *pv;
278 /** The size of the data */
279 uint32_t cb;
280 /** Format of the data */
281 uint32_t format;
282 /** A semaphore for waiting for the data */
283 RTSEMEVENT finished;
284};
285
286/** Wait for clipboard data requested from VBox to arrive. */
287static int clipWaitForDataFromVBox(VBOXCLIPBOARDCONTEXT *pCtx,
288 VBOXCLIPBOARDREQFROMVBOX *pReq,
289 uint32_t u32Format)
290{
291 int rc = VINF_SUCCESS;
292 LogRelFlowFunc(("pCtx=%p, pReq=%p, u32Format=%02X\n", pCtx, pReq, u32Format));
293 /* Request data from VBox */
294 vboxSvcClipboardReportMsg(pCtx->pClient,
295 VBOX_SHARED_CLIPBOARD_HOST_MSG_READ_DATA,
296 u32Format);
297 /* Which will signal us when it is ready. We use a timeout here
298 * because we can't be sure that the guest will behave correctly.
299 */
300 rc = RTSemEventWait(pReq->finished, CLIPBOARD_TIMEOUT);
301 /* If the request hasn't yet completed then we cancel it. We use
302 * the critical section to prevent these operations colliding. */
303 RTCritSectEnter(&pCtx->clipboardMutex);
304 /* The data may have arrived between the semaphore timing out and
305 * our grabbing the mutex. */
306 if (rc == VERR_TIMEOUT && pReq->pv != NULL)
307 rc = VINF_SUCCESS;
308 if (pCtx->pReq == pReq)
309 pCtx->pReq = NULL;
310 Assert(pCtx->pReq == NULL);
311 RTCritSectLeave(&pCtx->clipboardMutex);
312 if (RT_SUCCESS(rc) && (pReq->pv == NULL))
313 rc = VERR_NO_DATA;
314 LogRelFlowFunc(("returning %Rrc\n", rc));
315 return rc;
316}
317
318/** Post a request for clipboard data to VBox/the guest and wait for it to be
319 * completed. */
320static int clipRequestDataFromVBox(VBOXCLIPBOARDCONTEXT *pCtx,
321 VBOXCLIPBOARDREQFROMVBOX *pReq,
322 uint32_t u32Format)
323{
324 int rc = VINF_SUCCESS;
325 LogRelFlowFunc(("pCtx=%p, pReq=%p, u32Format=%02X\n", pCtx, pReq,
326 u32Format));
327 /* Start by "posting" the request for the next invocation of
328 * vboxClipboardWriteData. */
329 RTCritSectEnter(&pCtx->clipboardMutex);
330 if (pCtx->pReq != NULL)
331 {
332 /* This would be a violation of the protocol, see the comments in the
333 * context structure definition. */
334 Assert(false);
335 rc = VERR_WRONG_ORDER;
336 }
337 else
338 pCtx->pReq = pReq;
339 RTCritSectLeave(&pCtx->clipboardMutex);
340 if (RT_SUCCESS(rc))
341 rc = clipWaitForDataFromVBox(pCtx, pReq, u32Format);
342 LogRelFlowFunc(("returning %Rrc\n", rc));
343 return rc;
344}
345
346/**
347 * Send a request to VBox to transfer the contents of its clipboard to X11.
348 *
349 * @param pCtx Pointer to the host clipboard structure
350 * @param u32Format The format in which the data should be transfered
351 * @param ppv On success and if pcb > 0, this will point to a buffer
352 * to be freed with RTMemFree containing the data read.
353 * @param pcb On success, this contains the number of bytes of data
354 * returned
355 * @note Host glue code.
356 */
357int ClipRequestDataForX11 (VBOXCLIPBOARDCONTEXT *pCtx,
358 uint32_t u32Format, void **ppv,
359 uint32_t *pcb)
360{
361 VBOXCLIPBOARDREQFROMVBOX request = { NULL };
362
363 LogRelFlowFunc(("pCtx=%p, u32Format=%02X, ppv=%p, pcb=%p\n", pCtx,
364 u32Format, ppv, pcb));
365 if (pCtx->fShuttingDown)
366 {
367 /* The shared clipboard is disconnecting. */
368 LogRelFunc(("host requested guest clipboard data after guest had disconnected.\n"));
369 return VERR_WRONG_ORDER;
370 }
371 int rc = RTSemEventCreate(&request.finished);
372 if (RT_SUCCESS(rc))
373 {
374 rc = clipRequestDataFromVBox(pCtx, &request, u32Format);
375 RTSemEventDestroy(request.finished);
376 }
377 if (RT_SUCCESS(rc))
378 {
379 *ppv = request.pv;
380 *pcb = request.cb;
381 }
382 LogRelFlowFunc(("returning %Rrc\n", rc));
383 if (RT_SUCCESS(rc))
384 LogRelFlowFunc(("*ppv=%.*ls, *pcb=%u\n", *pcb / 2, *ppv, *pcb));
385 return rc;
386}
387
388/**
389 * Called when we have requested data from VBox and that data has arrived.
390 *
391 * @param pClient Context information about the guest VM
392 * @param pv Buffer to which the data was written
393 * @param cb The size of the data written
394 * @param u32Format The format of the data written
395 * @note Host glue code
396 */
397void vboxClipboardWriteData (VBOXCLIPBOARDCLIENTDATA *pClient,
398 void *pv, uint32_t cb, uint32_t u32Format)
399{
400 LogRelFlowFunc (("called. pClient=%p, pv=%p (%.*ls), cb=%u, u32Format=%02X\n",
401 pClient, pv, cb / 2, pv, cb, u32Format));
402
403 VBOXCLIPBOARDCONTEXT *pCtx = pClient->pCtx;
404 /* Grab the mutex and check whether there is a pending request for data.
405 */
406 RTCritSectEnter(&pCtx->clipboardMutex);
407 VBOXCLIPBOARDREQFROMVBOX *pReq = pCtx->pReq;
408 if (pReq != NULL)
409 {
410 if (cb > 0)
411 {
412 pReq->pv = RTMemDup(pv, cb);
413 if (pReq->pv != NULL) /* NULL may also mean no memory... */
414 {
415 pReq->cb = cb;
416 pReq->format = u32Format;
417 }
418 }
419 /* Signal that the request has been completed. */
420 RTSemEventSignal(pReq->finished);
421 pCtx->pReq = NULL;
422 }
423 RTCritSectLeave(&pCtx->clipboardMutex);
424}
425
426#ifdef TESTCASE
427#include <iprt/initterm.h>
428#include <iprt/stream.h>
429
430#define TEST_NAME "tstClipboardX11-2"
431
432struct _CLIPBACKEND
433{
434 uint32_t formats;
435 struct _READDATA
436 {
437 uint32_t format;
438 int rc;
439 CLIPREADCBREQ *pReq;
440 } readData;
441 struct _COMPLETEREAD
442 {
443 int rc;
444 uint32_t cbActual;
445 } completeRead;
446 struct _WRITEDATA
447 {
448 void *pv;
449 uint32_t cb;
450 uint32_t format;
451 bool timeout;
452 } writeData;
453 struct _REPORTDATA
454 {
455 uint32_t format;
456 } reportData;
457};
458
459void vboxSvcClipboardReportMsg (VBOXCLIPBOARDCLIENTDATA *pClient, uint32_t u32Msg, uint32_t u32Formats)
460{
461 CLIPBACKEND *pBackend = pClient->pCtx->pBackend;
462 if ( (u32Msg == VBOX_SHARED_CLIPBOARD_HOST_MSG_READ_DATA)
463 && !pBackend->writeData.timeout)
464 vboxClipboardWriteData(pClient, pBackend->writeData.pv,
465 pBackend->writeData.cb,
466 pBackend->writeData.format);
467 else
468 return;
469}
470
471void vboxSvcClipboardCompleteReadData(VBOXCLIPBOARDCLIENTDATA *pClient, int rc, uint32_t cbActual)
472{
473 CLIPBACKEND *pBackend = pClient->pCtx->pBackend;
474 pBackend->completeRead.rc = rc;
475 pBackend->completeRead.cbActual = cbActual;
476}
477
478CLIPBACKEND *ClipConstructX11(VBOXCLIPBOARDCONTEXT *pFrontend)
479{
480 return (CLIPBACKEND *)RTMemAllocZ(sizeof(CLIPBACKEND));
481}
482
483void ClipDestructX11(CLIPBACKEND *pBackend)
484{
485 RTMemFree(pBackend);
486}
487
488int ClipStartX11(CLIPBACKEND *pBackend, bool)
489{
490 return VINF_SUCCESS;
491}
492
493int ClipStopX11(CLIPBACKEND *pBackend)
494{
495 return VINF_SUCCESS;
496}
497
498void ClipAnnounceFormatToX11(CLIPBACKEND *pBackend,
499 uint32_t u32Formats)
500{
501 pBackend->formats = u32Formats;
502}
503
504extern int ClipRequestDataFromX11(CLIPBACKEND *pBackend, uint32_t u32Format,
505 CLIPREADCBREQ *pReq)
506{
507 pBackend->readData.format = u32Format;
508 pBackend->readData.pReq = pReq;
509 return pBackend->readData.rc;
510}
511
512int main()
513{
514 VBOXCLIPBOARDCLIENTDATA client;
515 unsigned cErrors = 0;
516 int rc = RTR3Init();
517 RTPrintf(TEST_NAME ": TESTING\n");
518 AssertRCReturn(rc, 1);
519 rc = vboxClipboardConnect(&client);
520 CLIPBACKEND *pBackend = client.pCtx->pBackend;
521 AssertRCReturn(rc, 1);
522 vboxClipboardFormatAnnounce(&client,
523 VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT);
524 if (pBackend->formats != VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT)
525 {
526 RTPrintf(TEST_NAME ": vboxClipboardFormatAnnounce failed with VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT\n");
527 ++cErrors;
528 }
529 pBackend->readData.rc = VINF_SUCCESS;
530 client.asyncRead.callHandle = (VBOXHGCMCALLHANDLE)pBackend;
531 client.asyncRead.paParms = (VBOXHGCMSVCPARM *)&client;
532 uint32_t u32Dummy;
533 rc = vboxClipboardReadData(&client, VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT,
534 &u32Dummy, 42, &u32Dummy);
535 if (rc != VINF_HGCM_ASYNC_EXECUTE)
536 {
537 RTPrintf(TEST_NAME ": vboxClipboardReadData returned %Rrc\n", rc);
538 ++cErrors;
539 }
540 else
541 {
542 if ( pBackend->readData.format !=
543 VBOX_SHARED_CLIPBOARD_FMT_UNICODETEXT
544 || pBackend->readData.pReq->pv != &u32Dummy
545 || pBackend->readData.pReq->cb != 42
546 || pBackend->readData.pReq->pcbActual != &u32Dummy)
547 {
548 RTPrintf(TEST_NAME ": format=%u, pReq->pv=%p, pReq->cb=%u, pReq->pcbActual=%p\n",
549 pBackend->readData.format, pBackend->readData.pReq->pv,
550 pBackend->readData.pReq->cb,
551 pBackend->readData.pReq->pcbActual);
552 ++cErrors;
553 }
554 else
555 {
556 ClipCompleteDataRequestFromX11(client.pCtx, VERR_NO_DATA,
557 pBackend->readData.pReq, NULL, 43);
558 if ( pBackend->completeRead.rc != VERR_NO_DATA
559 || pBackend->completeRead.cbActual != 43)
560 {
561 RTPrintf(TEST_NAME ": rc=%Rrc, cbActual=%u\n",
562 pBackend->completeRead.rc,
563 pBackend->completeRead.cbActual);
564 ++cErrors;
565 }
566 }
567 }
568 void *pv;
569 uint32_t cb;
570 pBackend->writeData.pv = (void *)"testing";
571 pBackend->writeData.cb = sizeof("testing");
572 pBackend->writeData.format = 1234;
573 pBackend->reportData.format = 4321; /* XX this should be handled! */
574 rc = ClipRequestDataForX11(client.pCtx, 23, &pv, &cb);
575 if ( rc != VINF_SUCCESS
576 || strcmp((const char *)pv, "testing") != 0
577 || cb != sizeof("testing"))
578 {
579 RTPrintf("rc=%Rrc, pv=%p, cb=%u\n", rc, pv, cb);
580 ++cErrors;
581 }
582 else
583 RTMemFree(pv);
584 pBackend->writeData.timeout = true;
585 rc = ClipRequestDataForX11(client.pCtx, 23, &pv, &cb);
586 if (rc != VERR_TIMEOUT)
587 {
588 RTPrintf("rc=%Rrc, expected VERR_TIMEOUT\n", rc);
589 ++cErrors;
590 }
591 pBackend->writeData.pv = NULL;
592 pBackend->writeData.cb = 0;
593 pBackend->writeData.timeout = false;
594 rc = ClipRequestDataForX11(client.pCtx, 23, &pv, &cb);
595 if (rc != VERR_NO_DATA)
596 {
597 RTPrintf("rc=%Rrc, expected VERR_NO_DATA\n", rc);
598 ++cErrors;
599 }
600 /* Data arriving after a timeout should *not* cause any segfaults or
601 * memory leaks. Check with Valgrind! */
602 vboxClipboardWriteData(&client, (void *)"tested", sizeof("tested"), 999);
603 vboxClipboardDisconnect(&client);
604 if (cErrors > 0)
605 RTPrintf(TEST_NAME ": errors: %u\n", cErrors);
606 return cErrors > 0 ? 1 : 0;
607}
608#endif /* TESTCASE */
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette