VirtualBox

source: vbox/trunk/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp@ 33790

Last change on this file since 33790 was 33790, checked in by vboxsync, 14 years ago

VBoxManage/Guest Copy: Documentation, renamed ctrlCopyFile -> ctrlCopyFileToGuest.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 46.3 KB
Line 
1/* $Id: VBoxManageGuestCtrl.cpp 33790 2010-11-05 12:39:23Z vboxsync $ */
2/** @file
3 * VBoxManage - Implementation of guestcontrol command.
4 */
5
6/*
7 * Copyright (C) 2010 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 "VBoxManage.h"
23
24#ifndef VBOX_ONLY_DOCS
25
26#include <VBox/com/com.h>
27#include <VBox/com/string.h>
28#include <VBox/com/array.h>
29#include <VBox/com/ErrorInfo.h>
30#include <VBox/com/errorprint.h>
31
32#include <VBox/com/VirtualBox.h>
33#include <VBox/com/EventQueue.h>
34
35#include <VBox/HostServices/GuestControlSvc.h> /* for PROC_STS_XXX */
36
37#include <iprt/asm.h>
38#include <iprt/dir.h>
39#include <iprt/file.h>
40#include <iprt/isofs.h>
41#include <iprt/getopt.h>
42#include <iprt/list.h>
43#include <iprt/path.h>
44
45#ifdef USE_XPCOM_QUEUE
46# include <sys/select.h>
47# include <errno.h>
48#endif
49
50#include <signal.h>
51
52#ifdef RT_OS_DARWIN
53# include <CoreFoundation/CFRunLoop.h>
54#endif
55
56using namespace com;
57
58/**
59 * IVirtualBoxCallback implementation for handling the GuestControlCallback in
60 * relation to the "guestcontrol * wait" command.
61 */
62/** @todo */
63
64/** Set by the signal handler. */
65static volatile bool g_fExecCanceled = false;
66static volatile bool g_fCopyCanceled = false;
67
68/*
69 * Structure holding a directory entry.
70 */
71typedef struct DIRECTORYENTRY
72{
73 char *pszSourcePath;
74 char *pszDestPath;
75 RTLISTNODE Node;
76} DIRECTORYENTRY, *PDIRECTORYENTRY;
77
78#endif /* VBOX_ONLY_DOCS */
79
80void usageGuestControl(PRTSTREAM pStrm)
81{
82 RTStrmPrintf(pStrm,
83 "VBoxManage guestcontrol execute <vmname>|<uuid>\n"
84 " <path to program>\n"
85 " --username <name> --password <password>\n"
86 " [--arguments \"<arguments>\"]\n"
87 " [--environment \"<NAME>=<VALUE> [<NAME>=<VALUE>]\"]\n"
88 " [--flags <flags>] [--timeout <msec>]\n"
89 " [--verbose] [--wait-for exit,stdout,stderr||]\n"
90 /** @todo Add a "--" parameter (has to be last parameter) to directly execute
91 * stuff, e.g. "VBoxManage guestcontrol execute <VMName> --username <> ... -- /bin/rm -Rf /foo". */
92 "\n"
93 " copyto <vmname>|<uuid>\n"
94 " <source on host> <destination on guest>\n"
95 " --username <name> --password <password>\n"
96 " [--dryrun] [--recursive] [--verbose] [--flags <flags>]\n"
97 "\n"
98 " updateadditions <vmname>|<uuid>\n"
99 " [--source <guest additions .ISO file to use>] [--verbose]\n"
100 "\n");
101}
102
103#ifndef VBOX_ONLY_DOCS
104
105/**
106 * Signal handler that sets g_fCanceled.
107 *
108 * This can be executed on any thread in the process, on Windows it may even be
109 * a thread dedicated to delivering this signal. Do not doing anything
110 * unnecessary here.
111 */
112static void ctrlExecProcessSignalHandler(int iSignal)
113{
114 NOREF(iSignal);
115 ASMAtomicWriteBool(&g_fExecCanceled, true);
116}
117
118static const char *ctrlExecGetStatus(ULONG uStatus)
119{
120 switch (uStatus)
121 {
122 case guestControl::PROC_STS_STARTED:
123 return "started";
124 case guestControl::PROC_STS_TEN:
125 return "successfully terminated";
126 case guestControl::PROC_STS_TES:
127 return "terminated by signal";
128 case guestControl::PROC_STS_TEA:
129 return "abnormally aborted";
130 case guestControl::PROC_STS_TOK:
131 return "timed out";
132 case guestControl::PROC_STS_TOA:
133 return "timed out, hanging";
134 case guestControl::PROC_STS_DWN:
135 return "killed";
136 case guestControl::PROC_STS_ERROR:
137 return "error";
138 default:
139 return "unknown";
140 }
141}
142
143static int handleCtrlExecProgram(HandlerArg *a)
144{
145 /*
146 * Check the syntax. We can deduce the correct syntax from the number of
147 * arguments.
148 */
149 if (a->argc < 2) /* At least the command we want to execute in the guest should be present :-). */
150 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
151
152 Utf8Str Utf8Cmd(a->argv[1]);
153 uint32_t uFlags = 0;
154 /* Note: this uses IN_BSTR as it must be BSTR on COM and CBSTR on XPCOM */
155 com::SafeArray<IN_BSTR> args;
156 com::SafeArray<IN_BSTR> env;
157 Utf8Str Utf8UserName;
158 Utf8Str Utf8Password;
159 uint32_t u32TimeoutMS = 0;
160 bool fWaitForExit = false;
161 bool fWaitForStdOut = false;
162 bool fWaitForStdErr = false;
163 bool fVerbose = false;
164 bool fTimeout = false;
165
166 /* Always use the actual command line as argv[0]. */
167 args.push_back(Bstr(Utf8Cmd).raw());
168
169 /* Iterate through all possible commands (if available). */
170 bool usageOK = true;
171 for (int i = 2; usageOK && i < a->argc; i++)
172 {
173 if ( !strcmp(a->argv[i], "--arguments")
174 || !strcmp(a->argv[i], "--args")
175 || !strcmp(a->argv[i], "--arg"))
176 {
177 if (i + 1 >= a->argc)
178 usageOK = false;
179 else
180 {
181 char **papszArg;
182 int cArgs;
183
184 int vrc = RTGetOptArgvFromString(&papszArg, &cArgs, a->argv[i + 1], NULL);
185 if (RT_SUCCESS(vrc))
186 {
187 for (int j = 0; j < cArgs; j++)
188 args.push_back(Bstr(papszArg[j]).raw());
189
190 RTGetOptArgvFree(papszArg);
191 }
192 ++i;
193 }
194 }
195 else if ( !strcmp(a->argv[i], "--environment")
196 || !strcmp(a->argv[i], "--env"))
197 {
198 if (i + 1 >= a->argc)
199 usageOK = false;
200 else
201 {
202 char **papszArg;
203 int cArgs;
204
205 int vrc = RTGetOptArgvFromString(&papszArg, &cArgs, a->argv[i + 1], NULL);
206 if (RT_SUCCESS(vrc))
207 {
208 for (int j = 0; j < cArgs; j++)
209 env.push_back(Bstr(papszArg[j]).raw());
210
211 RTGetOptArgvFree(papszArg);
212 }
213 ++i;
214 }
215 }
216 else if (!strcmp(a->argv[i], "--flags"))
217 {
218 if (i + 1 >= a->argc)
219 usageOK = false;
220 else
221 {
222 /** @todo Needs a bit better processing as soon as we have more flags. */
223 if (!strcmp(a->argv[i + 1], "ignoreorphanedprocesses"))
224 uFlags |= ExecuteProcessFlag_IgnoreOrphanedProcesses;
225 else
226 usageOK = false;
227 ++i;
228 }
229 }
230 else if ( !strcmp(a->argv[i], "--username")
231 || !strcmp(a->argv[i], "--user"))
232 {
233 if (i + 1 >= a->argc)
234 usageOK = false;
235 else
236 {
237 Utf8UserName = a->argv[i + 1];
238 ++i;
239 }
240 }
241 else if ( !strcmp(a->argv[i], "--password")
242 || !strcmp(a->argv[i], "--pwd"))
243 {
244 if (i + 1 >= a->argc)
245 usageOK = false;
246 else
247 {
248 Utf8Password = a->argv[i + 1];
249 ++i;
250 }
251 }
252 else if (!strcmp(a->argv[i], "--timeout"))
253 {
254 if ( i + 1 >= a->argc
255 || RTStrToUInt32Full(a->argv[i + 1], 10, &u32TimeoutMS) != VINF_SUCCESS
256 || u32TimeoutMS == 0)
257 {
258 usageOK = false;
259 }
260 else
261 {
262 fTimeout = true;
263 ++i;
264 }
265 }
266 else if (!strcmp(a->argv[i], "--wait-for"))
267 {
268 if (i + 1 >= a->argc)
269 usageOK = false;
270 else
271 {
272 if (!strcmp(a->argv[i + 1], "exit"))
273 fWaitForExit = true;
274 else if (!strcmp(a->argv[i + 1], "stdout"))
275 {
276 fWaitForExit = true;
277 fWaitForStdOut = true;
278 }
279 else if (!strcmp(a->argv[i + 1], "stderr"))
280 {
281 fWaitForExit = true;
282 fWaitForStdErr = true;
283 }
284 else
285 usageOK = false;
286 ++i;
287 }
288 }
289 else if (!strcmp(a->argv[i], "--verbose"))
290 fVerbose = true;
291 /** @todo Add fancy piping stuff here. */
292 else
293 return errorSyntax(USAGE_GUESTCONTROL,
294 "Invalid parameter '%s'", Utf8Str(a->argv[i]).c_str());
295 }
296
297 if (!usageOK)
298 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
299
300 if (Utf8UserName.isEmpty())
301 return errorSyntax(USAGE_GUESTCONTROL,
302 "No user name specified!");
303
304 /* Lookup VM. */
305 ComPtr<IMachine> machine;
306 /* Assume it's an UUID. */
307 HRESULT rc;
308 CHECK_ERROR(a->virtualBox, FindMachine(Bstr(a->argv[0]).raw(),
309 machine.asOutParam()));
310 if (FAILED(rc))
311 return 1;
312
313 /* Machine is running? */
314 MachineState_T machineState;
315 CHECK_ERROR_RET(machine, COMGETTER(State)(&machineState), 1);
316 if (machineState != MachineState_Running)
317 {
318 RTMsgError("Machine \"%s\" is not running!\n", a->argv[0]);
319 return 1;
320 }
321
322 /* Open a session for the VM. */
323 CHECK_ERROR_RET(machine, LockMachine(a->session, LockType_Shared), 1);
324
325 do
326 {
327 /* Get the associated console. */
328 ComPtr<IConsole> console;
329 CHECK_ERROR_BREAK(a->session, COMGETTER(Console)(console.asOutParam()));
330 /* ... and session machine */
331 ComPtr<IMachine> sessionMachine;
332 CHECK_ERROR_BREAK(a->session, COMGETTER(Machine)(sessionMachine.asOutParam()));
333
334 ComPtr<IGuest> guest;
335 CHECK_ERROR_BREAK(console, COMGETTER(Guest)(guest.asOutParam()));
336
337 ComPtr<IProgress> progress;
338 ULONG uPID = 0;
339
340 if (fVerbose)
341 {
342 if (u32TimeoutMS == 0)
343 RTPrintf("Waiting for guest to start process ...\n");
344 else
345 RTPrintf("Waiting for guest to start process (within %ums)\n", u32TimeoutMS);
346 }
347
348 /* Get current time stamp to later calculate rest of timeout left. */
349 uint64_t u64StartMS = RTTimeMilliTS();
350
351 /* Execute the process. */
352 rc = guest->ExecuteProcess(Bstr(Utf8Cmd).raw(), uFlags,
353 ComSafeArrayAsInParam(args),
354 ComSafeArrayAsInParam(env),
355 Bstr(Utf8UserName).raw(),
356 Bstr(Utf8Password).raw(), u32TimeoutMS,
357 &uPID, progress.asOutParam());
358 if (FAILED(rc))
359 {
360 /* If we got a VBOX_E_IPRT error we handle the error in a more gentle way
361 * because it contains more accurate info about what went wrong. */
362 ErrorInfo info(guest, COM_IIDOF(IGuest));
363 if (info.isFullAvailable())
364 {
365 if (rc == VBOX_E_IPRT_ERROR)
366 RTMsgError("%ls.", info.getText().raw());
367 else
368 RTMsgError("%ls (%Rhrc).", info.getText().raw(), info.getResultCode());
369 }
370 break;
371 }
372 if (fVerbose)
373 RTPrintf("Process '%s' (PID: %u) started\n", Utf8Cmd.c_str(), uPID);
374 if (fWaitForExit)
375 {
376 if (fTimeout)
377 {
378 /* Calculate timeout value left after process has been started. */
379 uint64_t u64Elapsed = RTTimeMilliTS() - u64StartMS;
380 /* Is timeout still bigger than current difference? */
381 if (u32TimeoutMS > u64Elapsed)
382 {
383 u32TimeoutMS -= (uint32_t)u64Elapsed;
384 if (fVerbose)
385 RTPrintf("Waiting for process to exit (%ums left) ...\n", u32TimeoutMS);
386 }
387 else
388 {
389 if (fVerbose)
390 RTPrintf("No time left to wait for process!\n");
391 }
392 }
393 else if (fVerbose)
394 RTPrintf("Waiting for process to exit ...\n");
395
396 /* Setup signal handling if cancelable. */
397 ASSERT(progress);
398 bool fCanceledAlready = false;
399 BOOL fCancelable;
400 HRESULT hrc = progress->COMGETTER(Cancelable)(&fCancelable);
401 if (FAILED(hrc))
402 fCancelable = FALSE;
403 if (fCancelable)
404 {
405 signal(SIGINT, ctrlExecProcessSignalHandler);
406 #ifdef SIGBREAK
407 signal(SIGBREAK, ctrlExecProcessSignalHandler);
408 #endif
409 }
410
411 /* Wait for process to exit ... */
412 BOOL fCompleted = FALSE;
413 BOOL fCanceled = FALSE;
414 while (SUCCEEDED(progress->COMGETTER(Completed(&fCompleted))))
415 {
416 SafeArray<BYTE> aOutputData;
417 ULONG cbOutputData = 0;
418
419 /*
420 * Some data left to output?
421 */
422 if ( fWaitForStdOut
423 || fWaitForStdErr)
424 {
425 rc = guest->GetProcessOutput(uPID, 0 /* aFlags */,
426 u32TimeoutMS, _64K, ComSafeArrayAsOutParam(aOutputData));
427 if (FAILED(rc))
428 {
429 /* If we got a VBOX_E_IPRT error we handle the error in a more gentle way
430 * because it contains more accurate info about what went wrong. */
431 ErrorInfo info(guest, COM_IIDOF(IGuest));
432 if (info.isFullAvailable())
433 {
434 if (rc == VBOX_E_IPRT_ERROR)
435 RTMsgError("%ls.", info.getText().raw());
436 else
437 RTMsgError("%ls (%Rhrc).", info.getText().raw(), info.getResultCode());
438 }
439 cbOutputData = 0;
440 fCompleted = true; /* rc contains a failure, so we'll go into aborted state down below. */
441 }
442 else
443 {
444 cbOutputData = aOutputData.size();
445 if (cbOutputData > 0)
446 {
447 /* aOutputData has a platform dependent line ending, standardize on
448 * Unix style, as RTStrmWrite does the LF -> CR/LF replacement on
449 * Windows. Otherwise we end up with CR/CR/LF on Windows. */
450 ULONG cbOutputDataPrint = cbOutputData;
451 for (BYTE *s = aOutputData.raw(), *d = s;
452 s - aOutputData.raw() < (ssize_t)cbOutputData;
453 s++, d++)
454 {
455 if (*s == '\r')
456 {
457 /* skip over CR, adjust destination */
458 d--;
459 cbOutputDataPrint--;
460 }
461 else if (s != d)
462 *d = *s;
463 }
464 RTStrmWrite(g_pStdOut, aOutputData.raw(), cbOutputDataPrint);
465 }
466 }
467 }
468 if (cbOutputData <= 0) /* No more output data left? */
469 {
470 if (fCompleted)
471 break;
472
473 if ( fTimeout
474 && RTTimeMilliTS() - u64StartMS > u32TimeoutMS + 5000)
475 {
476 progress->Cancel();
477 break;
478 }
479 }
480
481 /* Process async cancelation */
482 if (g_fExecCanceled && !fCanceledAlready)
483 {
484 hrc = progress->Cancel();
485 if (SUCCEEDED(hrc))
486 fCanceledAlready = TRUE;
487 else
488 g_fExecCanceled = false;
489 }
490
491 /* Progress canceled by Main API? */
492 if ( SUCCEEDED(progress->COMGETTER(Canceled(&fCanceled)))
493 && fCanceled)
494 {
495 break;
496 }
497 }
498
499 /* Undo signal handling */
500 if (fCancelable)
501 {
502 signal(SIGINT, SIG_DFL);
503 #ifdef SIGBREAK
504 signal(SIGBREAK, SIG_DFL);
505 #endif
506 }
507
508 if (fCanceled)
509 {
510 if (fVerbose)
511 RTPrintf("Process execution canceled!\n");
512 }
513 else if ( fCompleted
514 && SUCCEEDED(rc))
515 {
516 LONG iRc = false;
517 CHECK_ERROR_RET(progress, COMGETTER(ResultCode)(&iRc), rc);
518 if (FAILED(iRc))
519 {
520 com::ProgressErrorInfo info(progress);
521 if ( info.isFullAvailable()
522 || info.isBasicAvailable())
523 {
524 /* If we got a VBOX_E_IPRT error we handle the error in a more gentle way
525 * because it contains more accurate info about what went wrong. */
526 if (info.getResultCode() == VBOX_E_IPRT_ERROR)
527 RTMsgError("%ls.", info.getText().raw());
528 else
529 {
530 RTMsgError("Process error details:");
531 GluePrintErrorInfo(info);
532 }
533 }
534 else
535 com::GluePrintRCMessage(iRc);
536 }
537 else if (fVerbose)
538 {
539 ULONG uRetStatus, uRetExitCode, uRetFlags;
540 rc = guest->GetProcessStatus(uPID, &uRetExitCode, &uRetFlags, &uRetStatus);
541 if (SUCCEEDED(rc))
542 RTPrintf("Exit code=%u (Status=%u [%s], Flags=%u)\n", uRetExitCode, uRetStatus, ctrlExecGetStatus(uRetStatus), uRetFlags);
543 }
544 }
545 else
546 {
547 if (fVerbose)
548 RTPrintf("Process execution aborted!\n");
549 }
550 }
551 a->session->UnlockMachine();
552 } while (0);
553 return SUCCEEDED(rc) ? 0 : 1;
554}
555
556/**
557 * Signal handler that sets g_fCopyCanceled.
558 *
559 * This can be executed on any thread in the process, on Windows it may even be
560 * a thread dedicated to delivering this signal. Do not doing anything
561 * unnecessary here.
562 */
563static void ctrlCopySignalHandler(int iSignal)
564{
565 NOREF(iSignal);
566 ASMAtomicWriteBool(&g_fCopyCanceled, true);
567}
568
569/**
570 * Appends a new to-copy object to a copy list.
571 *
572 * @return IPRT status code.
573 * @param pszFileSource Full qualified source path of file to copy.
574 * @param pszFileDest Full qualified destination path.
575 * @param pList Copy list used for insertion.
576 */
577int ctrlCopyDirectoryEntryAppend(const char *pszFileSource, const char *pszFileDest,
578 PRTLISTNODE pList)
579{
580 AssertPtrReturn(pszFileSource, VERR_INVALID_POINTER);
581 AssertPtrReturn(pszFileDest, VERR_INVALID_POINTER);
582 AssertPtrReturn(pList, VERR_INVALID_POINTER);
583
584 PDIRECTORYENTRY pNode = (PDIRECTORYENTRY)RTMemAlloc(sizeof(DIRECTORYENTRY));
585 if (pNode == NULL)
586 return VERR_NO_MEMORY;
587
588 pNode->pszSourcePath = NULL;
589 pNode->pszDestPath = NULL;
590 if (RT_SUCCESS(RTStrAAppend(&pNode->pszSourcePath, pszFileSource)))
591 {
592 if (RT_SUCCESS(RTStrAAppend(&pNode->pszDestPath, pszFileDest)))
593 {
594 pNode->Node.pPrev = NULL;
595 pNode->Node.pNext = NULL;
596 RTListAppend(pList, &pNode->Node);
597 return VINF_SUCCESS;
598 }
599 return VERR_NO_MEMORY;
600 }
601 return VERR_NO_MEMORY;
602}
603
604/**
605 * Reads a specified directory (recursively) based on the copy flags
606 * and appends all matching entries to the supplied list.
607 *
608 * @return IPRT status code.
609 * @param pszRootDir Directory to start with. Must end with
610 * a trailing slash and must be absolute.
611 * @param pszSubDir Sub directory part relative to the root
612 * directory; needed for recursion.
613 * @param pszFilter Search filter (e.g. *.pdf).
614 * @param uFlags Copy flags.
615 * @param pcObjects Where to store the overall objects to
616 * copy found.
617 * @param pList Pointer to the object list to use.
618 */
619int ctrlCopyDirectoryRead(const char *pszRootDir, const char *pszSubDir, const char *pszFilter,
620 uint32_t uFlags, uint32_t *pcObjects, PRTLISTNODE pList)
621{
622 AssertPtrReturn(pszRootDir, VERR_INVALID_POINTER);
623 /* Sub directory is optional. */
624 /* Filter directory is optional. */
625 AssertPtrReturn(pcObjects, VERR_INVALID_POINTER);
626 AssertPtrReturn(pList, VERR_INVALID_POINTER);
627
628 PRTDIR pDir = NULL;
629
630 int rc = VINF_SUCCESS;
631 char szCurDir[RTPATH_MAX];
632 if (RTStrPrintf(szCurDir, sizeof(szCurDir), pszRootDir))
633 {
634 if (pszSubDir != NULL)
635 rc = RTPathAppend(szCurDir, sizeof(szCurDir), pszSubDir);
636 if (RT_SUCCESS(rc) && pszFilter != NULL)
637 rc = RTPathAppend(szCurDir, sizeof(szCurDir), pszFilter);
638 }
639 else
640 rc = VERR_NO_MEMORY;
641
642 if (pszFilter)
643 rc = RTDirOpenFiltered(&pDir, szCurDir,
644#ifdef RT_OS_WINDOWS
645 RTDIRFILTER_WINNT);
646#else
647 RTDIRFILTER_UNIX);
648#endif
649 else
650 rc = RTDirOpen(&pDir, szCurDir);
651
652 if (RT_SUCCESS(rc))
653 {
654 for (;;)
655 {
656 RTDIRENTRY DirEntry;
657 rc = RTDirRead(pDir, &DirEntry, NULL);
658 if (RT_FAILURE(rc))
659 {
660 if (rc == VERR_NO_MORE_FILES)
661 rc = VINF_SUCCESS;
662 break;
663 }
664 switch (DirEntry.enmType)
665 {
666 case RTDIRENTRYTYPE_DIRECTORY:
667 /* Skip "." and ".." entrires. */
668 if ( !strcmp(DirEntry.szName, ".")
669 || !strcmp(DirEntry.szName, ".."))
670 {
671 break;
672 }
673 if (uFlags & CopyFileFlag_Recursive)
674 {
675 char *pszNewSub = NULL;
676 if (pszSubDir)
677 RTStrAPrintf(&pszNewSub, "%s%s/", pszSubDir, DirEntry.szName);
678 else
679 RTStrAPrintf(&pszNewSub, "%s/", DirEntry.szName);
680
681 if (pszNewSub)
682 {
683 rc = ctrlCopyDirectoryRead(pszRootDir, pszNewSub, pszFilter,
684 uFlags, pcObjects, pList);
685 RTStrFree(pszNewSub);
686 }
687 else
688 rc = VERR_NO_MEMORY;
689 }
690 break;
691
692 case RTDIRENTRYTYPE_FILE:
693 {
694 char *pszFileSource = NULL;
695 char *pszFileDest = NULL;
696 if (RTStrAPrintf(&pszFileSource, "%s%s%s",
697 pszRootDir, pszSubDir ? pszSubDir : "",
698 DirEntry.szName) >= 0)
699 {
700 if (RTStrAPrintf(&pszFileDest, "%s%s",
701 pszSubDir ? pszSubDir : "",
702 DirEntry.szName) <= 0)
703 {
704 rc = VERR_NO_MEMORY;
705 }
706 }
707 else
708 rc = VERR_NO_MEMORY;
709
710 if (RT_SUCCESS(rc))
711 {
712 rc = ctrlCopyDirectoryEntryAppend(pszFileSource, pszFileDest, pList);
713 if (RT_SUCCESS(rc))
714 *pcObjects = *pcObjects + 1;
715 }
716
717 if (pszFileSource)
718 RTStrFree(pszFileSource);
719 if (pszFileDest)
720 RTStrFree(pszFileDest);
721 break;
722 }
723
724 case RTDIRENTRYTYPE_SYMLINK:
725 if ( (uFlags & CopyFileFlag_Recursive)
726 && (uFlags & CopyFileFlag_FollowLinks))
727 {
728 /* TODO */
729 }
730 break;
731
732 default:
733 break;
734 }
735 if (RT_FAILURE(rc))
736 break;
737 }
738 }
739
740 if (pDir)
741 RTDirClose(pDir);
742 return rc;
743}
744
745/**
746 * Initializes the copy process and builds up an object list
747 * with all required information to start the actual copy process.
748 *
749 * @return IPRT status code.
750 * @param pszSource Source path on host to use.
751 * @param pszDest Destination path on guest to use.
752 * @param uFlags Copy flags.
753 * @param pcObjects Where to store the count of objects to be copied.
754 * @param pList Where to store the object list.
755 */
756int ctrlCopyInit(const char *pszSource, const char *pszDest, uint32_t uFlags,
757 uint32_t *pcObjects, PRTLISTNODE pList)
758{
759 AssertPtrReturn(pszSource, VERR_INVALID_PARAMETER);
760 AssertPtrReturn(pszDest, VERR_INVALID_PARAMETER);
761 AssertPtrReturn(pcObjects, VERR_INVALID_PARAMETER);
762 AssertPtrReturn(pList, VERR_INVALID_PARAMETER);
763
764 int rc = VINF_SUCCESS;
765 char *pszSourceAbs = RTPathAbsDup(pszSource);
766 if (pszSourceAbs)
767 {
768 if ( RTPathFilename(pszSourceAbs)
769 && RTFileExists(pszSourceAbs)) /* We have a single file ... */
770 {
771 RTListInit(pList);
772 rc = ctrlCopyDirectoryEntryAppend(pszSourceAbs, pszDest, pList);
773 *pcObjects = 1;
774 }
775 else /* ... or a directory. */
776 {
777 /* Append trailing slash to absolute directory. */
778 if (RTDirExists(pszSourceAbs))
779 RTStrAAppend(&pszSourceAbs, RTPATH_SLASH_STR);
780
781 /* Extract directory filter (e.g. "*.exe"). */
782 char *pszFilter = RTPathFilename(pszSourceAbs);
783 char *pszSourceAbsRoot = RTStrDup(pszSourceAbs);
784 if (pszSourceAbsRoot)
785 {
786 if (pszFilter)
787 {
788 RTPathStripFilename(pszSourceAbsRoot);
789 RTStrAAppend(&pszSourceAbsRoot, RTPATH_SLASH_STR);
790 }
791 else
792 {
793 /*
794 * If we have more than one file to copy, make sure that we have
795 * a trailing slash so that we can construct a full path name
796 * (e.g. "foo.txt" -> "c:/foo/temp.txt") as destination.
797 */
798 size_t cch = strlen(pszSourceAbsRoot);
799 if ( cch > 1
800 && !RTPATH_IS_SLASH(pszSourceAbsRoot[cch - 1])
801 && !RTPATH_IS_SLASH(pszSourceAbsRoot[cch - 2]))
802 {
803 rc = RTStrAAppend(&pszSourceAbsRoot, RTPATH_SLASH_STR);
804 }
805 }
806
807 if (RT_SUCCESS(rc))
808 {
809 RTListInit(pList);
810 rc = ctrlCopyDirectoryRead(pszSourceAbsRoot, NULL /* Sub directory */,
811 pszFilter,
812 uFlags, pcObjects, pList);
813 if (RT_SUCCESS(rc) && *pcObjects == 0)
814 rc = VERR_NOT_FOUND;
815 }
816
817 RTStrFree(pszSourceAbsRoot);
818 }
819 else
820 rc = VERR_NO_MEMORY;
821 }
822 RTStrFree(pszSourceAbs);
823 }
824 else
825 rc = VERR_NO_MEMORY;
826 return rc;
827}
828
829/**
830 * Destroys a copy list.
831 */
832void ctrlCopyDestroy(PRTLISTNODE pList)
833{
834 AssertPtr(pList);
835
836 /* Destroy file list. */
837 PDIRECTORYENTRY pNode = RTListNodeGetFirst(pList, DIRECTORYENTRY, Node);
838 while (pNode)
839 {
840 PDIRECTORYENTRY pNext = RTListNodeGetNext(&pNode->Node, DIRECTORYENTRY, Node);
841 bool fLast = RTListNodeIsLast(pList, &pNode->Node);
842
843 if (pNode->pszSourcePath)
844 RTStrFree(pNode->pszSourcePath);
845 if (pNode->pszDestPath)
846 RTStrFree(pNode->pszDestPath);
847 RTListNodeRemove(&pNode->Node);
848 RTMemFree(pNode);
849
850 if (fLast)
851 break;
852
853 pNode = pNext;
854 }
855}
856
857/**
858 * Copys a file from host to the guest.
859 *
860 * @return IPRT status code.
861 * @param pGuest IGuest interface pointer.
862 * @param pszSource Source path of existing host file to copy.
863 * @param pszDest Destination path on guest to copy the file to.
864 * @param pszUserName User name on guest to use for the copy operation.
865 * @param pszPassword Password of user account.
866 * @param uFlags Copy flags.
867 */
868int ctrlCopyFileToGuest(IGuest *pGuest, const char *pszSource, const char *pszDest,
869 const char *pszUserName, const char *pszPassword,
870 uint32_t uFlags)
871{
872 AssertPtrReturn(pszSource, VERR_INVALID_PARAMETER);
873 AssertPtrReturn(pszDest, VERR_INVALID_PARAMETER);
874 AssertPtrReturn(pszUserName, VERR_INVALID_PARAMETER);
875 AssertPtrReturn(pszPassword, VERR_INVALID_PARAMETER);
876
877 int vrc = VINF_SUCCESS;
878 ComPtr<IProgress> progress;
879 HRESULT rc = pGuest->CopyToGuest(Bstr(pszSource).raw(), Bstr(pszDest).raw(),
880 Bstr(pszUserName).raw(), Bstr(pszPassword).raw(),
881 uFlags, progress.asOutParam());
882 if (FAILED(rc))
883 {
884 /* If we got a VBOX_E_IPRT error we handle the error in a more gentle way
885 * because it contains more accurate info about what went wrong. */
886 ErrorInfo info(pGuest, COM_IIDOF(IGuest));
887 if (info.isFullAvailable())
888 {
889 if (rc == VBOX_E_IPRT_ERROR)
890 RTMsgError("%ls.", info.getText().raw());
891 else
892 RTMsgError("%ls (%Rhrc).", info.getText().raw(), info.getResultCode());
893 }
894 vrc = VERR_GENERAL_FAILURE;
895 }
896 else
897 {
898 /* Setup signal handling if cancelable. */
899 ASSERT(progress);
900 bool fCanceledAlready = false;
901 BOOL fCancelable;
902 HRESULT hrc = progress->COMGETTER(Cancelable)(&fCancelable);
903 if (FAILED(hrc))
904 fCancelable = FALSE;
905 if (fCancelable)
906 {
907 signal(SIGINT, ctrlCopySignalHandler);
908 #ifdef SIGBREAK
909 signal(SIGBREAK, ctrlCopySignalHandler);
910 #endif
911 }
912
913 /* Wait for process to exit ... */
914 BOOL fCompleted = FALSE;
915 BOOL fCanceled = FALSE;
916 while ( SUCCEEDED(progress->COMGETTER(Completed(&fCompleted)))
917 && !fCompleted)
918 {
919 /* Process async cancelation */
920 if (g_fCopyCanceled && !fCanceledAlready)
921 {
922 hrc = progress->Cancel();
923 if (SUCCEEDED(hrc))
924 fCanceledAlready = TRUE;
925 else
926 g_fCopyCanceled = false;
927 }
928
929 /* Progress canceled by Main API? */
930 if ( SUCCEEDED(progress->COMGETTER(Canceled(&fCanceled)))
931 && fCanceled)
932 {
933 break;
934 }
935 }
936
937 /* Undo signal handling */
938 if (fCancelable)
939 {
940 signal(SIGINT, SIG_DFL);
941 #ifdef SIGBREAK
942 signal(SIGBREAK, SIG_DFL);
943 #endif
944 }
945
946 if (fCanceled)
947 {
948 //RTPrintf("Copy operation canceled!\n");
949 }
950 else if ( fCompleted
951 && SUCCEEDED(rc))
952 {
953 LONG iRc = false;
954 CHECK_ERROR_RET(progress, COMGETTER(ResultCode)(&iRc), rc);
955 if (FAILED(iRc))
956 {
957 com::ProgressErrorInfo info(progress);
958 if ( info.isFullAvailable()
959 || info.isBasicAvailable())
960 {
961 /* If we got a VBOX_E_IPRT error we handle the error in a more gentle way
962 * because it contains more accurate info about what went wrong. */
963 if (info.getResultCode() == VBOX_E_IPRT_ERROR)
964 RTMsgError("%ls.", info.getText().raw());
965 else
966 {
967 RTMsgError("Copy operation error details:");
968 GluePrintErrorInfo(info);
969 }
970 }
971 else
972 {
973 if (RT_FAILURE(vrc))
974 RTMsgError("Error while looking up error code, rc=%Rrc\n", vrc);
975 else
976 com::GluePrintRCMessage(iRc);
977 }
978 vrc = VERR_GENERAL_FAILURE;
979 }
980 }
981 }
982 return vrc;
983}
984
985static int handleCtrlCopyTo(HandlerArg *a)
986{
987 /*
988 * Check the syntax. We can deduce the correct syntax from the number of
989 * arguments.
990 */
991 if (a->argc < 3) /* At least the source + destination should be present :-). */
992 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
993
994 Utf8Str Utf8Source(a->argv[1]);
995 Utf8Str Utf8Dest(a->argv[2]);
996 Utf8Str Utf8UserName;
997 Utf8Str Utf8Password;
998 uint32_t uFlags = CopyFileFlag_None;
999 bool fVerbose = false;
1000 bool fCopyRecursive = false;
1001 bool fDryRun = false;
1002
1003 /* Iterate through all possible commands (if available). */
1004 bool usageOK = true;
1005 for (int i = 3; usageOK && i < a->argc; i++)
1006 {
1007 if ( !strcmp(a->argv[i], "--username")
1008 || !strcmp(a->argv[i], "--user"))
1009 {
1010 if (i + 1 >= a->argc)
1011 usageOK = false;
1012 else
1013 {
1014 Utf8UserName = a->argv[i + 1];
1015 ++i;
1016 }
1017 }
1018 else if ( !strcmp(a->argv[i], "--password")
1019 || !strcmp(a->argv[i], "--pwd"))
1020 {
1021 if (i + 1 >= a->argc)
1022 usageOK = false;
1023 else
1024 {
1025 Utf8Password = a->argv[i + 1];
1026 ++i;
1027 }
1028 }
1029 else if (!strcmp(a->argv[i], "--dryrun"))
1030 {
1031 fDryRun = true;
1032 }
1033 else if (!strcmp(a->argv[i], "--flags"))
1034 {
1035 if (i + 1 >= a->argc)
1036 usageOK = false;
1037 else
1038 {
1039 /* Nothing to do here yet. */
1040 ++i;
1041 }
1042 }
1043 else if ( !strcmp(a->argv[i], "--recursive")
1044 || !strcmp(a->argv[i], "--r"))
1045 {
1046 uFlags |= CopyFileFlag_Recursive;
1047 }
1048 else if ( !strcmp(a->argv[i], "--update")
1049 || !strcmp(a->argv[i], "--u"))
1050 {
1051 uFlags |= CopyFileFlag_Update;
1052 }
1053 else if ( !strcmp(a->argv[i], "--follow")
1054 || !strcmp(a->argv[i], "--f"))
1055 {
1056 uFlags |= CopyFileFlag_FollowLinks;
1057 }
1058 /** @todo Add force flag for overwriting existing stuff. */
1059 else if (!strcmp(a->argv[i], "--verbose"))
1060 fVerbose = true;
1061 else
1062 return errorSyntax(USAGE_GUESTCONTROL,
1063 "Invalid parameter '%s'", Utf8Str(a->argv[i]).c_str());
1064 }
1065
1066 if (!usageOK)
1067 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
1068
1069 if (Utf8Source.isEmpty())
1070 return errorSyntax(USAGE_GUESTCONTROL,
1071 "No source specified!");
1072
1073 if (Utf8Dest.isEmpty())
1074 return errorSyntax(USAGE_GUESTCONTROL,
1075 "No destination specified!");
1076
1077 if (Utf8UserName.isEmpty())
1078 return errorSyntax(USAGE_GUESTCONTROL,
1079 "No user name specified!");
1080
1081 /* Lookup VM. */
1082 ComPtr<IMachine> machine;
1083 /* Assume it's an UUID. */
1084 HRESULT rc;
1085 CHECK_ERROR(a->virtualBox, FindMachine(Bstr(a->argv[0]).raw(),
1086 machine.asOutParam()));
1087 if (FAILED(rc))
1088 return 1;
1089
1090 /* Machine is running? */
1091 MachineState_T machineState;
1092 CHECK_ERROR_RET(machine, COMGETTER(State)(&machineState), 1);
1093 if (machineState != MachineState_Running)
1094 {
1095 RTMsgError("Machine \"%s\" is not running!\n", a->argv[0]);
1096 return 1;
1097 }
1098
1099 /* Open a session for the VM. */
1100 CHECK_ERROR_RET(machine, LockMachine(a->session, LockType_Shared), 1);
1101
1102 do
1103 {
1104 /* Get the associated console. */
1105 ComPtr<IConsole> console;
1106 CHECK_ERROR_BREAK(a->session, COMGETTER(Console)(console.asOutParam()));
1107 /* ... and session machine */
1108 ComPtr<IMachine> sessionMachine;
1109 CHECK_ERROR_BREAK(a->session, COMGETTER(Machine)(sessionMachine.asOutParam()));
1110
1111 ComPtr<IGuest> guest;
1112 CHECK_ERROR_BREAK(console, COMGETTER(Guest)(guest.asOutParam()));
1113
1114 if (fVerbose)
1115 {
1116 if (fDryRun)
1117 RTPrintf("Dry run - no files copied!\n");
1118 RTPrintf("Gathering file information ...\n");
1119 }
1120
1121 RTLISTNODE listToCopy;
1122 uint32_t cObjects = 0;
1123 int vrc = ctrlCopyInit(Utf8Source.c_str(), Utf8Dest.c_str(), uFlags,
1124 &cObjects, &listToCopy);
1125 if (RT_FAILURE(vrc))
1126 {
1127 switch (vrc)
1128 {
1129 case VERR_NOT_FOUND:
1130 RTMsgError("No files to copy found!\n");
1131 break;
1132
1133 case VERR_PATH_NOT_FOUND:
1134 RTMsgError("Source path \"%s\" not found!\n", Utf8Source.c_str());
1135 break;
1136
1137 default:
1138 RTMsgError("Failed to initialize, rc=%Rrc\n", vrc);
1139 break;
1140 }
1141 }
1142 else
1143 {
1144 if (RT_SUCCESS(vrc) && fVerbose)
1145 {
1146 if (fCopyRecursive)
1147 RTPrintf("Recursively copying \"%s\" to \"%s\" (%u file(s)) ...\n",
1148 Utf8Source.c_str(), Utf8Dest.c_str(), cObjects);
1149 else
1150 RTPrintf("Copying \"%s\" to \"%s\" (%u file(s)) ...\n",
1151 Utf8Source.c_str(), Utf8Dest.c_str(), cObjects);
1152 }
1153
1154 if (RT_SUCCESS(vrc))
1155 {
1156 PDIRECTORYENTRY pNode;
1157 uint32_t uCurObject = 1;
1158 char szDest[RTPATH_MAX];
1159 RTListForEach(&listToCopy, pNode, DIRECTORYENTRY, Node)
1160 {
1161 /*
1162 * Build final destination path: Append the relative path
1163 * stored in the directory node to the destination directory
1164 * specified on the command line.
1165 */
1166 szDest[0] = '\0'; /* Terminate string, needed for RTPathAppend(). */
1167 vrc = RTPathAppend(szDest, sizeof(szDest), Utf8Dest.c_str());
1168 if (RT_SUCCESS(vrc))
1169 vrc = RTPathAppend(szDest, sizeof(szDest), pNode->pszDestPath);
1170
1171 if (RT_SUCCESS(vrc))
1172 {
1173 if (fVerbose)
1174 RTPrintf("Copying \"%s\" (%u/%u) ...\n",
1175 pNode->pszSourcePath, uCurObject, cObjects);
1176
1177 /* Finally copy the desired file (if no dry run selected). */
1178 if (!fDryRun)
1179 vrc = ctrlCopyFileToGuest(guest, pNode->pszSourcePath, szDest,
1180 Utf8UserName.c_str(), Utf8Password.c_str(), uFlags);
1181 }
1182 else
1183 RTMsgError("Error building destination file name, rc=%Rrc\n", vrc);
1184 if (RT_FAILURE(vrc))
1185 break;
1186 uCurObject++;
1187 }
1188 if (RT_SUCCESS(vrc) && fVerbose)
1189 RTPrintf("Copy operation successful!\n");
1190 }
1191 ctrlCopyDestroy(&listToCopy);
1192 }
1193 a->session->UnlockMachine();
1194 } while (0);
1195 return SUCCEEDED(rc) ? 0 : 1;
1196}
1197
1198static int handleCtrlUpdateAdditions(HandlerArg *a)
1199{
1200 /*
1201 * Check the syntax. We can deduce the correct syntax from the number of
1202 * arguments.
1203 */
1204 if (a->argc < 1) /* At least the VM name should be present :-). */
1205 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
1206
1207 Utf8Str Utf8Source;
1208 bool fVerbose = false;
1209
1210 /* Iterate through all possible commands (if available). */
1211 bool usageOK = true;
1212 for (int i = 1; usageOK && i < a->argc; i++)
1213 {
1214 if (!strcmp(a->argv[i], "--source"))
1215 {
1216 if (i + 1 >= a->argc)
1217 usageOK = false;
1218 else
1219 {
1220 Utf8Source = a->argv[i + 1];
1221 ++i;
1222 }
1223 }
1224 else if (!strcmp(a->argv[i], "--verbose"))
1225 fVerbose = true;
1226 else
1227 return errorSyntax(USAGE_GUESTCONTROL,
1228 "Invalid parameter '%s'", Utf8Str(a->argv[i]).c_str());
1229 }
1230
1231 if (!usageOK)
1232 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
1233
1234 /* Lookup VM. */
1235 ComPtr<IMachine> machine;
1236 /* Assume it's an UUID. */
1237 HRESULT rc;
1238 CHECK_ERROR(a->virtualBox, FindMachine(Bstr(a->argv[0]).raw(),
1239 machine.asOutParam()));
1240 if (FAILED(rc))
1241 return 1;
1242
1243 /* Machine is running? */
1244 MachineState_T machineState;
1245 CHECK_ERROR_RET(machine, COMGETTER(State)(&machineState), 1);
1246 if (machineState != MachineState_Running)
1247 {
1248 RTMsgError("Machine \"%s\" is not running!\n", a->argv[0]);
1249 return 1;
1250 }
1251
1252 /* Open a session for the VM. */
1253 CHECK_ERROR_RET(machine, LockMachine(a->session, LockType_Shared), 1);
1254
1255 do
1256 {
1257 /* Get the associated console. */
1258 ComPtr<IConsole> console;
1259 CHECK_ERROR_BREAK(a->session, COMGETTER(Console)(console.asOutParam()));
1260 /* ... and session machine */
1261 ComPtr<IMachine> sessionMachine;
1262 CHECK_ERROR_BREAK(a->session, COMGETTER(Machine)(sessionMachine.asOutParam()));
1263
1264 ComPtr<IGuest> guest;
1265 CHECK_ERROR_BREAK(console, COMGETTER(Guest)(guest.asOutParam()));
1266
1267 if (fVerbose)
1268 RTPrintf("Updating Guest Additions of machine \"%s\" ...\n", a->argv[0]);
1269
1270#ifdef DEBUG_andy
1271 if (Utf8Source.isEmpty())
1272 Utf8Source = "c:\\Downloads\\VBoxGuestAdditions-r67158.iso";
1273#endif
1274 /* Determine source if not set yet. */
1275 if (Utf8Source.isEmpty())
1276 {
1277 char strTemp[RTPATH_MAX];
1278 int vrc = RTPathAppPrivateNoArch(strTemp, sizeof(strTemp));
1279 AssertRC(vrc);
1280 Utf8Str Utf8Src1 = Utf8Str(strTemp).append("/VBoxGuestAdditions.iso");
1281
1282 vrc = RTPathExecDir(strTemp, sizeof(strTemp));
1283 AssertRC(vrc);
1284 Utf8Str Utf8Src2 = Utf8Str(strTemp).append("/additions/VBoxGuestAdditions.iso");
1285
1286 /* Check the standard image locations */
1287 if (RTFileExists(Utf8Src1.c_str()))
1288 Utf8Source = Utf8Src1;
1289 else if (RTFileExists(Utf8Src2.c_str()))
1290 Utf8Source = Utf8Src2;
1291 else
1292 {
1293 RTMsgError("Source could not be determined! Please use --source to specify a valid source.\n");
1294 break;
1295 }
1296 }
1297 else if (!RTFileExists(Utf8Source.c_str()))
1298 {
1299 RTMsgError("Source \"%s\" does not exist!\n", Utf8Source.c_str());
1300 break;
1301 }
1302 if (fVerbose)
1303 RTPrintf("Using source: %s\n", Utf8Source.c_str());
1304
1305 ComPtr<IProgress> progress;
1306 CHECK_ERROR_BREAK(guest, UpdateGuestAdditions(Bstr(Utf8Source).raw(),
1307 0 /* Flags, not used. */,
1308 progress.asOutParam()));
1309 rc = showProgress(progress);
1310 if (FAILED(rc))
1311 {
1312 com::ProgressErrorInfo info(progress);
1313 if (info.isBasicAvailable())
1314 RTMsgError("Failed to start Guest Additions update. Error message: %lS\n", info.getText().raw());
1315 else
1316 RTMsgError("Failed to start Guest Additions update. No error message available!\n");
1317 }
1318 else
1319 {
1320 if (fVerbose)
1321 RTPrintf("Guest Additions installer successfully copied and started.\n");
1322 }
1323 a->session->UnlockMachine();
1324 } while (0);
1325 return SUCCEEDED(rc) ? 0 : 1;
1326}
1327
1328/**
1329 * Access the guest control store.
1330 *
1331 * @returns 0 on success, 1 on failure
1332 * @note see the command line API description for parameters
1333 */
1334int handleGuestControl(HandlerArg *a)
1335{
1336 HandlerArg arg = *a;
1337 arg.argc = a->argc - 1;
1338 arg.argv = a->argv + 1;
1339
1340 if (a->argc == 0)
1341 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
1342
1343 /* switch (cmd) */
1344 if ( !strcmp(a->argv[0], "exec")
1345 || !strcmp(a->argv[0], "execute"))
1346 {
1347 return handleCtrlExecProgram(&arg);
1348 }
1349 else if (!strcmp(a->argv[0], "copyto"))
1350 {
1351 return handleCtrlCopyTo(&arg);
1352 }
1353 else if ( !strcmp(a->argv[0], "updateadditions")
1354 || !strcmp(a->argv[0], "updateadds"))
1355 {
1356 return handleCtrlUpdateAdditions(&arg);
1357 }
1358
1359 /* default: */
1360 return errorSyntax(USAGE_GUESTCONTROL, "Incorrect parameters");
1361}
1362
1363#endif /* !VBOX_ONLY_DOCS */
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