VirtualBox

source: vbox/trunk/src/VBox/Debugger/DBGCIo.cpp@ 86327

Last change on this file since 86327 was 86327, checked in by vboxsync, 4 years ago

Debugger: Allow for different I/O providers instead of only TCP

So far TCP was the only option to communicate remotely with the internal debugger, the other option
was to use the console from the GUI directly. This commit reworks basic I/O to allow for different
providers where TCP is just one option. The second one being introduced is an IPC provider using a local
socket or named pipe depending on the platform. This allows for Windows kernel debugging over a pipe
using the KD stub in VirtualBox and WinDbg running on the host (not tested yet).

Furthermore this commit allows multiple stubs to be listening for connections at the same time, so
one can have a GDB stub listening on one TCP port and the native VBox debugger listening on another one
or even using a different I/O provider. Only one session can be active at a time though, because sharing
debugger states is impossible. To configure this the following CFGM keys need to be set for each listener:

"DBGC/<Some unique ID>/Provider" "tcp|ipc"
"DBGC/<Some unique ID>/StubType" "native|gdb|kd"
"DBGC/<Some unique ID>/Address" "<ip>|<local named pipe or socket path>"
"DBGC/<Some unique ID>/Port" "<port>" (for TCP only)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 18.6 KB
Line 
1/* $Id: DBGCIo.cpp 86327 2020-09-28 16:20:50Z vboxsync $ */
2/** @file
3 * DBGC - Debugger Console, I/O provider handling.
4 */
5
6/*
7 * Copyright (C) 2020 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 */
17
18
19/*********************************************************************************************************************************
20* Header Files *
21*********************************************************************************************************************************/
22#include <VBox/dbg.h>
23#include <VBox/vmm/cfgm.h>
24#include <VBox/err.h>
25
26#include <iprt/mem.h>
27#include <iprt/thread.h>
28#include <VBox/log.h>
29#include <iprt/assert.h>
30
31#include <iprt/string.h>
32
33#include "DBGCIoProvInternal.h"
34#include "DBGCInternal.h"
35
36
37/*********************************************************************************************************************************
38* Structures and Typedefs *
39*********************************************************************************************************************************/
40
41/**
42 * Stub descriptor.
43 */
44typedef struct DBGCSTUB
45{
46 /** Name of the stub. */
47 const char *pszName;
48 /** Flag whether this is an ASCII based protocol which requires some newline handling. */
49 bool fAscii;
50 /**
51 * The runloop callback.
52 *
53 * @returns VBox status code.
54 * @param pUVM The user mode VM handle.
55 * @param pIo Pointer to the I/O callback table.
56 * @param fFlags Flags for the runloop, MBZ for now.
57 */
58 DECLCALLBACKMEMBER(int, pfnRunloop, (PUVM pUVM, PCDBGCIO pIo, unsigned fFlags));
59} DBGCSTUB;
60/** Pointer to a stub descriptor. */
61typedef DBGCSTUB *PDBGCSTUB;
62/** Pointer to a const stub descriptor. */
63typedef const DBGCSTUB *PCDBGCSTUB;
64
65
66/** Pointer to the instance data of the debug console I/O. */
67typedef struct DBGCIOINT *PDBGCIOINT;
68
69
70/**
71 * A single debug console I/O service.
72 */
73typedef struct DBGCIOSVC
74{
75 /** Pointer to the owning structure. */
76 PDBGCIOINT pDbgcIo;
77 /** The user mode VM handle this service belongs to. */
78 PUVM pUVM;
79 /** The I/O provider registration record for this service. */
80 PCDBGCIOPROVREG pIoProvReg;
81 /** The I/O provider instance. */
82 DBGCIOPROV hDbgcIoProv;
83 /** The stub type. */
84 PCDBGCSTUB pStub;
85 /** The thread managing the service. */
86 RTTHREAD hThreadSvc;
87 /** Pointer to the I/O callback table currently being served. */
88 PCDBGCIO pIo;
89 /** The wrapping DBGC I/O callback table for ASCII based protocols. */
90 DBGCIO IoAscii;
91} DBGCIOSVC;
92/** Pointer to a single debug console I/O service. */
93typedef DBGCIOSVC *PDBGCIOSVC;
94/** Poitner to a const single debug console I/O service. */
95typedef const DBGCIOSVC *PCDBGCIOSVC;
96
97
98/**
99 * Debug console I/O instance data.
100 */
101typedef struct DBGCIOINT
102{
103 /** Number of configured I/O service instances. */
104 volatile uint32_t cSvcsCfg;
105 /** Number of running I/O service instances. */
106 volatile uint32_t cSvcsRunning;
107 /** Flag whether the services were asked to shut down. */
108 volatile bool fShutdown;
109 /** Array of active I/O service instances. */
110 RT_FLEXIBLE_ARRAY_EXTENSION
111 DBGCIOSVC aSvc[RT_FLEXIBLE_ARRAY];
112} DBGCIOINT;
113
114
115/*********************************************************************************************************************************
116* Global Variables *
117*********************************************************************************************************************************/
118
119/**
120 * Array of supported I/O providers.
121 */
122static PCDBGCIOPROVREG g_aIoProv[] =
123{
124 &g_DbgcIoProvTcp,
125 &g_DbgcIoProvIpc
126};
127
128
129static DECLCALLBACK(int) dbgcIoNativeStubRunloop(PUVM pUVM, PCDBGCIO pIo, unsigned fFlags);
130
131/**
132 * Array of supported stubs.
133 */
134static const DBGCSTUB g_aStubs[] =
135{
136 /** pszName fAscii pfnRunloop */
137 { "Native", true, dbgcIoNativeStubRunloop },
138 { "Gdb", false, dbgcGdbStubRunloop },
139 { "Kd", false, dbgcKdStubRunloop }
140};
141
142
143/*********************************************************************************************************************************
144* Internal Functions *
145*********************************************************************************************************************************/
146
147
148/**
149 * Destroys all allocated data for the given dbeugger console I/O instance.
150 *
151 * @returns nothing.
152 * @param pDbgcIo Pointer to the dbeugger console I/O instance data.
153 */
154static void dbgcIoDestroy(PDBGCIOINT pDbgcIo)
155{
156 for (uint32_t i = 0; i < pDbgcIo->cSvcsCfg; i++)
157 {
158 PDBGCIOSVC pIoSvc = &pDbgcIo->aSvc[i];
159
160 if (pIoSvc->hThreadSvc != NIL_RTTHREAD)
161 {
162 int rc = RTThreadWait(pIoSvc->hThreadSvc, RT_MS_10SEC, NULL /*prc*/);
163 AssertRC(rc);
164
165 pIoSvc->hThreadSvc = NIL_RTTHREAD;
166 pIoSvc->pIoProvReg->pfnDestroy(pIoSvc->hDbgcIoProv);
167 }
168 }
169
170 RTMemFree(pDbgcIo);
171}
172
173
174/**
175 * Returns the number of I/O services configured.
176 *
177 * @returns I/O service count.
178 * @param pCfgRoot The root of the config.
179 */
180static uint32_t dbgcIoGetSvcCount(PCFGMNODE pCfgRoot)
181{
182 uint32_t cSvcs = 0;
183 PCFGMNODE pNd = CFGMR3GetFirstChild(pCfgRoot);
184 while (pNd)
185 {
186 cSvcs++;
187 pNd = CFGMR3GetNextChild(pNd);
188 }
189
190 return cSvcs;
191}
192
193
194/**
195 * Returns a pointer to the I/O provider registration record matching the given name.
196 *
197 * @returns Pointer to the registration record or NULL if not found.
198 * @param pszName The name to look for (case insensitive matching).
199 */
200static PCDBGCIOPROVREG dbgcIoProvFindRegByName(const char *pszName)
201{
202 for (uint32_t i = 0; i < RT_ELEMENTS(g_aIoProv); i++)
203 {
204 if (!RTStrICmp(g_aIoProv[i]->pszName, pszName))
205 return g_aIoProv[i];
206 }
207 return NULL;
208}
209
210
211/**
212 * Returns a pointer to the stub record matching the given name.
213 *
214 * @returns Pointer to the stub record or NULL if not found.
215 * @param pszName The name to look for (case insensitive matching).
216 */
217static PCDBGCSTUB dbgcIoFindStubByName(const char *pszName)
218{
219 for (uint32_t i = 0; i < RT_ELEMENTS(g_aStubs); i++)
220 {
221 if (!RTStrICmp(g_aStubs[i].pszName, pszName))
222 return &g_aStubs[i];
223 }
224 return NULL;
225}
226
227
228/**
229 * Wrapper around DBGCCreate() to get it working as a callback.
230 */
231static DECLCALLBACK(int) dbgcIoNativeStubRunloop(PUVM pUVM, PCDBGCIO pIo, unsigned fFlags)
232{
233 return DBGCCreate(pUVM, pIo, fFlags);
234}
235
236
237/**
238 * @interface_method_impl{DBGCIO,pfnDestroy}
239 */
240static DECLCALLBACK(void) dbgcIoAsciiDestroy(PCDBGCIO pIo)
241{
242 PDBGCIOSVC pIoSvc = RT_FROM_MEMBER(pIo, DBGCIOSVC, IoAscii);
243 pIoSvc->pIo->pfnDestroy(pIoSvc->pIo);
244}
245
246
247/**
248 * @interface_method_impl{DBGCIO,pfnInput}
249 */
250static DECLCALLBACK(bool) dbgcIoAsciiInput(PCDBGCIO pIo, uint32_t cMillies)
251{
252 PDBGCIOSVC pIoSvc = RT_FROM_MEMBER(pIo, DBGCIOSVC, IoAscii);
253 return pIoSvc->pIo->pfnInput(pIoSvc->pIo, cMillies);
254}
255
256
257/**
258 * @interface_method_impl{DBGCIO,pfnRead}
259 */
260static DECLCALLBACK(int) dbgcIoAsciiRead(PCDBGCIO pIo, void *pvBuf, size_t cbBuf, size_t *pcbRead)
261{
262 PDBGCIOSVC pIoSvc = RT_FROM_MEMBER(pIo, DBGCIOSVC, IoAscii);
263 return pIoSvc->pIo->pfnRead(pIoSvc->pIo, pvBuf, cbBuf, pcbRead);
264}
265
266
267/**
268 * @interface_method_impl{DBGCIO,pfnWrite}
269 */
270static DECLCALLBACK(int) dbgcIoAsciiWrite(PCDBGCIO pIo, const void *pvBuf, size_t cbBuf, size_t *pcbWritten)
271{
272 PDBGCIOSVC pIoSvc = RT_FROM_MEMBER(pIo, DBGCIOSVC, IoAscii);
273
274 /*
275 * convert '\n' to '\r\n' while writing.
276 */
277 int rc = 0;
278 size_t cbLeft = cbBuf;
279 while (cbLeft)
280 {
281 size_t cb = cbLeft;
282 /* write newlines */
283 if (*(const char *)pvBuf == '\n')
284 {
285 rc = pIoSvc->pIo->pfnWrite(pIoSvc->pIo, "\r\n", 2, NULL);
286 cb = 1;
287 }
288 /* write till next newline */
289 else
290 {
291 const char *pszNL = (const char *)memchr(pvBuf, '\n', cbLeft);
292 if (pszNL)
293 cb = (uintptr_t)pszNL - (uintptr_t)pvBuf;
294 rc = pIoSvc->pIo->pfnWrite(pIoSvc->pIo, pvBuf, cb, NULL);
295 }
296 if (RT_FAILURE(rc))
297 break;
298
299 /* advance */
300 cbLeft -= cb;
301 pvBuf = (const char *)pvBuf + cb;
302 }
303
304 /*
305 * Set returned value and return.
306 */
307 if (pcbWritten)
308 *pcbWritten = cbBuf - cbLeft;
309 return rc;
310}
311
312
313/**
314 * @interface_method_impl{DBGCIO,pfnSetReady}
315 */
316static DECLCALLBACK(void) dbgcIoAsciiSetReady(PCDBGCIO pIo, bool fReady)
317{
318 PDBGCIOSVC pIoSvc = RT_FROM_MEMBER(pIo, DBGCIOSVC, IoAscii);
319 return pIoSvc->pIo->pfnSetReady(pIoSvc->pIo, fReady);
320}
321
322
323/**
324 * The I/O thread handling the service.
325 */
326static DECLCALLBACK(int) dbgcIoSvcThread(RTTHREAD hThreadSelf, void *pvUser)
327{
328 RT_NOREF(hThreadSelf);
329
330 int rc = VINF_SUCCESS;
331 PDBGCIOSVC pIoSvc = (PDBGCIOSVC)pvUser;
332 PDBGCIOINT pDbgcIo = pIoSvc->pDbgcIo;
333 PCDBGCIOPROVREG pIoProvReg = pIoSvc->pIoProvReg;
334
335 while (!ASMAtomicReadBool(&pDbgcIo->fShutdown))
336 {
337 /* Wait until someone connects. */
338 rc = pIoProvReg->pfnWaitForConnect(pIoSvc->hDbgcIoProv, RT_INDEFINITE_WAIT, &pIoSvc->pIo);
339 if (RT_SUCCESS(rc))
340 {
341 PCDBGCIO pIo = pIoSvc->pIo;
342
343 if (pIoSvc->pStub->fAscii)
344 {
345 pIoSvc->IoAscii.pfnDestroy = dbgcIoAsciiDestroy;
346 pIoSvc->IoAscii.pfnInput = dbgcIoAsciiInput;
347 pIoSvc->IoAscii.pfnRead = dbgcIoAsciiRead;
348 pIoSvc->IoAscii.pfnWrite = dbgcIoAsciiWrite;
349 pIoSvc->IoAscii.pfnSetReady = dbgcIoAsciiSetReady;
350 pIo = &pIoSvc->IoAscii;
351 }
352
353 /* call the runloop for the connection. */
354 pIoSvc->pStub->pfnRunloop(pIoSvc->pUVM, pIo, 0 /*fFlags*/);
355
356 pIo->pfnDestroy(pIo);
357 }
358 else if ( rc != VERR_TIMEOUT
359 && rc != VERR_INTERRUPTED)
360 break;
361 }
362
363 if (!ASMAtomicDecU32(&pDbgcIo->cSvcsRunning))
364 dbgcIoDestroy(pDbgcIo);
365
366 return rc;
367}
368
369
370static int dbgcIoSvcInitWorker(PUVM pUVM, PDBGCIOSVC pIoSvc, PCDBGCIOPROVREG pIoProvReg,
371 PCDBGCSTUB pStub, PCFGMNODE pCfg, const char *pszName,
372 bool fIgnoreNetAddrInUse)
373{
374 pIoSvc->pUVM = pUVM;
375 pIoSvc->pIoProvReg = pIoProvReg;
376 pIoSvc->pStub = pStub;
377
378 /* Create the provider instance and spawn the dedicated thread handling that service. */
379 int rc = pIoProvReg->pfnCreate(&pIoSvc->hDbgcIoProv, pCfg);
380 if (RT_SUCCESS(rc))
381 {
382 rc = RTThreadCreateF(&pIoSvc->hThreadSvc, dbgcIoSvcThread, pIoSvc, 0 /*cbStack*/,
383 RTTHREADTYPE_DEBUGGER, RTTHREADFLAGS_WAITABLE, "DbgcThrd-%s", pszName);
384 if (RT_SUCCESS(rc))
385 {
386 ASMAtomicIncU32(&pIoSvc->pDbgcIo->cSvcsRunning);
387 return VINF_SUCCESS;
388 }
389 else
390 rc = VMR3SetError(pUVM, rc, RT_SRC_POS,
391 "Configuration error: Creating an instance of the service \"%s\" failed",
392 pszName);
393
394 pIoProvReg->pfnDestroy(pIoSvc->hDbgcIoProv);
395 }
396 else if ( rc != VERR_NET_ADDRESS_IN_USE
397 || !fIgnoreNetAddrInUse)
398 rc = VMR3SetError(pUVM, rc, RT_SRC_POS,
399 "Configuration error: Creating an instance of the I/O provider \"%s\" failed",
400 pIoProvReg->pszName);
401
402 return rc;
403}
404
405
406/**
407 * Tries to initialize the given I/O service from the given config.
408 *
409 * @returns VBox status code.
410 * @param pUVM The user mode VM handle.
411 */
412static int dbgcIoSvcInit(PUVM pUVM, PDBGCIOSVC pIoSvc, PCFGMNODE pCfg)
413{
414 char szName[32 + 1]; RT_ZERO(szName);
415 int rc = CFGMR3GetName(pCfg, &szName[0], sizeof(szName));
416 if (RT_SUCCESS(rc))
417 {
418 char szIoProvName[32 + 1]; RT_ZERO(szIoProvName);
419 rc = CFGMR3QueryString(pCfg, "Provider", &szIoProvName[0], sizeof(szIoProvName));
420 if (RT_SUCCESS(rc))
421 {
422 char szStub[32 + 1]; RT_ZERO(szStub);
423 rc = CFGMR3QueryString(pCfg, "StubType", &szStub[0], sizeof(szStub));
424 if (RT_SUCCESS(rc))
425 {
426 PCDBGCIOPROVREG pIoProvReg = dbgcIoProvFindRegByName(szIoProvName);
427 if (pIoProvReg)
428 {
429 PCDBGCSTUB pStub = dbgcIoFindStubByName(szStub);
430 if (pStub)
431 rc = dbgcIoSvcInitWorker(pUVM, pIoSvc, pIoProvReg, pStub, pCfg, szName,
432 false /*fIgnoreNetAddrInUse*/);
433 else
434 rc = VMR3SetError(pUVM, VERR_NOT_FOUND, RT_SRC_POS, "Configuration error: The stub type \"%s\" could not be found",
435 szStub);
436 }
437 else
438 rc = VMR3SetError(pUVM, VERR_NOT_FOUND, RT_SRC_POS, "Configuration error: The provider \"%s\" could not be found",
439 szIoProvName);
440 }
441 else
442 rc = VM_SET_ERROR_U(pUVM, rc, "Configuration error: Querying \"StubType\" failed");
443 }
444 else
445 rc = VM_SET_ERROR_U(pUVM, rc, "Configuration error: Querying \"Provider\" failed");
446 }
447 else
448 rc = VM_SET_ERROR_U(pUVM, rc, "Configuration error: Querying service identifier failed (maybe too long)");
449
450 return rc;
451}
452
453
454/**
455 * Creates the DBGC I/O services from the legacy TCP config.
456 *
457 * @returns VBox status code.
458 * @param pUVM The user mode VM handle.
459 * @param pKey The config key.
460 * @param ppvData Where to store the I/o instance data on success.
461 */
462static int dbgcIoCreateLegacyTcp(PUVM pUVM, PCFGMNODE pKey, void **ppvData)
463{
464 bool fEnabled;
465 int rc = CFGMR3QueryBoolDef(pKey, "Enabled", &fEnabled,
466#if defined(VBOX_WITH_DEBUGGER) && defined(VBOX_WITH_DEBUGGER_TCP_BY_DEFAULT)
467 true
468#else
469 false
470#endif
471 );
472 if (RT_FAILURE(rc))
473 return VM_SET_ERROR_U(pUVM, rc, "Configuration error: Failed querying \"DBGC/Enabled\"");
474
475 if (!fEnabled)
476 {
477 LogFlow(("DBGCTcpCreate: returns VINF_SUCCESS (Disabled)\n"));
478 return VINF_SUCCESS;
479 }
480
481 PDBGCIOINT pDbgcIo = (PDBGCIOINT)RTMemAllocZ(RT_UOFFSETOF_DYN(DBGCIOINT, aSvc[1]));
482 if (RT_LIKELY(pDbgcIo))
483 {
484 pDbgcIo->aSvc[0].pDbgcIo = pDbgcIo;
485 pDbgcIo->cSvcsCfg = 1;
486 pDbgcIo->cSvcsRunning = 1;
487 rc = dbgcIoSvcInitWorker(pUVM, &pDbgcIo->aSvc[0], &g_DbgcIoProvTcp, &g_aStubs[0], pKey, "TCP",
488 true /*fIgnoreNetAddrInUse*/);
489 if (RT_SUCCESS(rc))
490 {
491 *ppvData = pDbgcIo;
492 return VINF_SUCCESS;
493 }
494
495 RTMemFree(pDbgcIo);
496 if (rc == VERR_NET_ADDRESS_IN_USE)
497 rc = VINF_SUCCESS;
498 }
499 else
500 rc = VERR_NO_MEMORY;
501
502 if (RT_FAILURE(rc))
503 rc = VM_SET_ERROR_U(pUVM, rc, "Cannot start TCP-based debugging console service");
504 return rc;
505}
506
507
508/**
509 * Sets up debugger I/O based on the VM config.
510 *
511 * @returns VBox status code.
512 * @param pUVM The user mode VM handle.
513 * @param ppvData Where to store a pointer to the instance data.
514 */
515DBGDECL(int) DBGCIoCreate(PUVM pUVM, void **ppvData)
516{
517 /*
518 * Check what the configuration says.
519 */
520 PCFGMNODE pKey = CFGMR3GetChild(CFGMR3GetRootU(pUVM), "DBGC");
521 uint32_t cSvcs = dbgcIoGetSvcCount(pKey);
522 int rc = VINF_SUCCESS;
523
524 /* If no services are configured try the legacy config supporting TCP only. */
525 if (cSvcs)
526 {
527 PDBGCIOINT pDbgcIo = (PDBGCIOINT)RTMemAllocZ(RT_UOFFSETOF_DYN(DBGCIOINT, aSvc[cSvcs]));
528 if (RT_LIKELY(pDbgcIo))
529 {
530 pDbgcIo->cSvcsCfg = 0;
531 pDbgcIo->cSvcsRunning = 1;
532 pDbgcIo->fShutdown = false;
533
534 for (uint32_t i = 0; i < cSvcs; i++)
535 pDbgcIo->aSvc[i].hThreadSvc = NIL_RTTHREAD;
536
537 PCFGMNODE pSvcCfg = CFGMR3GetFirstChild(pKey);
538 for (uint32_t i = 0; i < cSvcs && RT_SUCCESS(rc); i++)
539 {
540 pDbgcIo->aSvc[i].pDbgcIo = pDbgcIo;
541
542 rc = dbgcIoSvcInit(pUVM, &pDbgcIo->aSvc[i], pSvcCfg);
543 if (RT_SUCCESS(rc))
544 pDbgcIo->cSvcsCfg++;
545 else
546 rc = VM_SET_ERROR_U(pUVM, rc, "Failed to initialize the debugger I/O service");
547
548 pSvcCfg = CFGMR3GetNextChild(pSvcCfg);
549 }
550
551 if (RT_SUCCESS(rc))
552 *ppvData = pDbgcIo;
553 else
554 {
555 if (!ASMAtomicDecU32(&pDbgcIo->cSvcsRunning))
556 dbgcIoDestroy(pDbgcIo);
557 }
558 }
559 else
560 rc = VM_SET_ERROR_U(pUVM, VERR_NO_MEMORY, "Failed to allocate memory for the debugger I/O service");
561 }
562 else
563 rc = dbgcIoCreateLegacyTcp(pUVM, pKey, ppvData);
564
565 return rc;
566}
567
568
569/**
570 * Terminates any running debugger services.
571 *
572 * @returns VBox status code.
573 * @param pUVM The user mode VM handle.
574 * @param pvData The data returned by DBGCIoCreate.
575 */
576DBGDECL(int) DBGCIoTerminate(PUVM pUVM, void *pvData)
577{
578 RT_NOREF(pUVM);
579 PDBGCIOINT pDbgcIo = (PDBGCIOINT)pvData;
580
581 if (pDbgcIo)
582 {
583 ASMAtomicXchgBool(&pDbgcIo->fShutdown, true);
584
585 for (uint32_t i = 0; i < pDbgcIo->cSvcsCfg; i++)
586 {
587 PDBGCIOSVC pIoSvc = &pDbgcIo->aSvc[i];
588
589 if (pIoSvc->hThreadSvc != NIL_RTTHREAD)
590 pIoSvc->pIoProvReg->pfnWaitInterrupt(pIoSvc->hDbgcIoProv);
591 }
592
593 if (!ASMAtomicDecU32(&pDbgcIo->cSvcsRunning))
594 dbgcIoDestroy(pDbgcIo);
595 }
596
597 return VINF_SUCCESS;
598}
599
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