VirtualBox

source: vbox/trunk/include/VBox/com/AutoLock.h

Last change on this file was 98103, checked in by vboxsync, 16 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.6 KB
Line 
1/** @file
2 * MS COM / XPCOM Abstraction Layer - Automatic locks, implementation.
3 */
4
5/*
6 * Copyright (C) 2006-2023 Oracle and/or its affiliates.
7 *
8 * This file is part of VirtualBox base platform packages, as
9 * available from https://www.virtualbox.org.
10 *
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU General Public License
13 * as published by the Free Software Foundation, in version 3 of the
14 * License.
15 *
16 * This program is distributed in the hope that it will be useful, but
17 * WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, see <https://www.gnu.org/licenses>.
23 *
24 * The contents of this file may alternatively be used under the terms
25 * of the Common Development and Distribution License Version 1.0
26 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
27 * in the VirtualBox distribution, in which case the provisions of the
28 * CDDL are applicable instead of those of the GPL.
29 *
30 * You may elect to license modified versions of this file under the
31 * terms and conditions of either the GPL or the CDDL or both.
32 *
33 * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
34 */
35
36#ifndef VBOX_INCLUDED_com_AutoLock_h
37#define VBOX_INCLUDED_com_AutoLock_h
38#ifndef RT_WITHOUT_PRAGMA_ONCE
39# pragma once
40#endif
41
42#include <iprt/types.h>
43
44
45/** @defgroup grp_com_autolock Automatic Locks
46 * @ingroup grp_com
47 * @{
48 */
49
50// macros for automatic lock validation; these will amount to nothing
51// unless lock validation is enabled for the runtime
52#if defined(RT_LOCK_STRICT)
53# define VBOX_WITH_MAIN_LOCK_VALIDATION
54# define COMMA_LOCKVAL_SRC_POS , RT_SRC_POS
55# define LOCKVAL_SRC_POS_DECL RT_SRC_POS_DECL
56# define COMMA_LOCKVAL_SRC_POS_DECL , RT_SRC_POS_DECL
57# define LOCKVAL_SRC_POS_ARGS RT_SRC_POS_ARGS
58# define COMMA_LOCKVAL_SRC_POS_ARGS , RT_SRC_POS_ARGS
59#else
60# define COMMA_LOCKVAL_SRC_POS
61# define LOCKVAL_SRC_POS_DECL
62# define COMMA_LOCKVAL_SRC_POS_DECL
63# define LOCKVAL_SRC_POS_ARGS
64# define COMMA_LOCKVAL_SRC_POS_ARGS
65#endif
66
67namespace util
68{
69
70////////////////////////////////////////////////////////////////////////////////
71//
72// Order classes for lock validation
73//
74////////////////////////////////////////////////////////////////////////////////
75
76/**
77 * IPRT now has a sophisticated system of run-time locking classes to validate
78 * locking order. Since the Main code is handled by simpler minds, we want
79 * compile-time constants for simplicity, and we'll look up the run-time classes
80 * in AutoLock.cpp transparently. These are passed to the constructors of the
81 * LockHandle classes.
82 */
83enum VBoxLockingClass
84{
85 LOCKCLASS_NONE = 0,
86 LOCKCLASS_WEBSERVICE = 1, // highest order: webservice locks
87 LOCKCLASS_VIRTUALBOXOBJECT = 2, // highest order within Main itself: VirtualBox object lock
88 LOCKCLASS_HOSTOBJECT = 3, // Host object lock
89 LOCKCLASS_LISTOFMACHINES = 4, // list of machines in VirtualBox object
90 LOCKCLASS_MACHINEOBJECT = 5, // Machine object lock
91 LOCKCLASS_SNAPSHOTOBJECT = 6, // snapshot object locks
92 // (the snapshots tree, including the child pointers in Snapshot,
93 // is protected by the normal Machine object lock)
94 LOCKCLASS_MEDIUMQUERY = 7, // lock used to protect Machine::queryInfo
95 LOCKCLASS_LISTOFMEDIA = 8, // list of media (hard disks, DVDs, floppies) in VirtualBox object
96 LOCKCLASS_LISTOFOTHEROBJECTS = 9, // any other list of objects
97 LOCKCLASS_OTHEROBJECT = 10, // any regular object member variable lock
98 LOCKCLASS_PROGRESSLIST = 11, // list of progress objects in VirtualBox; no other object lock
99 // may be held after this!
100 LOCKCLASS_OBJECTSTATE = 12, // object state lock (handled by AutoCaller classes)
101 LOCKCLASS_TRANSLATOR = 13 // translator internal lock
102};
103
104void InitAutoLockSystem();
105
106/**
107 * Check whether the current thread holds any locks in the given class
108 *
109 * @return true if any such locks are held, false otherwise. If the lock
110 * validator is not compiled in, always returns false.
111 * @param lockClass Which lock class to check.
112 */
113bool AutoLockHoldsLocksInClass(VBoxLockingClass lockClass);
114
115////////////////////////////////////////////////////////////////////////////////
116//
117// LockHandle and friends
118//
119////////////////////////////////////////////////////////////////////////////////
120
121/**
122 * Abstract base class for semaphore handles (RWLockHandle and WriteLockHandle).
123 * Don't use this directly, but this implements lock validation for them.
124 */
125class LockHandle
126{
127public:
128 LockHandle()
129 {}
130
131 virtual ~LockHandle()
132 {}
133
134 /**
135 * Returns @c true if the current thread holds a write lock on this
136 * read/write semaphore. Intended for debugging only.
137 */
138 virtual bool isWriteLockOnCurrentThread() const = 0;
139
140 /**
141 * Returns @c true if the current thread holds a read lock on this
142 * read/write semaphore. Intended for debugging only as it isn't always
143 * accurate given @a fWannaHear.
144 */
145 virtual bool isReadLockedOnCurrentThread(bool fWannaHear = true) const = 0;
146
147 /**
148 * Returns the current write lock level of this semaphore. The lock level
149 * determines the number of nested #lockWrite() calls on the given
150 * semaphore handle.
151 *
152 * Note that this call is valid only when the current thread owns a write
153 * lock on the given semaphore handle and will assert otherwise.
154 */
155 virtual uint32_t writeLockLevel() const = 0;
156
157 virtual void lockWrite(LOCKVAL_SRC_POS_DECL) = 0;
158 virtual void unlockWrite() = 0;
159 virtual void lockRead(LOCKVAL_SRC_POS_DECL) = 0;
160 virtual void unlockRead() = 0;
161
162#ifdef VBOX_WITH_MAIN_LOCK_VALIDATION
163 virtual const char* describe() const = 0;
164#endif
165
166private:
167 // prohibit copy + assignment
168 LockHandle(const LockHandle&);
169 LockHandle& operator=(const LockHandle&);
170};
171
172/**
173 * Full-featured read/write semaphore handle implementation.
174 *
175 * This is an auxiliary base class for classes that need full-featured
176 * read/write locking as described in the AutoWriteLock class documentation.
177 * Instances of classes inherited from this class can be passed as arguments to
178 * the AutoWriteLock and AutoReadLock constructors.
179 */
180class RWLockHandle : public LockHandle
181{
182public:
183 RWLockHandle(VBoxLockingClass lockClass);
184 virtual ~RWLockHandle();
185
186 virtual bool isWriteLockOnCurrentThread() const;
187 virtual bool isReadLockedOnCurrentThread(bool fWannaHear = true) const;
188
189 virtual void lockWrite(LOCKVAL_SRC_POS_DECL);
190 virtual void unlockWrite();
191 virtual void lockRead(LOCKVAL_SRC_POS_DECL);
192 virtual void unlockRead();
193
194 virtual uint32_t writeLockLevel() const;
195
196#ifdef VBOX_WITH_MAIN_LOCK_VALIDATION
197 virtual const char* describe() const;
198#endif
199
200private:
201 struct Data;
202 Data *m;
203
204 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(RWLockHandle); /* Shuts up MSC warning C4625. */
205};
206
207/**
208 * Write-only semaphore handle implementation.
209 *
210 * This is an auxiliary base class for classes that need write-only (exclusive)
211 * locking and do not need read (shared) locking. This implementation uses a
212 * cheap and fast critical section for both lockWrite() and lockRead() methods
213 * which makes a lockRead() call fully equivalent to the lockWrite() call and
214 * therefore makes it pointless to use instahces of this class with
215 * AutoReadLock instances -- shared locking will not be possible anyway and
216 * any call to lock() will block if there are lock owners on other threads.
217 *
218 * Use with care only when absolutely sure that shared locks are not necessary.
219 */
220class WriteLockHandle : public LockHandle
221{
222public:
223 WriteLockHandle(VBoxLockingClass lockClass);
224 virtual ~WriteLockHandle();
225 virtual bool isWriteLockOnCurrentThread() const;
226 virtual bool isReadLockedOnCurrentThread(bool fWannaHear = true) const;
227
228 virtual void lockWrite(LOCKVAL_SRC_POS_DECL);
229 virtual void unlockWrite();
230 virtual void lockRead(LOCKVAL_SRC_POS_DECL);
231 virtual void unlockRead();
232 virtual uint32_t writeLockLevel() const;
233
234#ifdef VBOX_WITH_MAIN_LOCK_VALIDATION
235 virtual const char* describe() const;
236#endif
237
238private:
239 struct Data;
240 Data *m;
241
242 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(WriteLockHandle); /* Shuts up MSC warning C4625. */
243};
244
245////////////////////////////////////////////////////////////////////////////////
246//
247// Lockable
248//
249////////////////////////////////////////////////////////////////////////////////
250
251/**
252 * Lockable interface.
253 *
254 * This is an abstract base for classes that need read/write locking. Unlike
255 * RWLockHandle and other classes that makes the read/write semaphore a part of
256 * class data, this class allows subclasses to decide which semaphore handle to
257 * use.
258 */
259class Lockable
260{
261public:
262 virtual ~Lockable() { } /* To make VC++ 2019 happy. */
263
264 /**
265 * Returns a pointer to a LockHandle used by AutoWriteLock/AutoReadLock
266 * for locking. Subclasses are allowed to return @c NULL -- in this case,
267 * the AutoWriteLock/AutoReadLock object constructed using an instance of
268 * such subclass will simply turn into no-op.
269 */
270 virtual LockHandle *lockHandle() const = 0;
271
272 /**
273 * Equivalent to <tt>#lockHandle()->isWriteLockOnCurrentThread()</tt>.
274 * Returns @c false if lockHandle() returns @c NULL.
275 */
276 bool isWriteLockOnCurrentThread()
277 {
278 LockHandle *h = lockHandle();
279 return h ? h->isWriteLockOnCurrentThread() : false;
280 }
281
282 /**
283 * Equivalent to <tt>#lockHandle()->isReadLockedOnCurrentThread()</tt>.
284 * Returns @c false if lockHandle() returns @c NULL.
285 * @note Use with care, simple debug assertions and similar only.
286 */
287 bool isReadLockedOnCurrentThread(bool fWannaHear = true) const
288 {
289 LockHandle *h = lockHandle();
290 return h ? h->isReadLockedOnCurrentThread(fWannaHear) : false;
291 }
292};
293
294////////////////////////////////////////////////////////////////////////////////
295//
296// AutoLockBase
297//
298////////////////////////////////////////////////////////////////////////////////
299
300/**
301 * Abstract base class for all autolocks.
302 *
303 * This cannot be used directly. Use AutoReadLock or AutoWriteLock or AutoMultiWriteLock2/3
304 * which directly and indirectly derive from this.
305 *
306 * In the implementation, the instance data contains a list of lock handles.
307 * The class provides some utility functions to help locking and unlocking
308 * them.
309 */
310
311class AutoLockBase
312{
313protected:
314 AutoLockBase(uint32_t cHandles
315 COMMA_LOCKVAL_SRC_POS_DECL);
316 AutoLockBase(uint32_t cHandles,
317 LockHandle *pHandle
318 COMMA_LOCKVAL_SRC_POS_DECL);
319 virtual ~AutoLockBase();
320
321 struct Data;
322 Data *m;
323
324 virtual void callLockImpl(LockHandle &l) = 0;
325 virtual void callUnlockImpl(LockHandle &l) = 0;
326
327 void callLockOnAllHandles();
328 void callUnlockOnAllHandles();
329
330 void cleanup();
331
332public:
333 void acquire();
334 void release();
335
336private:
337 // prohibit copy + assignment
338 AutoLockBase(const AutoLockBase&);
339 AutoLockBase& operator=(const AutoLockBase&);
340};
341
342////////////////////////////////////////////////////////////////////////////////
343//
344// AutoReadLock
345//
346////////////////////////////////////////////////////////////////////////////////
347
348/**
349 * Automatic read lock. Use this with a RWLockHandle to request a read/write
350 * semaphore in read mode. You can also use this with a WriteLockHandle but
351 * that makes little sense since they treat read mode like write mode.
352 *
353 * If constructed with a RWLockHandle or an instance of Lockable (which in
354 * practice means any VirtualBoxBase derivative), it autoamtically requests
355 * the lock in read mode and releases the read lock in the destructor.
356 */
357class AutoReadLock : public AutoLockBase
358{
359public:
360
361 /**
362 * Constructs a null instance that does not manage any read/write
363 * semaphore.
364 *
365 * Note that all method calls on a null instance are no-ops. This allows to
366 * have the code where lock protection can be selected (or omitted) at
367 * runtime.
368 */
369 AutoReadLock(LOCKVAL_SRC_POS_DECL)
370 : AutoLockBase(1,
371 NULL
372 COMMA_LOCKVAL_SRC_POS_ARGS)
373 { }
374
375 /**
376 * Constructs a new instance that will start managing the given read/write
377 * semaphore by requesting a read lock.
378 */
379 AutoReadLock(LockHandle *aHandle
380 COMMA_LOCKVAL_SRC_POS_DECL)
381 : AutoLockBase(1,
382 aHandle
383 COMMA_LOCKVAL_SRC_POS_ARGS)
384 {
385 acquire();
386 }
387
388 /**
389 * Constructs a new instance that will start managing the given read/write
390 * semaphore by requesting a read lock.
391 */
392 AutoReadLock(LockHandle &aHandle
393 COMMA_LOCKVAL_SRC_POS_DECL)
394 : AutoLockBase(1,
395 &aHandle
396 COMMA_LOCKVAL_SRC_POS_ARGS)
397 {
398 acquire();
399 }
400
401 /**
402 * Constructs a new instance that will start managing the given read/write
403 * semaphore by requesting a read lock.
404 */
405 AutoReadLock(const Lockable &aLockable
406 COMMA_LOCKVAL_SRC_POS_DECL)
407 : AutoLockBase(1,
408 aLockable.lockHandle()
409 COMMA_LOCKVAL_SRC_POS_ARGS)
410 {
411 acquire();
412 }
413
414 /**
415 * Constructs a new instance that will start managing the given read/write
416 * semaphore by requesting a read lock.
417 */
418 AutoReadLock(const Lockable *aLockable
419 COMMA_LOCKVAL_SRC_POS_DECL)
420 : AutoLockBase(1,
421 aLockable ? aLockable->lockHandle() : NULL
422 COMMA_LOCKVAL_SRC_POS_ARGS)
423 {
424 acquire();
425 }
426
427 virtual ~AutoReadLock();
428
429 virtual void callLockImpl(LockHandle &l);
430 virtual void callUnlockImpl(LockHandle &l);
431
432private:
433 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(AutoReadLock); /* Shuts up MSC warning C4625. */
434};
435
436////////////////////////////////////////////////////////////////////////////////
437//
438// AutoWriteLockBase
439//
440////////////////////////////////////////////////////////////////////////////////
441
442/**
443 * Base class for all auto write locks.
444 *
445 * This cannot be used directly. Use AutoWriteLock or AutoMultiWriteLock2/3
446 * which derive from this.
447 *
448 * It has some utility methods for subclasses.
449 */
450class AutoWriteLockBase : public AutoLockBase
451{
452protected:
453 AutoWriteLockBase(uint32_t cHandles
454 COMMA_LOCKVAL_SRC_POS_DECL)
455 : AutoLockBase(cHandles
456 COMMA_LOCKVAL_SRC_POS_ARGS)
457 { }
458
459 AutoWriteLockBase(uint32_t cHandles,
460 LockHandle *pHandle
461 COMMA_LOCKVAL_SRC_POS_DECL)
462 : AutoLockBase(cHandles,
463 pHandle
464 COMMA_LOCKVAL_SRC_POS_ARGS)
465 { }
466
467 virtual ~AutoWriteLockBase()
468 { }
469
470 virtual void callLockImpl(LockHandle &l);
471 virtual void callUnlockImpl(LockHandle &l);
472
473private:
474 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(AutoWriteLockBase); /* Shuts up MSC warning C4625. */
475};
476
477////////////////////////////////////////////////////////////////////////////////
478//
479// AutoWriteLock
480//
481////////////////////////////////////////////////////////////////////////////////
482
483/**
484 * Automatic write lock. Use this with a RWLockHandle to request a read/write
485 * semaphore in write mode. There can only ever be one writer of a read/write
486 * semaphore: while the lock is held in write mode, no other writer or reader
487 * can request the semaphore and will block.
488 *
489 * If constructed with a RWLockHandle or an instance of Lockable (which in
490 * practice means any VirtualBoxBase derivative), it autoamtically requests
491 * the lock in write mode and releases the write lock in the destructor.
492 *
493 * When used with a WriteLockHandle, it requests the semaphore contained therein
494 * exclusively.
495 */
496class AutoWriteLock : public AutoWriteLockBase
497{
498public:
499
500 /**
501 * Constructs a null instance that does not manage any read/write
502 * semaphore.
503 *
504 * Note that all method calls on a null instance are no-ops. This allows to
505 * have the code where lock protection can be selected (or omitted) at
506 * runtime.
507 */
508 AutoWriteLock(LOCKVAL_SRC_POS_DECL)
509 : AutoWriteLockBase(1,
510 NULL
511 COMMA_LOCKVAL_SRC_POS_ARGS)
512 { }
513
514 /**
515 * Constructs a new instance that will start managing the given read/write
516 * semaphore by requesting a write lock.
517 */
518 AutoWriteLock(LockHandle *aHandle
519 COMMA_LOCKVAL_SRC_POS_DECL)
520 : AutoWriteLockBase(1,
521 aHandle
522 COMMA_LOCKVAL_SRC_POS_ARGS)
523 {
524 acquire();
525 }
526
527 /**
528 * Constructs a new instance that will start managing the given read/write
529 * semaphore by requesting a write lock.
530 */
531 AutoWriteLock(LockHandle &aHandle
532 COMMA_LOCKVAL_SRC_POS_DECL)
533 : AutoWriteLockBase(1,
534 &aHandle
535 COMMA_LOCKVAL_SRC_POS_ARGS)
536 {
537 acquire();
538 }
539
540 /**
541 * Constructs a new instance that will start managing the given read/write
542 * semaphore by requesting a write lock.
543 */
544 AutoWriteLock(const Lockable &aLockable
545 COMMA_LOCKVAL_SRC_POS_DECL)
546 : AutoWriteLockBase(1,
547 aLockable.lockHandle()
548 COMMA_LOCKVAL_SRC_POS_ARGS)
549 {
550 acquire();
551 }
552
553 /**
554 * Constructs a new instance that will start managing the given read/write
555 * semaphore by requesting a write lock.
556 */
557 AutoWriteLock(const Lockable *aLockable
558 COMMA_LOCKVAL_SRC_POS_DECL)
559 : AutoWriteLockBase(1,
560 aLockable ? aLockable->lockHandle() : NULL
561 COMMA_LOCKVAL_SRC_POS_ARGS)
562 {
563 acquire();
564 }
565
566 /**
567 * Constructs a new instance that will start managing the given read/write
568 * semaphore by requesting a write lock.
569 */
570 AutoWriteLock(uint32_t cHandles,
571 LockHandle** pHandles
572 COMMA_LOCKVAL_SRC_POS_DECL);
573
574 /**
575 * Release all write locks acquired by this instance through the #acquire()
576 * call and destroys the instance.
577 *
578 * Note that if there there are nested #acquire() calls without the
579 * corresponding number of #release() calls when the destructor is called, it
580 * will assert. This is because having an unbalanced number of nested locks
581 * is a program logic error which must be fixed.
582 */
583 virtual ~AutoWriteLock()
584 {
585 cleanup();
586 }
587
588 void attach(LockHandle *aHandle);
589
590 /** @see attach (LockHandle *) */
591 void attach(LockHandle &aHandle)
592 {
593 attach(&aHandle);
594 }
595
596 /** @see attach (LockHandle *) */
597 void attach(const Lockable &aLockable)
598 {
599 attach(aLockable.lockHandle());
600 }
601
602 /** @see attach (LockHandle *) */
603 void attach(const Lockable *aLockable)
604 {
605 attach(aLockable ? aLockable->lockHandle() : NULL);
606 }
607
608 bool isWriteLockOnCurrentThread() const;
609 uint32_t writeLockLevel() const;
610
611 bool isReadLockedOnCurrentThread(bool fWannaHear = true) const;
612
613private:
614 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(AutoWriteLock); /* Shuts up MSC warning C4625. */
615};
616
617////////////////////////////////////////////////////////////////////////////////
618//
619// AutoMultiWriteLock*
620//
621////////////////////////////////////////////////////////////////////////////////
622
623/**
624 * A multi-write-lock containing two other write locks.
625 *
626 */
627class AutoMultiWriteLock2 : public AutoWriteLockBase
628{
629public:
630 AutoMultiWriteLock2(Lockable *pl1,
631 Lockable *pl2
632 COMMA_LOCKVAL_SRC_POS_DECL);
633 AutoMultiWriteLock2(LockHandle *pl1,
634 LockHandle *pl2
635 COMMA_LOCKVAL_SRC_POS_DECL);
636
637 virtual ~AutoMultiWriteLock2()
638 {
639 cleanup();
640 }
641
642private:
643 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(AutoMultiWriteLock2); /* Shuts up MSC warning C4625. */
644};
645
646/**
647 * A multi-write-lock containing three other write locks.
648 *
649 */
650class AutoMultiWriteLock3 : public AutoWriteLockBase
651{
652public:
653 AutoMultiWriteLock3(Lockable *pl1,
654 Lockable *pl2,
655 Lockable *pl3
656 COMMA_LOCKVAL_SRC_POS_DECL);
657 AutoMultiWriteLock3(LockHandle *pl1,
658 LockHandle *pl2,
659 LockHandle *pl3
660 COMMA_LOCKVAL_SRC_POS_DECL);
661
662 virtual ~AutoMultiWriteLock3()
663 {
664 cleanup();
665 }
666
667private:
668 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(AutoMultiWriteLock3); /* Shuts up MSC warning C4625. */
669};
670
671/**
672 * A multi-write-lock containing four other write locks.
673 *
674 */
675class AutoMultiWriteLock4 : public AutoWriteLockBase
676{
677public:
678 AutoMultiWriteLock4(Lockable *pl1,
679 Lockable *pl2,
680 Lockable *pl3,
681 Lockable *pl4
682 COMMA_LOCKVAL_SRC_POS_DECL);
683 AutoMultiWriteLock4(LockHandle *pl1,
684 LockHandle *pl2,
685 LockHandle *pl3,
686 LockHandle *pl4
687 COMMA_LOCKVAL_SRC_POS_DECL);
688
689 virtual ~AutoMultiWriteLock4()
690 {
691 cleanup();
692 }
693
694private:
695 DECLARE_CLS_COPY_CTOR_ASSIGN_NOOP(AutoMultiWriteLock4); /* Shuts up MSC warning C4625. */
696};
697
698} /* namespace util */
699
700/** @} */
701
702#endif /* !VBOX_INCLUDED_com_AutoLock_h */
703
704/* vi: set tabstop=4 shiftwidth=4 expandtab: */
Note: See TracBrowser for help on using the repository browser.

© 2023 Oracle
ContactPrivacy policyTerms of Use