VirtualBox

source: vbox/trunk/src/VBox/Main/src-client/EmulatedUSBImpl.cpp

Last change on this file was 106061, checked in by vboxsync, 3 weeks ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 18.3 KB
Line 
1/* $Id: EmulatedUSBImpl.cpp 106061 2024-09-16 14:03:52Z vboxsync $ */
2/** @file
3 * Emulated USB manager implementation.
4 */
5
6/*
7 * Copyright (C) 2013-2024 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#define LOG_GROUP LOG_GROUP_MAIN_EMULATEDUSB
29#include "LoggingNew.h"
30
31#include "EmulatedUSBImpl.h"
32#include "ConsoleImpl.h"
33
34#include <VBox/vmm/pdmusb.h>
35#include <VBox/vmm/vmmr3vtable.h>
36
37
38/*
39 * Emulated USB webcam device instance.
40 */
41typedef std::map <Utf8Str, Utf8Str> EUSBSettingsMap;
42
43typedef enum EUSBDEVICESTATUS
44{
45 EUSBDEVICE_CREATED,
46 EUSBDEVICE_ATTACHING,
47 EUSBDEVICE_ATTACHED
48} EUSBDEVICESTATUS;
49
50class EUSBWEBCAM /* : public EUSBDEVICE */
51{
52private:
53 int32_t volatile mcRefs;
54
55 EmulatedUSB *mpEmulatedUSB;
56
57 RTUUID mUuid;
58 char mszUuid[RTUUID_STR_LENGTH];
59
60 Utf8Str mPath;
61 Utf8Str mSettings;
62
63 EUSBSettingsMap mDevSettings;
64 EUSBSettingsMap mDrvSettings;
65
66 void *mpvObject;
67
68 static DECLCALLBACK(int) emulatedWebcamAttach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis, const char *pszDriver);
69 static DECLCALLBACK(int) emulatedWebcamDetach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis);
70
71 HRESULT settingsParse(void);
72
73 ~EUSBWEBCAM()
74 {
75 }
76
77public:
78 EUSBWEBCAM()
79 :
80 mcRefs(1),
81 mpEmulatedUSB(NULL),
82 mpvObject(NULL),
83 enmStatus(EUSBDEVICE_CREATED)
84 {
85 RT_ZERO(mUuid);
86 RT_ZERO(mszUuid);
87 }
88
89 int32_t AddRef(void)
90 {
91 return ASMAtomicIncS32(&mcRefs);
92 }
93
94 void Release(void)
95 {
96 int32_t c = ASMAtomicDecS32(&mcRefs);
97 if (c == 0)
98 {
99 delete this;
100 }
101 }
102
103 HRESULT Initialize(Console *pConsole,
104 EmulatedUSB *pEmulatedUSB,
105 const com::Utf8Str *aPath,
106 const com::Utf8Str *aSettings,
107 void *pvObject);
108 HRESULT Attach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM, const char *pszDriver);
109 HRESULT Detach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM);
110
111 bool HasId(const char *pszId) { return RTStrCmp(pszId, mszUuid) == 0;}
112
113 void *getObjectPtr() { return mpvObject; }
114
115 EUSBDEVICESTATUS enmStatus;
116};
117
118
119static int emulatedWebcamInsertSettings(PCFGMNODE pConfig, PCVMMR3VTABLE pVMM, EUSBSettingsMap *pSettings)
120{
121 for (EUSBSettingsMap::const_iterator it = pSettings->begin(); it != pSettings->end(); ++it)
122 {
123 /* Convert some well known settings for backward compatibility. */
124 int vrc;
125 if ( RTStrCmp(it->first.c_str(), "MaxPayloadTransferSize") == 0
126 || RTStrCmp(it->first.c_str(), "MaxFramerate") == 0)
127 {
128 uint32_t u32 = 0;
129 vrc = RTStrToUInt32Full(it->second.c_str(), 10, &u32);
130 if (vrc == VINF_SUCCESS)
131 vrc = pVMM->pfnCFGMR3InsertInteger(pConfig, it->first.c_str(), u32);
132 else if (RT_SUCCESS(vrc)) /* VWRN_* */
133 vrc = VERR_INVALID_PARAMETER;
134 }
135 else
136 vrc = pVMM->pfnCFGMR3InsertString(pConfig, it->first.c_str(), it->second.c_str());
137 if (RT_FAILURE(vrc))
138 return vrc;
139 }
140
141 return VINF_SUCCESS;
142}
143
144/*static*/ DECLCALLBACK(int)
145EUSBWEBCAM::emulatedWebcamAttach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis, const char *pszDriver)
146{
147 PCFGMNODE pInstance = pVMM->pfnCFGMR3CreateTree(pUVM);
148 PCFGMNODE pConfig;
149 int vrc = pVMM->pfnCFGMR3InsertNode(pInstance, "Config", &pConfig);
150 AssertRCReturn(vrc, vrc);
151 vrc = emulatedWebcamInsertSettings(pConfig, pVMM, &pThis->mDevSettings);
152 AssertRCReturn(vrc, vrc);
153
154 PCFGMNODE pEUSB;
155 vrc = pVMM->pfnCFGMR3InsertNode(pConfig, "EmulatedUSB", &pEUSB);
156 AssertRCReturn(vrc, vrc);
157 vrc = pVMM->pfnCFGMR3InsertString(pEUSB, "Id", pThis->mszUuid);
158 AssertRCReturn(vrc, vrc);
159
160 PCFGMNODE pLunL0;
161 vrc = pVMM->pfnCFGMR3InsertNode(pInstance, "LUN#0", &pLunL0);
162 AssertRCReturn(vrc, vrc);
163 vrc = pVMM->pfnCFGMR3InsertString(pLunL0, "Driver", pszDriver);
164 AssertRCReturn(vrc, vrc);
165 vrc = pVMM->pfnCFGMR3InsertNode(pLunL0, "Config", &pConfig);
166 AssertRCReturn(vrc, vrc);
167 vrc = pVMM->pfnCFGMR3InsertString(pConfig, "DevicePath", pThis->mPath.c_str());
168 AssertRCReturn(vrc, vrc);
169 vrc = pVMM->pfnCFGMR3InsertString(pConfig, "Id", pThis->mszUuid);
170 AssertRCReturn(vrc, vrc);
171 vrc = emulatedWebcamInsertSettings(pConfig, pVMM, &pThis->mDrvSettings);
172 AssertRCReturn(vrc, vrc);
173
174 /* pInstance will be used by PDM and deallocated on error. */
175 vrc = pVMM->pfnPDMR3UsbCreateEmulatedDevice(pUVM, "Webcam", pInstance, &pThis->mUuid, NULL);
176 LogRelFlowFunc(("PDMR3UsbCreateEmulatedDevice %Rrc\n", vrc));
177 return vrc;
178}
179
180/*static*/ DECLCALLBACK(int)
181EUSBWEBCAM::emulatedWebcamDetach(PUVM pUVM, PCVMMR3VTABLE pVMM, EUSBWEBCAM *pThis)
182{
183 return pVMM->pfnPDMR3UsbDetachDevice(pUVM, &pThis->mUuid);
184}
185
186HRESULT EUSBWEBCAM::Initialize(Console *pConsole,
187 EmulatedUSB *pEmulatedUSB,
188 const com::Utf8Str *aPath,
189 const com::Utf8Str *aSettings,
190 void *pvObject)
191{
192 HRESULT hrc = S_OK;
193
194 int vrc = RTUuidCreate(&mUuid);
195 AssertRCReturn(vrc, pConsole->setError(vrc, EmulatedUSB::tr("Init emulated USB webcam (RTUuidCreate -> %Rrc)"), vrc));
196
197 RTStrPrintf(mszUuid, sizeof(mszUuid), "%RTuuid", &mUuid);
198 hrc = mPath.assignEx(*aPath);
199 if (SUCCEEDED(hrc))
200 {
201 hrc = mSettings.assignEx(*aSettings);
202 if (SUCCEEDED(hrc))
203 {
204 hrc = settingsParse();
205 if (SUCCEEDED(hrc))
206 {
207 mpEmulatedUSB = pEmulatedUSB;
208 mpvObject = pvObject;
209 }
210 }
211 }
212
213 return hrc;
214}
215
216HRESULT EUSBWEBCAM::settingsParse(void)
217{
218 HRESULT hr = S_OK;
219
220 /* Parse mSettings string:
221 * "[dev:|drv:]Name1=Value1;[dev:|drv:]Name2=Value2"
222 */
223 char *pszSrc = mSettings.mutableRaw();
224
225 if (pszSrc)
226 {
227 while (*pszSrc)
228 {
229 /* Does the setting belong to device of driver. Default is both. */
230 bool fDev = true;
231 bool fDrv = true;
232 if (RTStrNICmp(pszSrc, RT_STR_TUPLE("drv:")) == 0)
233 {
234 pszSrc += sizeof("drv:")-1;
235 fDev = false;
236 }
237 else if (RTStrNICmp(pszSrc, RT_STR_TUPLE("dev:")) == 0)
238 {
239 pszSrc += sizeof("dev:")-1;
240 fDrv = false;
241 }
242
243 char *pszEq = strchr(pszSrc, '=');
244 if (!pszEq)
245 {
246 hr = E_INVALIDARG;
247 break;
248 }
249
250 char *pszEnd = strchr(pszEq, ';');
251 if (!pszEnd)
252 pszEnd = pszEq + strlen(pszEq);
253
254 *pszEq = 0;
255 char chEnd = *pszEnd;
256 *pszEnd = 0;
257
258 /* Empty strings not allowed. */
259 if (*pszSrc != 0 && pszEq[1] != 0)
260 {
261 if (fDev)
262 mDevSettings[pszSrc] = &pszEq[1];
263 if (fDrv)
264 mDrvSettings[pszSrc] = &pszEq[1];
265 }
266
267 *pszEq = '=';
268 *pszEnd = chEnd;
269
270 pszSrc = pszEnd;
271 if (*pszSrc == ';')
272 pszSrc++;
273 }
274
275 if (SUCCEEDED(hr))
276 {
277 EUSBSettingsMap::const_iterator it;
278 for (it = mDevSettings.begin(); it != mDevSettings.end(); ++it)
279 LogRelFlowFunc(("[dev:%s] = [%s]\n", it->first.c_str(), it->second.c_str()));
280 for (it = mDrvSettings.begin(); it != mDrvSettings.end(); ++it)
281 LogRelFlowFunc(("[drv:%s] = [%s]\n", it->first.c_str(), it->second.c_str()));
282 }
283 }
284
285 return hr;
286}
287
288HRESULT EUSBWEBCAM::Attach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM, const char *pszDriver)
289{
290 int vrc = pVMM->pfnVMR3ReqCallWaitU(pUVM, 0 /* idDstCpu (saved state, see #6232) */, (PFNRT)emulatedWebcamAttach, 4,
291 pUVM, pVMM, this, pszDriver);
292 if (RT_SUCCESS(vrc))
293 return S_OK;
294 LogFlowThisFunc(("%Rrc\n", vrc));
295 return pConsole->setErrorBoth(VBOX_E_VM_ERROR, vrc, EmulatedUSB::tr("Attach emulated USB webcam (%Rrc)"), vrc);
296}
297
298HRESULT EUSBWEBCAM::Detach(Console *pConsole, PUVM pUVM, PCVMMR3VTABLE pVMM)
299{
300 int vrc = pVMM->pfnVMR3ReqCallWaitU(pUVM, 0 /* idDstCpu (saved state, see #6232) */, (PFNRT)emulatedWebcamDetach, 3,
301 pUVM, pVMM, this);
302 if (RT_SUCCESS(vrc))
303 return S_OK;
304 LogFlowThisFunc(("%Rrc\n", vrc));
305 return pConsole->setErrorBoth(VBOX_E_VM_ERROR, vrc, EmulatedUSB::tr("Detach emulated USB webcam (%Rrc)"), vrc);
306}
307
308
309/*
310 * EmulatedUSB implementation.
311 */
312DEFINE_EMPTY_CTOR_DTOR(EmulatedUSB)
313
314HRESULT EmulatedUSB::FinalConstruct()
315{
316 return BaseFinalConstruct();
317}
318
319void EmulatedUSB::FinalRelease()
320{
321 uninit();
322
323 BaseFinalRelease();
324}
325
326/*
327 * Initializes the instance.
328 *
329 * @param pConsole The owner.
330 */
331HRESULT EmulatedUSB::init(ComObjPtr<Console> pConsole)
332{
333 LogFlowThisFunc(("\n"));
334
335 ComAssertRet(!pConsole.isNull(), E_INVALIDARG);
336
337 /* Enclose the state transition NotReady->InInit->Ready */
338 AutoInitSpan autoInitSpan(this);
339 AssertReturn(autoInitSpan.isOk(), E_FAIL);
340
341 m.pConsole = pConsole;
342
343 mEmUsbIf.pvUser = this;
344 mEmUsbIf.pfnQueryEmulatedUsbDataById = EmulatedUSB::i_QueryEmulatedUsbDataById;
345
346 /* Confirm a successful initialization */
347 autoInitSpan.setSucceeded();
348
349 return S_OK;
350}
351
352/*
353 * Uninitializes the instance.
354 * Called either from FinalRelease() or by the parent when it gets destroyed.
355 */
356void EmulatedUSB::uninit()
357{
358 LogFlowThisFunc(("\n"));
359
360 m.pConsole.setNull();
361
362 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
363 for (WebcamsMap::iterator it = m.webcams.begin(); it != m.webcams.end(); ++it)
364 {
365 EUSBWEBCAM *p = it->second;
366 if (p)
367 {
368 it->second = NULL;
369 p->Release();
370 }
371 }
372 m.webcams.clear();
373 alock.release();
374
375 /* Enclose the state transition Ready->InUninit->NotReady */
376 AutoUninitSpan autoUninitSpan(this);
377 if (autoUninitSpan.uninitDone())
378 return;
379}
380
381HRESULT EmulatedUSB::getWebcams(std::vector<com::Utf8Str> &aWebcams)
382{
383 HRESULT hrc = S_OK;
384
385 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
386
387 try
388 {
389 aWebcams.resize(m.webcams.size());
390 }
391 catch (std::bad_alloc &)
392 {
393 hrc = E_OUTOFMEMORY;
394 }
395 catch (...)
396 {
397 hrc = E_FAIL;
398 }
399
400 if (SUCCEEDED(hrc))
401 {
402 size_t i;
403 WebcamsMap::const_iterator it;
404 for (i = 0, it = m.webcams.begin(); it != m.webcams.end(); ++it)
405 aWebcams[i++] = it->first;
406 }
407
408 return hrc;
409}
410
411PEMULATEDUSBIF EmulatedUSB::i_getEmulatedUsbIf()
412{
413 return &mEmUsbIf;
414}
415
416static const Utf8Str s_pathDefault(".0");
417
418HRESULT EmulatedUSB::webcamAttach(const com::Utf8Str &aPath,
419 const com::Utf8Str &aSettings)
420{
421 return i_webcamAttachInternal(aPath, aSettings, "HostWebcam", NULL);
422}
423
424HRESULT EmulatedUSB::i_webcamAttachInternal(const com::Utf8Str &aPath,
425 const com::Utf8Str &aSettings,
426 const char *pszDriver,
427 void *pvObject)
428{
429 HRESULT hrc = S_OK;
430
431 const Utf8Str &path = aPath.isEmpty() || aPath == "."? s_pathDefault: aPath;
432
433 Console::SafeVMPtr ptrVM(m.pConsole);
434 if (ptrVM.isOk())
435 {
436 EUSBWEBCAM *p = new EUSBWEBCAM();
437 if (p)
438 {
439 hrc = p->Initialize(m.pConsole, this, &path, &aSettings, pvObject);
440 if (SUCCEEDED(hrc))
441 {
442 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
443 WebcamsMap::const_iterator it = m.webcams.find(path);
444 if (it == m.webcams.end())
445 {
446 p->AddRef();
447 try
448 {
449 m.webcams[path] = p;
450 }
451 catch (std::bad_alloc &)
452 {
453 hrc = E_OUTOFMEMORY;
454 }
455 catch (...)
456 {
457 hrc = E_FAIL;
458 }
459 p->enmStatus = EUSBDEVICE_ATTACHING;
460 }
461 else
462 {
463 hrc = E_FAIL;
464 }
465 }
466
467 if (SUCCEEDED(hrc))
468 hrc = p->Attach(m.pConsole, ptrVM.rawUVM(), ptrVM.vtable(), pszDriver);
469
470 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
471 if (SUCCEEDED(hrc))
472 p->enmStatus = EUSBDEVICE_ATTACHED;
473 else if (p->enmStatus != EUSBDEVICE_CREATED)
474 m.webcams.erase(path);
475 alock.release();
476
477 p->Release();
478 }
479 else
480 {
481 hrc = E_OUTOFMEMORY;
482 }
483 }
484 else
485 {
486 hrc = VBOX_E_INVALID_VM_STATE;
487 }
488
489 return hrc;
490}
491
492HRESULT EmulatedUSB::webcamDetach(const com::Utf8Str &aPath)
493{
494 return i_webcamDetachInternal(aPath);
495}
496
497HRESULT EmulatedUSB::i_webcamDetachInternal(const com::Utf8Str &aPath)
498{
499 HRESULT hrc = S_OK;
500
501 const Utf8Str &path = aPath.isEmpty() || aPath == "."? s_pathDefault: aPath;
502
503 Console::SafeVMPtr ptrVM(m.pConsole);
504 if (ptrVM.isOk())
505 {
506 EUSBWEBCAM *p = NULL;
507
508 AutoWriteLock alock(this COMMA_LOCKVAL_SRC_POS);
509 WebcamsMap::iterator it = m.webcams.find(path);
510 if (it != m.webcams.end())
511 {
512 if (it->second->enmStatus == EUSBDEVICE_ATTACHED)
513 {
514 p = it->second;
515 m.webcams.erase(it);
516 }
517 }
518 alock.release();
519
520 if (p)
521 {
522 hrc = p->Detach(m.pConsole, ptrVM.rawUVM(), ptrVM.vtable());
523 p->Release();
524 }
525 else
526 {
527 hrc = E_INVALIDARG;
528 }
529 }
530 else
531 {
532 hrc = VBOX_E_INVALID_VM_STATE;
533 }
534
535 return hrc;
536}
537
538/*static*/ DECLCALLBACK(int)
539EmulatedUSB::eusbCallbackEMT(EmulatedUSB *pThis, char *pszId, uint32_t iEvent, void *pvData, uint32_t cbData)
540{
541 LogRelFlowFunc(("id %s event %d, data %p %d\n", pszId, iEvent, pvData, cbData));
542
543 NOREF(cbData);
544
545 int vrc = VINF_SUCCESS;
546 if (iEvent == 0)
547 {
548 com::Utf8Str path;
549 HRESULT hrc = pThis->webcamPathFromId(&path, pszId);
550 if (SUCCEEDED(hrc))
551 {
552 hrc = pThis->webcamDetach(path);
553 if (FAILED(hrc))
554 {
555 vrc = VERR_INVALID_STATE;
556 }
557 }
558 else
559 {
560 vrc = VERR_NOT_FOUND;
561 }
562 }
563 else
564 {
565 vrc = VERR_INVALID_PARAMETER;
566 }
567
568 RTMemFree(pszId);
569 RTMemFree(pvData);
570
571 LogRelFlowFunc(("vrc %Rrc\n", vrc));
572 return vrc;
573}
574
575/* static */ DECLCALLBACK(int)
576EmulatedUSB::i_eusbCallback(void *pv, const char *pszId, uint32_t iEvent, const void *pvData, uint32_t cbData)
577{
578 /* Make a copy of parameters, forward to EMT and leave the callback to not hold any lock in the device. */
579 int vrc = VINF_SUCCESS;
580 void *pvDataCopy = NULL;
581 if (cbData > 0)
582 {
583 pvDataCopy = RTMemDup(pvData, cbData);
584 if (!pvDataCopy)
585 vrc = VERR_NO_MEMORY;
586 }
587 if (RT_SUCCESS(vrc))
588 {
589 void *pvIdCopy = RTMemDup(pszId, strlen(pszId) + 1);
590 if (pvIdCopy)
591 {
592 if (RT_SUCCESS(vrc))
593 {
594 EmulatedUSB *pThis = (EmulatedUSB *)pv;
595 Console::SafeVMPtr ptrVM(pThis->m.pConsole);
596 if (ptrVM.isOk())
597 {
598 /* No wait. */
599 vrc = ptrVM.vtable()->pfnVMR3ReqCallNoWaitU(ptrVM.rawUVM(), 0 /* idDstCpu */,
600 (PFNRT)EmulatedUSB::eusbCallbackEMT, 5,
601 pThis, pvIdCopy, iEvent, pvDataCopy, cbData);
602 if (RT_SUCCESS(vrc))
603 return vrc;
604 }
605 else
606 vrc = VERR_INVALID_STATE;
607 }
608 RTMemFree(pvIdCopy);
609 }
610 else
611 vrc = VERR_NO_MEMORY;
612 RTMemFree(pvDataCopy);
613 }
614 return vrc;
615}
616
617/*static*/
618DECLCALLBACK(int) EmulatedUSB::i_QueryEmulatedUsbDataById(void *pvUser, const char *pszId, void **ppvEmUsbCb, void **ppvEmUsbCbData, void **ppvObject)
619{
620 EmulatedUSB *pEmUsb = (EmulatedUSB *)pvUser;
621
622 AutoReadLock alock(pEmUsb COMMA_LOCKVAL_SRC_POS);
623 WebcamsMap::const_iterator it;
624 for (it = pEmUsb->m.webcams.begin(); it != pEmUsb->m.webcams.end(); ++it)
625 {
626 EUSBWEBCAM *p = it->second;
627 if (p->HasId(pszId))
628 {
629 if (ppvEmUsbCb)
630 *ppvEmUsbCb = (void *)EmulatedUSB::i_eusbCallback;
631 if (ppvEmUsbCbData)
632 *ppvEmUsbCbData = pEmUsb;
633 if (ppvObject)
634 *ppvObject = p->getObjectPtr();
635
636 return VINF_SUCCESS;
637 }
638 }
639
640 return VERR_NOT_FOUND;
641}
642
643HRESULT EmulatedUSB::webcamPathFromId(com::Utf8Str *pPath, const char *pszId)
644{
645 HRESULT hrc = S_OK;
646
647 Console::SafeVMPtr ptrVM(m.pConsole);
648 if (ptrVM.isOk())
649 {
650 AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS);
651 WebcamsMap::const_iterator it;
652 for (it = m.webcams.begin(); it != m.webcams.end(); ++it)
653 {
654 EUSBWEBCAM *p = it->second;
655 if (p->HasId(pszId))
656 {
657 *pPath = it->first;
658 break;
659 }
660 }
661
662 if (it == m.webcams.end())
663 {
664 hrc = E_FAIL;
665 }
666 alock.release();
667 }
668 else
669 {
670 hrc = VBOX_E_INVALID_VM_STATE;
671 }
672
673 return hrc;
674}
675
676/* vi: set tabstop=4 shiftwidth=4 expandtab: */
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