VirtualBox

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

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

VBoxService: Compilable/runnable without memory ballooning.

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

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