VirtualBox

source: vbox/trunk/src/VBox/Additions/common/VBoxService/VBoxService.cpp@ 28603

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

VBoxService: improved POSIX shutdown mechanism

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.3 KB
Line 
1/* $Id: VBoxService.cpp 28603 2010-04-22 15:48:25Z vboxsync $ */
2/** @file
3 * VBoxService - Guest Additions Service Skeleton.
4 */
5
6/*
7 * Copyright (C) 2007-2010 Sun Microsystems, Inc.
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 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
18 * Clara, CA 95054 USA or visit http://www.sun.com if you need
19 * additional information or have any questions.
20 */
21
22
23
24/*******************************************************************************
25* Header Files *
26*******************************************************************************/
27/** @todo LOG_GROUP*/
28#ifndef _MSC_VER
29# include <unistd.h>
30#endif
31#include <errno.h>
32#ifndef RT_OS_WINDOWS
33# include <signal.h>
34#endif
35
36#include "product-generated.h"
37#include <iprt/asm.h>
38#include <iprt/buildconfig.h>
39#include <iprt/initterm.h>
40#include <iprt/path.h>
41#include <iprt/string.h>
42#include <iprt/stream.h>
43#include <iprt/thread.h>
44
45#include <VBox/VBoxGuestLib.h>
46#include <VBox/log.h>
47
48#include "VBoxServiceInternal.h"
49
50
51/*******************************************************************************
52* Global Variables *
53*******************************************************************************/
54/** The program name (derived from argv[0]). */
55char *g_pszProgName = (char *)"";
56/** The current verbosity level. */
57int g_cVerbosity = 0;
58/** The default service interval (the -i | --interval) option). */
59uint32_t g_DefaultInterval = 0;
60/** Shutdown the main thread. (later, for signals.) */
61bool volatile g_fShutdown;
62
63/**
64 * The details of the services that has been compiled in.
65 */
66static struct
67{
68 /** Pointer to the service descriptor. */
69 PCVBOXSERVICE pDesc;
70 /** The worker thread. NIL_RTTHREAD if it's the main thread. */
71 RTTHREAD Thread;
72 /** Shutdown indicator. */
73 bool volatile fShutdown;
74 /** Indicator set by the service thread exiting. */
75 bool volatile fStopped;
76 /** Whether the service was started or not. */
77 bool fStarted;
78 /** Whether the service is enabled or not. */
79 bool fEnabled;
80} g_aServices[] =
81{
82#ifdef VBOXSERVICE_CONTROL
83 { &g_Control, NIL_RTTHREAD, false, false, false, true },
84#endif
85#ifdef VBOXSERVICE_TIMESYNC
86 { &g_TimeSync, NIL_RTTHREAD, false, false, false, true },
87#endif
88#ifdef VBOXSERVICE_CLIPBOARD
89 { &g_Clipboard, NIL_RTTHREAD, false, false, false, true },
90#endif
91#ifdef VBOXSERVICE_VMINFO
92 { &g_VMInfo, NIL_RTTHREAD, false, false, false, true },
93#endif
94#ifdef VBOXSERVICE_EXEC
95 { &g_Exec, NIL_RTTHREAD, false, false, false, true },
96#endif
97#ifdef VBOXSERVICE_CPUHOTPLUG
98 { &g_CpuHotPlug, NIL_RTTHREAD, false, false, false, true },
99#endif
100#ifdef VBOXSERVICE_MANAGEMENT
101 { &g_MemBalloon, NIL_RTTHREAD, false, false, false, true },
102 { &g_VMStatistics, NIL_RTTHREAD, false, false, false, true },
103#endif
104};
105
106
107/**
108 * Displays the program usage message.
109 *
110 * @returns 1.
111 */
112static int VBoxServiceUsage(void)
113{
114 RTPrintf("usage: %s [-f|--foreground] [-v|--verbose] [-i|--interval <seconds>]\n"
115 " [--disable-<service>] [--enable-<service>] [-h|-?|--help]\n", g_pszProgName);
116#ifdef RT_OS_WINDOWS
117 RTPrintf(" [-r|--register] [-u|--unregister]\n");
118#endif
119 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
120 RTPrintf(" %s\n", g_aServices[j].pDesc->pszUsage);
121 RTPrintf("\n"
122 "Options:\n"
123 " -i | --interval The default interval.\n"
124 " -f | --foreground Don't daemonzie the program. For debugging.\n"
125 " -v | --verbose Increment the verbosity level. For debugging.\n"
126 " -h | -? | --help Show this message and exit with status 1.\n"
127 );
128#ifdef RT_OS_WINDOWS
129 RTPrintf(" -r | --register Installs the service.\n"
130 " -u | --unregister Uninstall service.\n");
131#endif
132
133 RTPrintf("\n"
134 "Service specific options:\n");
135 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
136 {
137 RTPrintf(" --enable-%-10s Enables the %s service. (default)\n", g_aServices[j].pDesc->pszName, g_aServices[j].pDesc->pszName);
138 RTPrintf(" --disable-%-9s Disables the %s service.\n", g_aServices[j].pDesc->pszName, g_aServices[j].pDesc->pszName);
139 if (g_aServices[j].pDesc->pszOptions)
140 RTPrintf("%s", g_aServices[j].pDesc->pszOptions);
141 }
142 RTPrintf("\n"
143 " Copyright (C) 2009-" VBOX_C_YEAR " " VBOX_VENDOR "\n");
144
145 return 1;
146}
147
148
149/**
150 * Displays a syntax error message.
151 *
152 * @returns 1
153 * @param pszFormat The message text.
154 * @param ... Format arguments.
155 */
156int VBoxServiceSyntax(const char *pszFormat, ...)
157{
158 RTStrmPrintf(g_pStdErr, "%s: syntax error: ", g_pszProgName);
159
160 va_list va;
161 va_start(va, pszFormat);
162 RTStrmPrintfV(g_pStdErr, pszFormat, va);
163 va_end(va);
164
165 return 1;
166}
167
168
169/**
170 * Displays an error message.
171 *
172 * @returns 1
173 * @param pszFormat The message text.
174 * @param ... Format arguments.
175 */
176int VBoxServiceError(const char *pszFormat, ...)
177{
178 RTStrmPrintf(g_pStdErr, "%s: error: ", g_pszProgName);
179
180 va_list va;
181 va_start(va, pszFormat);
182 RTStrmPrintfV(g_pStdErr, pszFormat, va);
183 va_end(va);
184
185 va_start(va, pszFormat);
186 LogRel(("%s: Error: %N", g_pszProgName, pszFormat, &va));
187 va_end(va);
188
189 return 1;
190}
191
192
193/**
194 * Displays a verbose message.
195 *
196 * @returns 1
197 * @param pszFormat The message text.
198 * @param ... Format arguments.
199 */
200void VBoxServiceVerbose(int iLevel, const char *pszFormat, ...)
201{
202 if (iLevel <= g_cVerbosity)
203 {
204 RTStrmPrintf(g_pStdOut, "%s: ", g_pszProgName);
205 va_list va;
206 va_start(va, pszFormat);
207 RTStrmPrintfV(g_pStdOut, pszFormat, va);
208 va_end(va);
209
210 va_start(va, pszFormat);
211 LogRel(("%s: %N", g_pszProgName, pszFormat, &va));
212 va_end(va);
213 }
214}
215
216
217/**
218 * Gets a 32-bit value argument.
219 *
220 * @returns 0 on success, non-zero exit code on error.
221 * @param argc The argument count.
222 * @param argv The argument vector
223 * @param psz Where in *pi to start looking for the value argument.
224 * @param pi Where to find and perhaps update the argument index.
225 * @param pu32 Where to store the 32-bit value.
226 * @param u32Min The minimum value.
227 * @param u32Max The maximum value.
228 */
229int VBoxServiceArgUInt32(int argc, char **argv, const char *psz, int *pi, uint32_t *pu32, uint32_t u32Min, uint32_t u32Max)
230{
231 if (*psz == ':' || *psz == '=')
232 psz++;
233 if (!*psz)
234 {
235 if (*pi + 1 >= argc)
236 return VBoxServiceSyntax("Missing value for the '%s' argument\n", argv[*pi]);
237 psz = argv[++*pi];
238 }
239
240 char *pszNext;
241 int rc = RTStrToUInt32Ex(psz, &pszNext, 0, pu32);
242 if (RT_FAILURE(rc) || *pszNext)
243 return VBoxServiceSyntax("Failed to convert interval '%s' to a number.\n", psz);
244 if (*pu32 < u32Min || *pu32 > u32Max)
245 return VBoxServiceSyntax("The timesync interval of %RU32 secconds is out of range [%RU32..%RU32].\n",
246 *pu32, u32Min, u32Max);
247 return 0;
248}
249
250
251/**
252 * The service thread.
253 *
254 * @returns Whatever the worker function returns.
255 * @param ThreadSelf My thread handle.
256 * @param pvUser The service index.
257 */
258static DECLCALLBACK(int) VBoxServiceThread(RTTHREAD ThreadSelf, void *pvUser)
259{
260 const unsigned i = (uintptr_t)pvUser;
261
262#ifndef RT_OS_WINDOWS
263 /*
264 * Block all signals for this thread. Only the main thread will handle signals.
265 */
266 sigset_t signalMask;
267 sigfillset(&signalMask);
268 pthread_sigmask(SIG_BLOCK, &signalMask, NULL);
269#endif
270
271 int rc = g_aServices[i].pDesc->pfnWorker(&g_aServices[i].fShutdown);
272 ASMAtomicXchgBool(&g_aServices[i].fShutdown, true);
273 RTThreadUserSignal(ThreadSelf);
274 return rc;
275}
276
277
278unsigned VBoxServiceGetStartedServices(void)
279{
280 unsigned iMain = ~0U;
281 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
282 if (g_aServices[j].fEnabled)
283 {
284 iMain = j;
285 break;
286 }
287
288 return iMain; /* Return the index of the main service (must always come last!). */
289}
290
291/**
292 * Starts the service.
293 *
294 * @returns VBox status code, errors are fully bitched.
295 *
296 * @param iMain The index of the service that belongs to the main
297 * thread. Pass ~0U if none does.
298 */
299int VBoxServiceStartServices(unsigned iMain)
300{
301 int rc;
302
303 /*
304 * Initialize the services.
305 */
306 VBoxServiceVerbose(2, "Initializing services ...\n");
307 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
308 if (g_aServices[j].fEnabled)
309 {
310 rc = g_aServices[j].pDesc->pfnInit();
311 if (RT_FAILURE(rc))
312 {
313 VBoxServiceError("Service '%s' failed to initialize: %Rrc\n",
314 g_aServices[j].pDesc->pszName, rc);
315 return rc;
316 }
317 }
318
319 /*
320 * Start the service(s).
321 */
322 VBoxServiceVerbose(2, "Starting services ...\n");
323 rc = VINF_SUCCESS;
324 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
325 {
326 if ( !g_aServices[j].fEnabled
327 || j == iMain)
328 continue;
329
330 VBoxServiceVerbose(2, "Starting service '%s' ...\n", g_aServices[j].pDesc->pszName);
331 rc = RTThreadCreate(&g_aServices[j].Thread, VBoxServiceThread, (void *)(uintptr_t)j, 0,
332 RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, g_aServices[j].pDesc->pszName);
333 if (RT_FAILURE(rc))
334 {
335 VBoxServiceError("RTThreadCreate failed, rc=%Rrc\n", rc);
336 break;
337 }
338 g_aServices[j].fStarted = true;
339
340 /* wait for the thread to initialize */
341 RTThreadUserWait(g_aServices[j].Thread, 60 * 1000);
342 if (g_aServices[j].fShutdown)
343 {
344 VBoxServiceError("Service '%s' failed to start!\n", g_aServices[j].pDesc->pszName);
345 rc = VERR_GENERAL_FAILURE;
346 }
347 }
348 if ( RT_SUCCESS(rc)
349 && iMain != ~0U)
350 {
351 /* The final service runs in the main thread. */
352 VBoxServiceVerbose(1, "Starting '%s' in the main thread\n", g_aServices[iMain].pDesc->pszName);
353 rc = g_aServices[iMain].pDesc->pfnWorker(&g_fShutdown);
354 if (rc != VINF_SUCCESS) /* Only complain if service returned an error. Otherwise the service is a one-timer. */
355 {
356 VBoxServiceError("Service '%s' stopped unexpected; rc=%Rrc\n", g_aServices[iMain].pDesc->pszName, rc);
357 }
358 }
359 return rc;
360}
361
362
363/**
364 * Stops and terminates the services.
365 *
366 * This should be called even when VBoxServiceStartServices fails so it can
367 * clean up anything that we succeeded in starting.
368 */
369int VBoxServiceStopServices(void)
370{
371 int rc = VINF_SUCCESS;
372
373 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
374 ASMAtomicXchgBool(&g_aServices[j].fShutdown, true);
375 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
376 if (g_aServices[j].fStarted)
377 g_aServices[j].pDesc->pfnStop();
378 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
379 if (g_aServices[j].fEnabled)
380 {
381 if (g_aServices[j].Thread != NIL_RTTHREAD)
382 {
383 VBoxServiceVerbose(2, "Waiting for service '%s' to stop ...\n", g_aServices[j].pDesc->pszName);
384 for (int i = 0; i < 30; i++) /* Wait 30 seconds in total */
385 {
386 rc = RTThreadWait(g_aServices[j].Thread, 1000 /* Wait 1 second */, NULL);
387 if (RT_SUCCESS(rc))
388 break;
389#ifdef RT_OS_WINDOWS
390 /* Notify SCM that it takes a bit longer ... */
391 VBoxServiceWinSetStatus(SERVICE_STOP_PENDING, i);
392#endif
393 }
394 if (RT_FAILURE(rc))
395 VBoxServiceError("Service '%s' failed to stop. (%Rrc)\n", g_aServices[j].pDesc->pszName, rc);
396 }
397 VBoxServiceVerbose(3, "Terminating service '%s' (%d) ...\n", g_aServices[j].pDesc->pszName, j);
398 g_aServices[j].pDesc->pfnTerm();
399 }
400
401 VBoxServiceVerbose(2, "Stopping services returned: rc=%Rrc\n", rc);
402 return rc;
403}
404
405#ifndef RT_OS_WINDOWS
406/*
407 * Block all important signals, then explicitly wait until one of these signal arrives.
408 */
409static void VBoxServiceWaitSignal(void)
410{
411 sigset_t signalMask;
412 int iSignal;
413 sigemptyset(&signalMask);
414 sigaddset(&signalMask, SIGHUP);
415 sigaddset(&signalMask, SIGINT);
416 sigaddset(&signalMask, SIGQUIT);
417 sigaddset(&signalMask, SIGABRT);
418 sigaddset(&signalMask, SIGTERM);
419 pthread_sigmask(SIG_BLOCK, &signalMask, NULL);
420 sigwait(&signalMask, &iSignal);
421 VBoxServiceVerbose(3, "VBoxServiceWaitSignal: Received signal %d\n", iSignal);
422}
423#endif
424
425
426int main(int argc, char **argv)
427{
428 int rc = VINF_SUCCESS;
429 /*
430 * Init globals and such.
431 */
432 RTR3Init();
433
434 /*
435 * Connect to the kernel part before daemonizing so we can fail
436 * and complain if there is some kind of problem. We need to initialize
437 * the guest lib *before* we do the pre-init just in case one of services
438 * needs do to some initial stuff with it.
439 */
440 VBoxServiceVerbose(2, "Calling VbgR3Init()\n");
441 rc = VbglR3Init();
442 if (RT_FAILURE(rc))
443 return VBoxServiceError("VbglR3Init failed with rc=%Rrc.\n", rc);
444
445 /* Do pre-init of services. */
446 g_pszProgName = RTPathFilename(argv[0]);
447 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
448 {
449 rc = g_aServices[j].pDesc->pfnPreInit();
450 if (RT_FAILURE(rc))
451 return VBoxServiceError("Service '%s' failed pre-init: %Rrc\n", g_aServices[j].pDesc->pszName);
452 }
453
454#ifdef RT_OS_WINDOWS
455 /* Make sure only one instance of VBoxService runs at a time. Create a global mutex for that.
456 Do not use a global namespace ("Global\\") for mutex name here, will blow up NT4 compatibility! */
457 HANDLE hMutexAppRunning = CreateMutex (NULL, FALSE, VBOXSERVICE_NAME);
458 if ( hMutexAppRunning != NULL
459 && GetLastError() == ERROR_ALREADY_EXISTS)
460 {
461 VBoxServiceError("%s is already running! Terminating.", g_pszProgName);
462
463 /* Close the mutex for this application instance. */
464 CloseHandle(hMutexAppRunning);
465 hMutexAppRunning = NULL;
466 }
467#endif
468
469 /*
470 * Parse the arguments.
471 */
472 bool fDaemonize = true;
473 bool fDaemonized = false;
474 for (int i = 1; i < argc; i++)
475 {
476 const char *psz = argv[i];
477 if (*psz != '-')
478 return VBoxServiceSyntax("Unknown argument '%s'\n", psz);
479 psz++;
480
481 /* translate long argument to short */
482 if (*psz == '-')
483 {
484 psz++;
485 size_t cch = strlen(psz);
486#define MATCHES(strconst) ( cch == sizeof(strconst) - 1 \
487 && !memcmp(psz, strconst, sizeof(strconst) - 1) )
488 if (MATCHES("foreground"))
489 psz = "f";
490 else if (MATCHES("verbose"))
491 psz = "v";
492 else if (MATCHES("help"))
493 psz = "h";
494 else if (MATCHES("interval"))
495 psz = "i";
496#ifdef RT_OS_WINDOWS
497 else if (MATCHES("register"))
498 psz = "r";
499 else if (MATCHES("unregister"))
500 psz = "u";
501#endif
502 else if (MATCHES("daemonized"))
503 {
504 fDaemonized = true;
505 continue;
506 }
507 else
508 {
509 bool fFound = false;
510
511 if (cch > sizeof("enable-") && !memcmp(psz, "enable-", sizeof("enable-") - 1))
512 for (unsigned j = 0; !fFound && j < RT_ELEMENTS(g_aServices); j++)
513 if ((fFound = !RTStrICmp(psz + sizeof("enable-") - 1, g_aServices[j].pDesc->pszName)))
514 g_aServices[j].fEnabled = true;
515
516 if (cch > sizeof("disable-") && !memcmp(psz, "disable-", sizeof("disable-") - 1))
517 for (unsigned j = 0; !fFound && j < RT_ELEMENTS(g_aServices); j++)
518 if ((fFound = !RTStrICmp(psz + sizeof("disable-") - 1, g_aServices[j].pDesc->pszName)))
519 g_aServices[j].fEnabled = false;
520
521 if (!fFound)
522 for (unsigned j = 0; !fFound && j < RT_ELEMENTS(g_aServices); j++)
523 {
524 rc = g_aServices[j].pDesc->pfnOption(NULL, argc, argv, &i);
525 fFound = rc == 0;
526 if (fFound)
527 break;
528 if (rc != -1)
529 return rc;
530 }
531 if (!fFound)
532 return VBoxServiceSyntax("Unknown option '%s'\n", argv[i]);
533 continue;
534 }
535#undef MATCHES
536 }
537
538 /* handle the string of short options. */
539 do
540 {
541 switch (*psz)
542 {
543 case 'i':
544 rc = VBoxServiceArgUInt32(argc, argv, psz + 1, &i,
545 &g_DefaultInterval, 1, (UINT32_MAX / 1000) - 1);
546 if (rc)
547 return rc;
548 psz = NULL;
549 break;
550
551 case 'f':
552 fDaemonize = false;
553 break;
554
555 case 'v':
556 g_cVerbosity++;
557 break;
558
559 case 'h':
560 case '?':
561 return VBoxServiceUsage();
562
563#ifdef RT_OS_WINDOWS
564 case 'r':
565 return VBoxServiceWinInstall();
566
567 case 'u':
568 return VBoxServiceWinUninstall();
569#endif
570
571 default:
572 {
573 bool fFound = false;
574 for (unsigned j = 0; j < RT_ELEMENTS(g_aServices); j++)
575 {
576 rc = g_aServices[j].pDesc->pfnOption(&psz, argc, argv, &i);
577 fFound = rc == 0;
578 if (fFound)
579 break;
580 if (rc != -1)
581 return rc;
582 }
583 if (!fFound)
584 return VBoxServiceSyntax("Unknown option '%c' (%s)\n", *psz, argv[i]);
585 break;
586 }
587 }
588 } while (psz && *++psz);
589 }
590 /*
591 * Check that at least one service is enabled.
592 */
593 unsigned iMain = VBoxServiceGetStartedServices();
594 if (iMain == ~0U)
595 return VBoxServiceSyntax("At least one service must be enabled.\n");
596
597#ifndef RT_OS_WINDOWS
598 /*
599 * POSIX: No main service thread.
600 */
601 iMain = ~0U;
602#endif
603
604 VBoxServiceVerbose(0, "%s r%s started. Verbose level = %d\n",
605 RTBldCfgVersion(), RTBldCfgRevisionStr(), g_cVerbosity);
606
607 /*
608 * Daemonize if requested.
609 */
610 if (fDaemonize && !fDaemonized)
611 {
612#ifdef RT_OS_WINDOWS
613 /** @todo Should do something like VBoxSVC here, OR automatically re-register
614 * the service and start it. Involving VbglR3Daemonize isn't an option
615 * here.
616 *
617 * Also, the idea here, IIRC, was to map the sub service to windows
618 * services. The todo below is for mimicking windows services on
619 * non-windows systems. Not sure if this is doable or not, but in anycase
620 * this code can be moved into -win.
621 *
622 * You should return when StartServiceCtrlDispatcher, btw., not
623 * continue.
624 */
625 VBoxServiceVerbose(2, "Starting service dispatcher ...\n");
626 if (!StartServiceCtrlDispatcher(&g_aServiceTable[0]))
627 return VBoxServiceError("StartServiceCtrlDispatcher: %u. Please start %s with option -f (foreground)!",
628 GetLastError(), g_pszProgName);
629 /* Service now lives in the control dispatcher registered above. */
630#else
631 VBoxServiceVerbose(1, "Daemonizing...\n");
632 rc = VbglR3Daemonize(false /* fNoChDir */, false /* fNoClose */);
633 if (RT_FAILURE(rc))
634 return VBoxServiceError("Daemon failed: %Rrc\n", rc);
635 /* in-child */
636#endif
637 }
638#ifdef RT_OS_WINDOWS
639 else
640 {
641 /* Run the app just like a console one if not daemonized. */
642#endif
643 /*
644 * Windows: Start the services, enter the main threads' run loop and stop them
645 * again when it returns.
646 *
647 * POSIX: Start all services and return immediately.
648 */
649 rc = VBoxServiceStartServices(iMain);
650#ifndef RT_OS_WINDOWS
651 VBoxServiceWaitSignal();
652#endif
653 VBoxServiceStopServices();
654#ifdef RT_OS_WINDOWS
655 }
656#endif
657
658#ifdef RT_OS_WINDOWS
659 /*
660 * Release instance mutex if we got it.
661 */
662 if (hMutexAppRunning != NULL)
663 {
664 ::CloseHandle(hMutexAppRunning);
665 hMutexAppRunning = NULL;
666 }
667#endif
668
669 VBoxServiceVerbose(0, "Ended.\n");
670 return RT_SUCCESS(rc) ? 0 : 1;
671}
672
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