diff options
Diffstat (limited to 'src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp')
-rw-r--r-- | src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp | 3435 |
1 files changed, 3435 insertions, 0 deletions
diff --git a/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp b/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp new file mode 100644 index 00000000..8d0b3372 --- /dev/null +++ b/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp @@ -0,0 +1,3435 @@ +/* $Id: VBoxManageGuestCtrl.cpp $ */ +/** @file + * VBoxManage - Implementation of guestcontrol command. + */ + +/* + * Copyright (C) 2010-2019 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include "VBoxManage.h" +#include "VBoxManageGuestCtrl.h" + +#ifndef VBOX_ONLY_DOCS + +#include <VBox/com/array.h> +#include <VBox/com/com.h> +#include <VBox/com/ErrorInfo.h> +#include <VBox/com/errorprint.h> +#include <VBox/com/listeners.h> +#include <VBox/com/NativeEventQueue.h> +#include <VBox/com/string.h> +#include <VBox/com/VirtualBox.h> + +#include <VBox/err.h> +#include <VBox/log.h> + +#include <iprt/asm.h> +#include <iprt/dir.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/list.h> +#include <iprt/path.h> +#include <iprt/process.h> /* For RTProcSelf(). */ +#include <iprt/thread.h> +#include <iprt/vfs.h> + +#include <map> +#include <vector> + +#ifdef USE_XPCOM_QUEUE +# include <sys/select.h> +# include <errno.h> +#endif + +#include <signal.h> + +#ifdef RT_OS_DARWIN +# include <CoreFoundation/CFRunLoop.h> +#endif + +using namespace com; + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#define GCTLCMD_COMMON_OPT_USER 999 /**< The --username option number. */ +#define GCTLCMD_COMMON_OPT_PASSWORD 998 /**< The --password option number. */ +#define GCTLCMD_COMMON_OPT_PASSWORD_FILE 997 /**< The --password-file option number. */ +#define GCTLCMD_COMMON_OPT_DOMAIN 996 /**< The --domain option number. */ +/** Common option definitions. */ +#define GCTLCMD_COMMON_OPTION_DEFS() \ + { "--username", GCTLCMD_COMMON_OPT_USER, RTGETOPT_REQ_STRING }, \ + { "--passwordfile", GCTLCMD_COMMON_OPT_PASSWORD_FILE, RTGETOPT_REQ_STRING }, \ + { "--password", GCTLCMD_COMMON_OPT_PASSWORD, RTGETOPT_REQ_STRING }, \ + { "--domain", GCTLCMD_COMMON_OPT_DOMAIN, RTGETOPT_REQ_STRING }, \ + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, \ + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + +/** Handles common options in the typical option parsing switch. */ +#define GCTLCMD_COMMON_OPTION_CASES(a_pCtx, a_ch, a_pValueUnion) \ + case 'v': \ + case 'q': \ + case GCTLCMD_COMMON_OPT_USER: \ + case GCTLCMD_COMMON_OPT_DOMAIN: \ + case GCTLCMD_COMMON_OPT_PASSWORD: \ + case GCTLCMD_COMMON_OPT_PASSWORD_FILE: \ + { \ + RTEXITCODE rcExitCommon = gctlCtxSetOption(a_pCtx, a_ch, a_pValueUnion); \ + if (RT_UNLIKELY(rcExitCommon != RTEXITCODE_SUCCESS)) \ + return rcExitCommon; \ + } break + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Set by the signal handler when current guest control + * action shall be aborted. */ +static volatile bool g_fGuestCtrlCanceled = false; + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Listener declarations. + */ +VBOX_LISTENER_DECLARE(GuestFileEventListenerImpl) +VBOX_LISTENER_DECLARE(GuestProcessEventListenerImpl) +VBOX_LISTENER_DECLARE(GuestSessionEventListenerImpl) +VBOX_LISTENER_DECLARE(GuestEventListenerImpl) + + +/** + * Definition of a guestcontrol command, with handler and various flags. + */ +typedef struct GCTLCMDDEF +{ + /** The command name. */ + const char *pszName; + + /** + * Actual command handler callback. + * + * @param pCtx Pointer to command context to use. + */ + DECLR3CALLBACKMEMBER(RTEXITCODE, pfnHandler, (struct GCTLCMDCTX *pCtx, int argc, char **argv)); + + /** The command usage flags. */ + uint32_t fCmdUsage; + /** Command context flags (GCTLCMDCTX_F_XXX). */ + uint32_t fCmdCtx; +} GCTLCMD; +/** Pointer to a const guest control command definition. */ +typedef GCTLCMDDEF const *PCGCTLCMDDEF; + +/** @name GCTLCMDCTX_F_XXX - Command context flags. + * @{ + */ +/** No flags set. */ +#define GCTLCMDCTX_F_NONE 0 +/** Don't install a signal handler (CTRL+C trap). */ +#define GCTLCMDCTX_F_NO_SIGNAL_HANDLER RT_BIT(0) +/** No guest session needed. */ +#define GCTLCMDCTX_F_SESSION_ANONYMOUS RT_BIT(1) +/** @} */ + +/** + * Context for handling a specific command. + */ +typedef struct GCTLCMDCTX +{ + HandlerArg *pArg; + + /** Pointer to the command definition. */ + PCGCTLCMDDEF pCmdDef; + /** The VM name or UUID. */ + const char *pszVmNameOrUuid; + + /** Whether we've done the post option parsing init already. */ + bool fPostOptionParsingInited; + /** Whether we've locked the VM session. */ + bool fLockedVmSession; + /** Whether to detach (@c true) or close the session. */ + bool fDetachGuestSession; + /** Set if we've installed the signal handler. */ + bool fInstalledSignalHandler; + /** The verbosity level. */ + uint32_t cVerbose; + /** User name. */ + Utf8Str strUsername; + /** Password. */ + Utf8Str strPassword; + /** Domain. */ + Utf8Str strDomain; + /** Pointer to the IGuest interface. */ + ComPtr<IGuest> pGuest; + /** Pointer to the to be used guest session. */ + ComPtr<IGuestSession> pGuestSession; + /** The guest session ID. */ + ULONG uSessionID; + +} GCTLCMDCTX, *PGCTLCMDCTX; + + +/** + * An entry for an element which needs to be copied/created to/on the guest. + */ +typedef struct DESTFILEENTRY +{ + DESTFILEENTRY(Utf8Str strFilename) : mFilename(strFilename) {} + Utf8Str mFilename; +} DESTFILEENTRY, *PDESTFILEENTRY; +/* + * Map for holding destination entries, whereas the key is the destination + * directory and the mapped value is a vector holding all elements for this directory. + */ +typedef std::map< Utf8Str, std::vector<DESTFILEENTRY> > DESTDIRMAP, *PDESTDIRMAP; +typedef std::map< Utf8Str, std::vector<DESTFILEENTRY> >::iterator DESTDIRMAPITER, *PDESTDIRMAPITER; + + +/** + * RTGetOpt-IDs for the guest execution control command line. + */ +enum GETOPTDEF_EXEC +{ + GETOPTDEF_EXEC_IGNOREORPHANEDPROCESSES = 1000, + GETOPTDEF_EXEC_NO_PROFILE, + GETOPTDEF_EXEC_OUTPUTFORMAT, + GETOPTDEF_EXEC_DOS2UNIX, + GETOPTDEF_EXEC_UNIX2DOS, + GETOPTDEF_EXEC_WAITFOREXIT, + GETOPTDEF_EXEC_WAITFORSTDOUT, + GETOPTDEF_EXEC_WAITFORSTDERR +}; + +enum kStreamTransform +{ + kStreamTransform_None = 0, + kStreamTransform_Dos2Unix, + kStreamTransform_Unix2Dos +}; +#endif /* VBOX_ONLY_DOCS */ + + +void usageGuestControl(PRTSTREAM pStrm, const char *pcszSep1, const char *pcszSep2, uint32_t uSubCmd) +{ + const uint32_t fAnonSubCmds = USAGE_GSTCTRL_CLOSESESSION + | USAGE_GSTCTRL_LIST + | USAGE_GSTCTRL_CLOSEPROCESS + | USAGE_GSTCTRL_CLOSESESSION + | USAGE_GSTCTRL_UPDATEGA + | USAGE_GSTCTRL_WATCH; + + /* 0 1 2 3 4 5 6 7 8XXXXXXXXXX */ + /* 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 */ + if (~fAnonSubCmds & uSubCmd) + RTStrmPrintf(pStrm, + "%s guestcontrol %s <uuid|vmname> [--verbose|-v] [--quiet|-q]\n" + " [--username <name>] [--domain <domain>]\n" + " [--passwordfile <file> | --password <password>]\n%s", + pcszSep1, pcszSep2, uSubCmd == ~0U ? "\n" : ""); + if (uSubCmd & USAGE_GSTCTRL_RUN) + RTStrmPrintf(pStrm, + " run [common-options]\n" + " [--exe <path to executable>] [--timeout <msec>]\n" + " [-E|--putenv <NAME>[=<VALUE>]] [--unquoted-args]\n" + " [--ignore-operhaned-processes] [--profile]\n" + " [--no-wait-stdout|--wait-stdout]\n" + " [--no-wait-stderr|--wait-stderr]\n" + " [--dos2unix] [--unix2dos]\n" + " -- <program/arg0> [argument1] ... [argumentN]]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_START) + RTStrmPrintf(pStrm, + " start [common-options]\n" + " [--exe <path to executable>] [--timeout <msec>]\n" + " [-E|--putenv <NAME>[=<VALUE>]] [--unquoted-args]\n" + " [--ignore-operhaned-processes] [--profile]\n" + " -- <program/arg0> [argument1] ... [argumentN]]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_COPYFROM) + RTStrmPrintf(pStrm, + " copyfrom [common-options]\n" + " [--follow] [-R|--recursive]\n" + " <guest-src0> [guest-src1 [...]] <host-dst>\n" + "\n" + " copyfrom [common-options]\n" + " [--follow] [-R|--recursive]\n" + " [--target-directory <host-dst-dir>]\n" + " <guest-src0> [guest-src1 [...]]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_COPYTO) + RTStrmPrintf(pStrm, + " copyto [common-options]\n" + " [--follow] [-R|--recursive]\n" + " <host-src0> [host-src1 [...]] <guest-dst>\n" + "\n" + " copyto [common-options]\n" + " [--follow] [-R|--recursive]\n" + " [--target-directory <guest-dst>]\n" + " <host-src0> [host-src1 [...]]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_MKDIR) + RTStrmPrintf(pStrm, + " mkdir|createdir[ectory] [common-options]\n" + " [--parents] [--mode <mode>]\n" + " <guest directory> [...]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_RMDIR) + RTStrmPrintf(pStrm, + " rmdir|removedir[ectory] [common-options]\n" + " [-R|--recursive]\n" + " <guest directory> [...]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_RM) + RTStrmPrintf(pStrm, + " removefile|rm [common-options] [-f|--force]\n" + " <guest file> [...]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_MV) + RTStrmPrintf(pStrm, + " mv|move|ren[ame] [common-options]\n" + " <source> [source1 [...]] <dest>\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_MKTEMP) + RTStrmPrintf(pStrm, + " mktemp|createtemp[orary] [common-options]\n" + " [--secure] [--mode <mode>] [--tmpdir <directory>]\n" + " <template>\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_STAT) + RTStrmPrintf(pStrm, + " stat [common-options]\n" + " <file> [...]\n" + "\n"); + + /* + * Command not requiring authentication. + */ + if (fAnonSubCmds & uSubCmd) + { + /* 0 1 2 3 4 5 6 7 8XXXXXXXXXX */ + /* 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 */ + RTStrmPrintf(pStrm, + "%s guestcontrol %s <uuid|vmname> [--verbose|-v] [--quiet|-q]\n%s", + pcszSep1, pcszSep2, uSubCmd == ~0U ? "\n" : ""); + if (uSubCmd & USAGE_GSTCTRL_LIST) + RTStrmPrintf(pStrm, + " list <all|sessions|processes|files> [common-opts]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_CLOSEPROCESS) + RTStrmPrintf(pStrm, + " closeprocess [common-options]\n" + " < --session-id <ID>\n" + " | --session-name <name or pattern>\n" + " <PID1> [PID1 [...]]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_CLOSESESSION) + RTStrmPrintf(pStrm, + " closesession [common-options]\n" + " < --all | --session-id <ID>\n" + " | --session-name <name or pattern> >\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_UPDATEGA) + RTStrmPrintf(pStrm, + " updatega|updateguestadditions|updateadditions\n" + " [--source <guest additions .ISO>]\n" + " [--wait-start] [common-options]\n" + " [-- [<argument1>] ... [<argumentN>]]\n" + "\n"); + if (uSubCmd & USAGE_GSTCTRL_WATCH) + RTStrmPrintf(pStrm, + " watch [common-options]\n" + "\n"); + } +} + +#ifndef VBOX_ONLY_DOCS + + +#ifdef RT_OS_WINDOWS +static BOOL WINAPI gctlSignalHandler(DWORD dwCtrlType) +{ + bool fEventHandled = FALSE; + switch (dwCtrlType) + { + /* User pressed CTRL+C or CTRL+BREAK or an external event was sent + * via GenerateConsoleCtrlEvent(). */ + case CTRL_BREAK_EVENT: + case CTRL_CLOSE_EVENT: + case CTRL_C_EVENT: + ASMAtomicWriteBool(&g_fGuestCtrlCanceled, true); + fEventHandled = TRUE; + break; + default: + break; + /** @todo Add other events here. */ + } + + return fEventHandled; +} +#else /* !RT_OS_WINDOWS */ +/** + * Signal handler that sets g_fGuestCtrlCanceled. + * + * This can be executed on any thread in the process, on Windows it may even be + * a thread dedicated to delivering this signal. Don't do anything + * unnecessary here. + */ +static void gctlSignalHandler(int iSignal) +{ + NOREF(iSignal); + ASMAtomicWriteBool(&g_fGuestCtrlCanceled, true); +} +#endif + + +/** + * Installs a custom signal handler to get notified + * whenever the user wants to intercept the program. + * + * @todo Make this handler available for all VBoxManage modules? + */ +static int gctlSignalHandlerInstall(void) +{ + g_fGuestCtrlCanceled = false; + + int rc = VINF_SUCCESS; +#ifdef RT_OS_WINDOWS + if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)gctlSignalHandler, TRUE /* Add handler */)) + { + rc = RTErrConvertFromWin32(GetLastError()); + RTMsgError("Unable to install console control handler, rc=%Rrc\n", rc); + } +#else + signal(SIGINT, gctlSignalHandler); + signal(SIGTERM, gctlSignalHandler); +# ifdef SIGBREAK + signal(SIGBREAK, gctlSignalHandler); +# endif +#endif + return rc; +} + + +/** + * Uninstalls a previously installed signal handler. + */ +static int gctlSignalHandlerUninstall(void) +{ + int rc = VINF_SUCCESS; +#ifdef RT_OS_WINDOWS + if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)NULL, FALSE /* Remove handler */)) + { + rc = RTErrConvertFromWin32(GetLastError()); + RTMsgError("Unable to uninstall console control handler, rc=%Rrc\n", rc); + } +#else + signal(SIGINT, SIG_DFL); + signal(SIGTERM, SIG_DFL); +# ifdef SIGBREAK + signal(SIGBREAK, SIG_DFL); +# endif +#endif + return rc; +} + + +/** + * Translates a process status to a human readable string. + */ +const char *gctlProcessStatusToText(ProcessStatus_T enmStatus) +{ + switch (enmStatus) + { + case ProcessStatus_Starting: + return "starting"; + case ProcessStatus_Started: + return "started"; + case ProcessStatus_Paused: + return "paused"; + case ProcessStatus_Terminating: + return "terminating"; + case ProcessStatus_TerminatedNormally: + return "successfully terminated"; + case ProcessStatus_TerminatedSignal: + return "terminated by signal"; + case ProcessStatus_TerminatedAbnormally: + return "abnormally aborted"; + case ProcessStatus_TimedOutKilled: + return "timed out"; + case ProcessStatus_TimedOutAbnormally: + return "timed out, hanging"; + case ProcessStatus_Down: + return "killed"; + case ProcessStatus_Error: + return "error"; + default: + break; + } + return "unknown"; +} + +/** + * Translates a guest process wait result to a human readable string. + */ +const char *gctlProcessWaitResultToText(ProcessWaitResult_T enmWaitResult) +{ + switch (enmWaitResult) + { + case ProcessWaitResult_Start: + return "started"; + case ProcessWaitResult_Terminate: + return "terminated"; + case ProcessWaitResult_Status: + return "status changed"; + case ProcessWaitResult_Error: + return "error"; + case ProcessWaitResult_Timeout: + return "timed out"; + case ProcessWaitResult_StdIn: + return "stdin ready"; + case ProcessWaitResult_StdOut: + return "data on stdout"; + case ProcessWaitResult_StdErr: + return "data on stderr"; + case ProcessWaitResult_WaitFlagNotSupported: + return "waiting flag not supported"; + default: + break; + } + return "unknown"; +} + +/** + * Translates a guest session status to a human readable string. + */ +const char *gctlGuestSessionStatusToText(GuestSessionStatus_T enmStatus) +{ + switch (enmStatus) + { + case GuestSessionStatus_Starting: + return "starting"; + case GuestSessionStatus_Started: + return "started"; + case GuestSessionStatus_Terminating: + return "terminating"; + case GuestSessionStatus_Terminated: + return "terminated"; + case GuestSessionStatus_TimedOutKilled: + return "timed out"; + case GuestSessionStatus_TimedOutAbnormally: + return "timed out, hanging"; + case GuestSessionStatus_Down: + return "killed"; + case GuestSessionStatus_Error: + return "error"; + default: + break; + } + return "unknown"; +} + +/** + * Translates a guest file status to a human readable string. + */ +const char *gctlFileStatusToText(FileStatus_T enmStatus) +{ + switch (enmStatus) + { + case FileStatus_Opening: + return "opening"; + case FileStatus_Open: + return "open"; + case FileStatus_Closing: + return "closing"; + case FileStatus_Closed: + return "closed"; + case FileStatus_Down: + return "killed"; + case FileStatus_Error: + return "error"; + default: + break; + } + return "unknown"; +} + +/** + * Translates a file system objec type to a string. + */ +const char *gctlFsObjTypeToName(FsObjType_T enmType) +{ + switch (enmType) + { + case FsObjType_Unknown: return "unknown"; + case FsObjType_Fifo: return "fifo"; + case FsObjType_DevChar: return "char-device"; + case FsObjType_Directory: return "directory"; + case FsObjType_DevBlock: return "block-device"; + case FsObjType_File: return "file"; + case FsObjType_Symlink: return "symlink"; + case FsObjType_Socket: return "socket"; + case FsObjType_WhiteOut: return "white-out"; +#ifdef VBOX_WITH_XPCOM_CPP_ENUM_HACK + case FsObjType_32BitHack: break; +#endif + } + return "unknown"; +} + +static int gctlPrintError(com::ErrorInfo &errorInfo) +{ + if ( errorInfo.isFullAvailable() + || errorInfo.isBasicAvailable()) + { + /* If we got a VBOX_E_IPRT error we handle the error in a more gentle way + * because it contains more accurate info about what went wrong. */ + if (errorInfo.getResultCode() == VBOX_E_IPRT_ERROR) + RTMsgError("%ls.", errorInfo.getText().raw()); + else + { + RTMsgError("Error details:"); + GluePrintErrorInfo(errorInfo); + } + return VERR_GENERAL_FAILURE; /** @todo */ + } + AssertMsgFailedReturn(("Object has indicated no error (%Rhrc)!?\n", errorInfo.getResultCode()), + VERR_INVALID_PARAMETER); +} + +static int gctlPrintError(IUnknown *pObj, const GUID &aIID) +{ + com::ErrorInfo ErrInfo(pObj, aIID); + return gctlPrintError(ErrInfo); +} + +static int gctlPrintProgressError(ComPtr<IProgress> pProgress) +{ + int vrc = VINF_SUCCESS; + HRESULT rc; + + do + { + BOOL fCanceled; + CHECK_ERROR_BREAK(pProgress, COMGETTER(Canceled)(&fCanceled)); + if (!fCanceled) + { + LONG rcProc; + CHECK_ERROR_BREAK(pProgress, COMGETTER(ResultCode)(&rcProc)); + if (FAILED(rcProc)) + { + com::ProgressErrorInfo ErrInfo(pProgress); + vrc = gctlPrintError(ErrInfo); + } + } + + } while(0); + + AssertMsgStmt(SUCCEEDED(rc), ("Could not lookup progress information\n"), vrc = VERR_COM_UNEXPECTED); + + return vrc; +} + + + +/* + * + * + * Guest Control Command Context + * Guest Control Command Context + * Guest Control Command Context + * Guest Control Command Context + * + * + * + */ + + +/** + * Initializes a guest control command context structure. + * + * @returns RTEXITCODE_SUCCESS on success, RTEXITCODE_FAILURE on failure (after + * informing the user of course). + * @param pCtx The command context to init. + * @param pArg The handle argument package. + */ +static RTEXITCODE gctrCmdCtxInit(PGCTLCMDCTX pCtx, HandlerArg *pArg) +{ + RT_ZERO(*pCtx); + pCtx->pArg = pArg; + + /* + * The user name defaults to the host one, if we can get at it. + */ + char szUser[1024]; + int rc = RTProcQueryUsername(RTProcSelf(), szUser, sizeof(szUser), NULL); + if ( RT_SUCCESS(rc) + && RTStrIsValidEncoding(szUser)) /* paranoia required on posix */ + { + try + { + pCtx->strUsername = szUser; + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Out of memory"); + } + } + /* else: ignore this failure. */ + + return RTEXITCODE_SUCCESS; +} + + +/** + * Worker for GCTLCMD_COMMON_OPTION_CASES. + * + * @returns RTEXITCODE_SUCCESS if the option was handled successfully. If not, + * an error message is printed and an appropriate failure exit code is + * returned. + * @param pCtx The guest control command context. + * @param ch The option char or ordinal. + * @param pValueUnion The option value union. + */ +static RTEXITCODE gctlCtxSetOption(PGCTLCMDCTX pCtx, int ch, PRTGETOPTUNION pValueUnion) +{ + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + switch (ch) + { + case GCTLCMD_COMMON_OPT_USER: /* User name */ + if (!pCtx->pCmdDef || !(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS)) + pCtx->strUsername = pValueUnion->psz; + else + RTMsgWarning("The --username|-u option is ignored by '%s'", pCtx->pCmdDef->pszName); + break; + + case GCTLCMD_COMMON_OPT_PASSWORD: /* Password */ + if (!pCtx->pCmdDef || !(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS)) + { + if (pCtx->strPassword.isNotEmpty()) + RTMsgWarning("Password is given more than once."); + pCtx->strPassword = pValueUnion->psz; + } + else + RTMsgWarning("The --password option is ignored by '%s'", pCtx->pCmdDef->pszName); + break; + + case GCTLCMD_COMMON_OPT_PASSWORD_FILE: /* Password file */ + if (!pCtx->pCmdDef || !(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS)) + rcExit = readPasswordFile(pValueUnion->psz, &pCtx->strPassword); + else + RTMsgWarning("The --password-file|-p option is ignored by '%s'", pCtx->pCmdDef->pszName); + break; + + case GCTLCMD_COMMON_OPT_DOMAIN: /* domain */ + if (!pCtx->pCmdDef || !(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS)) + pCtx->strDomain = pValueUnion->psz; + else + RTMsgWarning("The --domain option is ignored by '%s'", pCtx->pCmdDef->pszName); + break; + + case 'v': /* --verbose */ + pCtx->cVerbose++; + break; + + case 'q': /* --quiet */ + if (pCtx->cVerbose) + pCtx->cVerbose--; + break; + + default: + AssertFatalMsgFailed(("ch=%d (%c)\n", ch, ch)); + } + return rcExit; +} + + +/** + * Initializes the VM for IGuest operation. + * + * This opens a shared session to a running VM and gets hold of IGuest. + * + * @returns RTEXITCODE_SUCCESS on success. RTEXITCODE_FAILURE and user message + * on failure. + * @param pCtx The guest control command context. + * GCTLCMDCTX::pGuest will be set on success. + */ +static RTEXITCODE gctlCtxInitVmSession(PGCTLCMDCTX pCtx) +{ + HRESULT rc; + AssertPtr(pCtx); + AssertPtr(pCtx->pArg); + + /* + * Find the VM and check if it's running. + */ + ComPtr<IMachine> machine; + CHECK_ERROR(pCtx->pArg->virtualBox, FindMachine(Bstr(pCtx->pszVmNameOrUuid).raw(), machine.asOutParam())); + if (SUCCEEDED(rc)) + { + MachineState_T enmMachineState; + CHECK_ERROR(machine, COMGETTER(State)(&enmMachineState)); + if ( SUCCEEDED(rc) + && enmMachineState == MachineState_Running) + { + /* + * It's running. So, open a session to it and get the IGuest interface. + */ + CHECK_ERROR(machine, LockMachine(pCtx->pArg->session, LockType_Shared)); + if (SUCCEEDED(rc)) + { + pCtx->fLockedVmSession = true; + ComPtr<IConsole> ptrConsole; + CHECK_ERROR(pCtx->pArg->session, COMGETTER(Console)(ptrConsole.asOutParam())); + if (SUCCEEDED(rc)) + { + if (ptrConsole.isNotNull()) + { + CHECK_ERROR(ptrConsole, COMGETTER(Guest)(pCtx->pGuest.asOutParam())); + if (SUCCEEDED(rc)) + return RTEXITCODE_SUCCESS; + } + else + RTMsgError("Failed to get a IConsole pointer for the machine. Is it still running?\n"); + } + } + } + else if (SUCCEEDED(rc)) + RTMsgError("Machine \"%s\" is not running (currently %s)!\n", + pCtx->pszVmNameOrUuid, machineStateToName(enmMachineState, false)); + } + return RTEXITCODE_FAILURE; +} + + +/** + * Creates a guest session with the VM. + * + * @retval RTEXITCODE_SUCCESS on success. + * @retval RTEXITCODE_FAILURE and user message on failure. + * @param pCtx The guest control command context. + * GCTCMDCTX::pGuestSession and GCTLCMDCTX::uSessionID + * will be set. + */ +static RTEXITCODE gctlCtxInitGuestSession(PGCTLCMDCTX pCtx) +{ + HRESULT rc; + AssertPtr(pCtx); + Assert(!(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS)); + Assert(pCtx->pGuest.isNotNull()); + + /* + * Build up a reasonable guest session name. Useful for identifying + * a specific session when listing / searching for them. + */ + char *pszSessionName; + if (RTStrAPrintf(&pszSessionName, + "[%RU32] VBoxManage Guest Control [%s] - %s", + RTProcSelf(), pCtx->pszVmNameOrUuid, pCtx->pCmdDef->pszName) < 0) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "No enough memory for session name"); + + /* + * Create a guest session. + */ + if (pCtx->cVerbose) + RTPrintf("Creating guest session as user '%s'...\n", pCtx->strUsername.c_str()); + try + { + CHECK_ERROR(pCtx->pGuest, CreateSession(Bstr(pCtx->strUsername).raw(), + Bstr(pCtx->strPassword).raw(), + Bstr(pCtx->strDomain).raw(), + Bstr(pszSessionName).raw(), + pCtx->pGuestSession.asOutParam())); + } + catch (std::bad_alloc &) + { + RTMsgError("Out of memory setting up IGuest::CreateSession call"); + rc = E_OUTOFMEMORY; + } + if (SUCCEEDED(rc)) + { + /* + * Wait for guest session to start. + */ + if (pCtx->cVerbose) + RTPrintf("Waiting for guest session to start...\n"); + GuestSessionWaitResult_T enmWaitResult = GuestSessionWaitResult_None; /* Shut up MSC */ + try + { + com::SafeArray<GuestSessionWaitForFlag_T> aSessionWaitFlags; + aSessionWaitFlags.push_back(GuestSessionWaitForFlag_Start); + CHECK_ERROR(pCtx->pGuestSession, WaitForArray(ComSafeArrayAsInParam(aSessionWaitFlags), + /** @todo Make session handling timeouts configurable. */ + 30 * 1000, &enmWaitResult)); + } + catch (std::bad_alloc &) + { + RTMsgError("Out of memory setting up IGuestSession::WaitForArray call"); + rc = E_OUTOFMEMORY; + } + if (SUCCEEDED(rc)) + { + /* The WaitFlagNotSupported result may happen with GAs older than 4.3. */ + if ( enmWaitResult == GuestSessionWaitResult_Start + || enmWaitResult == GuestSessionWaitResult_WaitFlagNotSupported) + { + /* + * Get the session ID and we're ready to rumble. + */ + CHECK_ERROR(pCtx->pGuestSession, COMGETTER(Id)(&pCtx->uSessionID)); + if (SUCCEEDED(rc)) + { + if (pCtx->cVerbose) + RTPrintf("Successfully started guest session (ID %RU32)\n", pCtx->uSessionID); + RTStrFree(pszSessionName); + return RTEXITCODE_SUCCESS; + } + } + else + { + GuestSessionStatus_T enmSessionStatus; + CHECK_ERROR(pCtx->pGuestSession, COMGETTER(Status)(&enmSessionStatus)); + RTMsgError("Error starting guest session (current status is: %s)\n", + SUCCEEDED(rc) ? gctlGuestSessionStatusToText(enmSessionStatus) : "<unknown>"); + } + } + } + + RTStrFree(pszSessionName); + return RTEXITCODE_FAILURE; +} + + +/** + * Completes the guest control context initialization after parsing arguments. + * + * Will validate common arguments, open a VM session, and if requested open a + * guest session and install the CTRL-C signal handler. + * + * It is good to validate all the options and arguments you can before making + * this call. However, the VM session, IGuest and IGuestSession interfaces are + * not availabe till after this call, so take care. + * + * @retval RTEXITCODE_SUCCESS on success. + * @retval RTEXITCODE_FAILURE and user message on failure. + * @param pCtx The guest control command context. + * GCTCMDCTX::pGuestSession and GCTLCMDCTX::uSessionID + * will be set. + * @remarks Can safely be called multiple times, will only do work once. + */ +static RTEXITCODE gctlCtxPostOptionParsingInit(PGCTLCMDCTX pCtx) +{ + if (pCtx->fPostOptionParsingInited) + return RTEXITCODE_SUCCESS; + + /* + * Check that the user name isn't empty when we need it. + */ + RTEXITCODE rcExit; + if ( (pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS) + || pCtx->strUsername.isNotEmpty()) + { + /* + * Open the VM session and if required, a guest session. + */ + rcExit = gctlCtxInitVmSession(pCtx); + if ( rcExit == RTEXITCODE_SUCCESS + && !(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS)) + rcExit = gctlCtxInitGuestSession(pCtx); + if (rcExit == RTEXITCODE_SUCCESS) + { + /* + * Install signal handler if requested (errors are ignored). + */ + if (!(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_NO_SIGNAL_HANDLER)) + { + int rc = gctlSignalHandlerInstall(); + pCtx->fInstalledSignalHandler = RT_SUCCESS(rc); + } + } + } + else + rcExit = errorSyntaxEx(USAGE_GUESTCONTROL, pCtx->pCmdDef->fCmdUsage, "No user name specified!"); + + pCtx->fPostOptionParsingInited = rcExit == RTEXITCODE_SUCCESS; + return rcExit; +} + + +/** + * Cleans up the context when the command returns. + * + * This will close any open guest session, unless the DETACH flag is set. + * It will also close any VM session that may be been established. Any signal + * handlers we've installed will also be removed. + * + * Un-initializes the VM after guest control usage. + * @param pCmdCtx Pointer to command context. + */ +static void gctlCtxTerm(PGCTLCMDCTX pCtx) +{ + HRESULT rc; + AssertPtr(pCtx); + + /* + * Uninstall signal handler. + */ + if (pCtx->fInstalledSignalHandler) + { + gctlSignalHandlerUninstall(); + pCtx->fInstalledSignalHandler = false; + } + + /* + * Close, or at least release, the guest session. + */ + if (pCtx->pGuestSession.isNotNull()) + { + if ( !(pCtx->pCmdDef->fCmdCtx & GCTLCMDCTX_F_SESSION_ANONYMOUS) + && !pCtx->fDetachGuestSession) + { + if (pCtx->cVerbose) + RTPrintf("Closing guest session ...\n"); + + CHECK_ERROR(pCtx->pGuestSession, Close()); + } + else if ( pCtx->fDetachGuestSession + && pCtx->cVerbose) + RTPrintf("Guest session detached\n"); + + pCtx->pGuestSession.setNull(); + } + + /* + * Close the VM session. + */ + if (pCtx->fLockedVmSession) + { + Assert(pCtx->pArg->session.isNotNull()); + CHECK_ERROR(pCtx->pArg->session, UnlockMachine()); + pCtx->fLockedVmSession = false; + } +} + + + + + +/* + * + * + * Guest Control Command Handling. + * Guest Control Command Handling. + * Guest Control Command Handling. + * Guest Control Command Handling. + * Guest Control Command Handling. + * + * + */ + + +/** @name EXITCODEEXEC_XXX - Special run exit codes. + * + * Special exit codes for returning errors/information of a started guest + * process to the command line VBoxManage was started from. Useful for e.g. + * scripting. + * + * ASSUMING that all platforms have at least 7-bits for the exit code we can do + * the following mapping: + * - Guest exit code 0 is mapped to 0 on the host. + * - Guest exit codes 1 thru 93 (0x5d) are displaced by 32, so that 1 + * becomes 33 (0x21) on the host and 93 becomes 125 (0x7d) on the host. + * - Guest exit codes 94 (0x5e) and above are mapped to 126 (0x5e). + * + * We ASSUME that all VBoxManage status codes are in the range 0 thru 32. + * + * @note These are frozen as of 4.1.0. + * @note The guest exit code mappings was introduced with 5.0 and the 'run' + * command, they are/was not supported by 'exec'. + * @sa gctlRunCalculateExitCode + */ +/** Process exited normally but with an exit code <> 0. */ +#define EXITCODEEXEC_CODE ((RTEXITCODE)16) +#define EXITCODEEXEC_FAILED ((RTEXITCODE)17) +#define EXITCODEEXEC_TERM_SIGNAL ((RTEXITCODE)18) +#define EXITCODEEXEC_TERM_ABEND ((RTEXITCODE)19) +#define EXITCODEEXEC_TIMEOUT ((RTEXITCODE)20) +#define EXITCODEEXEC_DOWN ((RTEXITCODE)21) +/** Execution was interrupt by user (ctrl-c). */ +#define EXITCODEEXEC_CANCELED ((RTEXITCODE)22) +/** The first mapped guest (non-zero) exit code. */ +#define EXITCODEEXEC_MAPPED_FIRST 33 +/** The last mapped guest (non-zero) exit code value (inclusive). */ +#define EXITCODEEXEC_MAPPED_LAST 125 +/** The number of exit codes from EXITCODEEXEC_MAPPED_FIRST to + * EXITCODEEXEC_MAPPED_LAST. This is also the highest guest exit code number + * we're able to map. */ +#define EXITCODEEXEC_MAPPED_RANGE (93) +/** The guest exit code displacement value. */ +#define EXITCODEEXEC_MAPPED_DISPLACEMENT 32 +/** The guest exit code was too big to be mapped. */ +#define EXITCODEEXEC_MAPPED_BIG ((RTEXITCODE)126) +/** @} */ + +/** + * Calculates the exit code of VBoxManage. + * + * @returns The exit code to return. + * @param enmStatus The guest process status. + * @param uExitCode The associated guest process exit code (where + * applicable). + * @param fReturnExitCodes Set if we're to use the 32-126 range for guest + * exit codes. + */ +static RTEXITCODE gctlRunCalculateExitCode(ProcessStatus_T enmStatus, ULONG uExitCode, bool fReturnExitCodes) +{ + switch (enmStatus) + { + case ProcessStatus_TerminatedNormally: + if (uExitCode == 0) + return RTEXITCODE_SUCCESS; + if (!fReturnExitCodes) + return EXITCODEEXEC_CODE; + if (uExitCode <= EXITCODEEXEC_MAPPED_RANGE) + return (RTEXITCODE) (uExitCode + EXITCODEEXEC_MAPPED_DISPLACEMENT); + return EXITCODEEXEC_MAPPED_BIG; + + case ProcessStatus_TerminatedAbnormally: + return EXITCODEEXEC_TERM_ABEND; + case ProcessStatus_TerminatedSignal: + return EXITCODEEXEC_TERM_SIGNAL; + +#if 0 /* see caller! */ + case ProcessStatus_TimedOutKilled: + return EXITCODEEXEC_TIMEOUT; + case ProcessStatus_Down: + return EXITCODEEXEC_DOWN; /* Service/OS is stopping, process was killed. */ + case ProcessStatus_Error: + return EXITCODEEXEC_FAILED; + + /* The following is probably for detached? */ + case ProcessStatus_Starting: + return RTEXITCODE_SUCCESS; + case ProcessStatus_Started: + return RTEXITCODE_SUCCESS; + case ProcessStatus_Paused: + return RTEXITCODE_SUCCESS; + case ProcessStatus_Terminating: + return RTEXITCODE_SUCCESS; /** @todo ???? */ +#endif + + default: + AssertMsgFailed(("Unknown exit status (%u/%u) from guest process returned!\n", enmStatus, uExitCode)); + return RTEXITCODE_FAILURE; + } +} + + +/** + * Pumps guest output to the host. + * + * @return IPRT status code. + * @param pProcess Pointer to appropriate process object. + * @param hVfsIosDst Where to write the data. + * @param uHandle Handle where to read the data from. + * @param cMsTimeout Timeout (in ms) to wait for the operation to + * complete. + */ +static int gctlRunPumpOutput(IProcess *pProcess, RTVFSIOSTREAM hVfsIosDst, ULONG uHandle, RTMSINTERVAL cMsTimeout) +{ + AssertPtrReturn(pProcess, VERR_INVALID_POINTER); + Assert(hVfsIosDst != NIL_RTVFSIOSTREAM); + + int vrc; + + SafeArray<BYTE> aOutputData; + HRESULT hrc = pProcess->Read(uHandle, _64K, RT_MAX(cMsTimeout, 1), ComSafeArrayAsOutParam(aOutputData)); + if (SUCCEEDED(hrc)) + { + size_t cbOutputData = aOutputData.size(); + if (cbOutputData == 0) + vrc = VINF_SUCCESS; + else + { + BYTE const *pbBuf = aOutputData.raw(); + AssertPtr(pbBuf); + + vrc = RTVfsIoStrmWrite(hVfsIosDst, pbBuf, cbOutputData, true /*fBlocking*/, NULL); + if (RT_FAILURE(vrc)) + RTMsgError("Unable to write output, rc=%Rrc\n", vrc); + } + } + else + vrc = gctlPrintError(pProcess, COM_IIDOF(IProcess)); + return vrc; +} + + +/** + * Configures a host handle for pumping guest bits. + * + * @returns true if enabled and we successfully configured it. + * @param fEnabled Whether pumping this pipe is configured. + * @param enmHandle The IPRT standard handle designation. + * @param pszName The name for user messages. + * @param enmTransformation The transformation to apply. + * @param phVfsIos Where to return the resulting I/O stream handle. + */ +static bool gctlRunSetupHandle(bool fEnabled, RTHANDLESTD enmHandle, const char *pszName, + kStreamTransform enmTransformation, PRTVFSIOSTREAM phVfsIos) +{ + if (fEnabled) + { + int vrc = RTVfsIoStrmFromStdHandle(enmHandle, 0, true /*fLeaveOpen*/, phVfsIos); + if (RT_SUCCESS(vrc)) + { + if (enmTransformation != kStreamTransform_None) + { + RTMsgWarning("Unsupported %s line ending conversion", pszName); + /** @todo Implement dos2unix and unix2dos stream filters. */ + } + return true; + } + RTMsgWarning("Error getting %s handle: %Rrc", pszName, vrc); + } + return false; +} + + +/** + * Returns the remaining time (in ms) based on the start time and a set + * timeout value. Returns RT_INDEFINITE_WAIT if no timeout was specified. + * + * @return RTMSINTERVAL Time left (in ms). + * @param u64StartMs Start time (in ms). + * @param cMsTimeout Timeout value (in ms). + */ +static RTMSINTERVAL gctlRunGetRemainingTime(uint64_t u64StartMs, RTMSINTERVAL cMsTimeout) +{ + if (!cMsTimeout || cMsTimeout == RT_INDEFINITE_WAIT) /* If no timeout specified, wait forever. */ + return RT_INDEFINITE_WAIT; + + uint64_t u64ElapsedMs = RTTimeMilliTS() - u64StartMs; + if (u64ElapsedMs >= cMsTimeout) + return 0; + + return cMsTimeout - (RTMSINTERVAL)u64ElapsedMs; +} + +/** + * Common handler for the 'run' and 'start' commands. + * + * @returns Command exit code. + * @param pCtx Guest session context. + * @param argc The argument count. + * @param argv The argument vector for this command. + * @param fRunCmd Set if it's 'run' clear if 'start'. + * @param fHelp The help flag for the command. + */ +static RTEXITCODE gctlHandleRunCommon(PGCTLCMDCTX pCtx, int argc, char **argv, bool fRunCmd, uint32_t fHelp) +{ + RT_NOREF(fHelp); + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + /* + * Parse arguments. + */ + enum kGstCtrlRunOpt + { + kGstCtrlRunOpt_IgnoreOrphanedProcesses = 1000, + kGstCtrlRunOpt_NoProfile, /** @todo Deprecated and will be removed soon; use kGstCtrlRunOpt_Profile instead, if needed. */ + kGstCtrlRunOpt_Profile, + kGstCtrlRunOpt_Dos2Unix, + kGstCtrlRunOpt_Unix2Dos, + kGstCtrlRunOpt_WaitForStdOut, + kGstCtrlRunOpt_NoWaitForStdOut, + kGstCtrlRunOpt_WaitForStdErr, + kGstCtrlRunOpt_NoWaitForStdErr + }; + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--putenv", 'E', RTGETOPT_REQ_STRING }, + { "--exe", 'e', RTGETOPT_REQ_STRING }, + { "--timeout", 't', RTGETOPT_REQ_UINT32 }, + { "--unquoted-args", 'u', RTGETOPT_REQ_NOTHING }, + { "--ignore-operhaned-processes", kGstCtrlRunOpt_IgnoreOrphanedProcesses, RTGETOPT_REQ_NOTHING }, + { "--no-profile", kGstCtrlRunOpt_NoProfile, RTGETOPT_REQ_NOTHING }, /** @todo Deprecated. */ + { "--profile", kGstCtrlRunOpt_Profile, RTGETOPT_REQ_NOTHING }, + /* run only: 6 - options */ + { "--dos2unix", kGstCtrlRunOpt_Dos2Unix, RTGETOPT_REQ_NOTHING }, + { "--unix2dos", kGstCtrlRunOpt_Unix2Dos, RTGETOPT_REQ_NOTHING }, + { "--no-wait-stdout", kGstCtrlRunOpt_NoWaitForStdOut, RTGETOPT_REQ_NOTHING }, + { "--wait-stdout", kGstCtrlRunOpt_WaitForStdOut, RTGETOPT_REQ_NOTHING }, + { "--no-wait-stderr", kGstCtrlRunOpt_NoWaitForStdErr, RTGETOPT_REQ_NOTHING }, + { "--wait-stderr", kGstCtrlRunOpt_WaitForStdErr, RTGETOPT_REQ_NOTHING }, + }; + + /** @todo stdin handling. */ + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + int vrc = RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions) - (fRunCmd ? 0 : 6), + 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + AssertRC(vrc); + + com::SafeArray<ProcessCreateFlag_T> aCreateFlags; + com::SafeArray<ProcessWaitForFlag_T> aWaitFlags; + com::SafeArray<IN_BSTR> aArgs; + com::SafeArray<IN_BSTR> aEnv; + const char * pszImage = NULL; + bool fWaitForStdOut = fRunCmd; + bool fWaitForStdErr = fRunCmd; + RTVFSIOSTREAM hVfsStdOut = NIL_RTVFSIOSTREAM; + RTVFSIOSTREAM hVfsStdErr = NIL_RTVFSIOSTREAM; + enum kStreamTransform enmStdOutTransform = kStreamTransform_None; + enum kStreamTransform enmStdErrTransform = kStreamTransform_None; + RTMSINTERVAL cMsTimeout = 0; + + try + { + /* Wait for process start in any case. This is useful for scripting VBoxManage + * when relying on its overall exit code. */ + aWaitFlags.push_back(ProcessWaitForFlag_Start); + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 'E': + if ( ValueUnion.psz[0] == '\0' + || ValueUnion.psz[0] == '=') + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_RUN, + "Invalid argument variable[=value]: '%s'", ValueUnion.psz); + aEnv.push_back(Bstr(ValueUnion.psz).raw()); + break; + + case kGstCtrlRunOpt_IgnoreOrphanedProcesses: + aCreateFlags.push_back(ProcessCreateFlag_IgnoreOrphanedProcesses); + break; + + case kGstCtrlRunOpt_NoProfile: + /** @todo Deprecated, will be removed. */ + RTPrintf("Warning: Deprecated option \"--no-profile\" specified\n"); + break; + + case kGstCtrlRunOpt_Profile: + aCreateFlags.push_back(ProcessCreateFlag_Profile); + break; + + case 'e': + pszImage = ValueUnion.psz; + break; + + case 'u': + aCreateFlags.push_back(ProcessCreateFlag_UnquotedArguments); + break; + + /** @todo Add a hidden flag. */ + + case 't': /* Timeout */ + cMsTimeout = ValueUnion.u32; + break; + + /* run only options: */ + case kGstCtrlRunOpt_Dos2Unix: + Assert(fRunCmd); + enmStdErrTransform = enmStdOutTransform = kStreamTransform_Dos2Unix; + break; + case kGstCtrlRunOpt_Unix2Dos: + Assert(fRunCmd); + enmStdErrTransform = enmStdOutTransform = kStreamTransform_Unix2Dos; + break; + + case kGstCtrlRunOpt_WaitForStdOut: + Assert(fRunCmd); + fWaitForStdOut = true; + break; + case kGstCtrlRunOpt_NoWaitForStdOut: + Assert(fRunCmd); + fWaitForStdOut = false; + break; + + case kGstCtrlRunOpt_WaitForStdErr: + Assert(fRunCmd); + fWaitForStdErr = true; + break; + case kGstCtrlRunOpt_NoWaitForStdErr: + Assert(fRunCmd); + fWaitForStdErr = false; + break; + + case VINF_GETOPT_NOT_OPTION: + aArgs.push_back(Bstr(ValueUnion.psz).raw()); + if (!pszImage) + { + Assert(aArgs.size() == 1); + pszImage = ValueUnion.psz; + } + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_RUN, ch, &ValueUnion); + + } /* switch */ + } /* while RTGetOpt */ + + /* Must have something to execute. */ + if (!pszImage || !*pszImage) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_RUN, "No executable specified!"); + + /* + * Finalize process creation and wait flags and input/output streams. + */ + if (!fRunCmd) + { + aCreateFlags.push_back(ProcessCreateFlag_WaitForProcessStartOnly); + Assert(!fWaitForStdOut); + Assert(!fWaitForStdErr); + } + else + { + aWaitFlags.push_back(ProcessWaitForFlag_Terminate); + fWaitForStdOut = gctlRunSetupHandle(fWaitForStdOut, RTHANDLESTD_OUTPUT, "stdout", enmStdOutTransform, &hVfsStdOut); + if (fWaitForStdOut) + { + aCreateFlags.push_back(ProcessCreateFlag_WaitForStdOut); + aWaitFlags.push_back(ProcessWaitForFlag_StdOut); + } + fWaitForStdErr = gctlRunSetupHandle(fWaitForStdErr, RTHANDLESTD_ERROR, "stderr", enmStdErrTransform, &hVfsStdErr); + if (fWaitForStdErr) + { + aCreateFlags.push_back(ProcessCreateFlag_WaitForStdErr); + aWaitFlags.push_back(ProcessWaitForFlag_StdErr); + } + } + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, "VERR_NO_MEMORY\n"); + } + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + HRESULT rc; + + try + { + do + { + /* Get current time stamp to later calculate rest of timeout left. */ + uint64_t msStart = RTTimeMilliTS(); + + /* + * Create the process. + */ + if (pCtx->cVerbose) + { + if (cMsTimeout == 0) + RTPrintf("Starting guest process ...\n"); + else + RTPrintf("Starting guest process (within %ums)\n", cMsTimeout); + } + ComPtr<IGuestProcess> pProcess; + CHECK_ERROR_BREAK(pCtx->pGuestSession, ProcessCreate(Bstr(pszImage).raw(), + ComSafeArrayAsInParam(aArgs), + ComSafeArrayAsInParam(aEnv), + ComSafeArrayAsInParam(aCreateFlags), + gctlRunGetRemainingTime(msStart, cMsTimeout), + pProcess.asOutParam())); + + /* + * Explicitly wait for the guest process to be in a started state. + */ + com::SafeArray<ProcessWaitForFlag_T> aWaitStartFlags; + aWaitStartFlags.push_back(ProcessWaitForFlag_Start); + ProcessWaitResult_T waitResult; + CHECK_ERROR_BREAK(pProcess, WaitForArray(ComSafeArrayAsInParam(aWaitStartFlags), + gctlRunGetRemainingTime(msStart, cMsTimeout), &waitResult)); + + ULONG uPID = 0; + CHECK_ERROR_BREAK(pProcess, COMGETTER(PID)(&uPID)); + if (fRunCmd && pCtx->cVerbose) + RTPrintf("Process '%s' (PID %RU32) started\n", pszImage, uPID); + else if (!fRunCmd && pCtx->cVerbose) + { + /* Just print plain PID to make it easier for scripts + * invoking VBoxManage. */ + RTPrintf("[%RU32 - Session %RU32]\n", uPID, pCtx->uSessionID); + } + + /* + * Wait for process to exit/start... + */ + RTMSINTERVAL cMsTimeLeft = 1; /* Will be calculated. */ + bool fReadStdOut = false; + bool fReadStdErr = false; + bool fCompleted = false; + bool fCompletedStartCmd = false; + + vrc = VINF_SUCCESS; + while ( !fCompleted + && cMsTimeLeft > 0) + { + cMsTimeLeft = gctlRunGetRemainingTime(msStart, cMsTimeout); + CHECK_ERROR_BREAK(pProcess, WaitForArray(ComSafeArrayAsInParam(aWaitFlags), + RT_MIN(500 /*ms*/, RT_MAX(cMsTimeLeft, 1 /*ms*/)), + &waitResult)); + switch (waitResult) + { + case ProcessWaitResult_Start: + fCompletedStartCmd = fCompleted = !fRunCmd; /* Only wait for startup if the 'start' command. */ + break; + case ProcessWaitResult_StdOut: + fReadStdOut = true; + break; + case ProcessWaitResult_StdErr: + fReadStdErr = true; + break; + case ProcessWaitResult_Terminate: + if (pCtx->cVerbose) + RTPrintf("Process terminated\n"); + /* Process terminated, we're done. */ + fCompleted = true; + break; + case ProcessWaitResult_WaitFlagNotSupported: + /* The guest does not support waiting for stdout/err, so + * yield to reduce the CPU load due to busy waiting. */ + RTThreadYield(); + fReadStdOut = fReadStdErr = true; + break; + case ProcessWaitResult_Timeout: + { + /** @todo It is really unclear whether we will get stuck with the timeout + * result here if the guest side times out the process and fails to + * kill the process... To be on the save side, double the IPC and + * check the process status every time we time out. */ + ProcessStatus_T enmProcStatus; + CHECK_ERROR_BREAK(pProcess, COMGETTER(Status)(&enmProcStatus)); + if ( enmProcStatus == ProcessStatus_TimedOutKilled + || enmProcStatus == ProcessStatus_TimedOutAbnormally) + fCompleted = true; + fReadStdOut = fReadStdErr = true; + break; + } + case ProcessWaitResult_Status: + /* ignore. */ + break; + case ProcessWaitResult_Error: + /* waitFor is dead in the water, I think, so better leave the loop. */ + vrc = VERR_CALLBACK_RETURN; + break; + + case ProcessWaitResult_StdIn: AssertFailed(); /* did ask for this! */ break; + case ProcessWaitResult_None: AssertFailed(); /* used. */ break; + default: AssertFailed(); /* huh? */ break; + } + + if (g_fGuestCtrlCanceled) + break; + + /* + * Pump output as needed. + */ + if (fReadStdOut) + { + cMsTimeLeft = gctlRunGetRemainingTime(msStart, cMsTimeout); + int vrc2 = gctlRunPumpOutput(pProcess, hVfsStdOut, 1 /* StdOut */, cMsTimeLeft); + if (RT_FAILURE(vrc2) && RT_SUCCESS(vrc)) + vrc = vrc2; + fReadStdOut = false; + } + if (fReadStdErr) + { + cMsTimeLeft = gctlRunGetRemainingTime(msStart, cMsTimeout); + int vrc2 = gctlRunPumpOutput(pProcess, hVfsStdErr, 2 /* StdErr */, cMsTimeLeft); + if (RT_FAILURE(vrc2) && RT_SUCCESS(vrc)) + vrc = vrc2; + fReadStdErr = false; + } + if ( RT_FAILURE(vrc) + || g_fGuestCtrlCanceled) + break; + + /* + * Process events before looping. + */ + NativeEventQueue::getMainEventQueue()->processEventQueue(0); + } /* while */ + + /* + * Report status back to the user. + */ + if (g_fGuestCtrlCanceled) + { + if (pCtx->cVerbose) + RTPrintf("Process execution aborted!\n"); + rcExit = EXITCODEEXEC_CANCELED; + } + else if (fCompletedStartCmd) + { + if (pCtx->cVerbose) + RTPrintf("Process successfully started!\n"); + rcExit = RTEXITCODE_SUCCESS; + } + else if (fCompleted) + { + ProcessStatus_T procStatus; + CHECK_ERROR_BREAK(pProcess, COMGETTER(Status)(&procStatus)); + if ( procStatus == ProcessStatus_TerminatedNormally + || procStatus == ProcessStatus_TerminatedAbnormally + || procStatus == ProcessStatus_TerminatedSignal) + { + LONG lExitCode; + CHECK_ERROR_BREAK(pProcess, COMGETTER(ExitCode)(&lExitCode)); + if (pCtx->cVerbose) + RTPrintf("Exit code=%u (Status=%u [%s])\n", + lExitCode, procStatus, gctlProcessStatusToText(procStatus)); + + rcExit = gctlRunCalculateExitCode(procStatus, lExitCode, true /*fReturnExitCodes*/); + } + else if ( procStatus == ProcessStatus_TimedOutKilled + || procStatus == ProcessStatus_TimedOutAbnormally) + { + if (pCtx->cVerbose) + RTPrintf("Process timed out (guest side) and %s\n", + procStatus == ProcessStatus_TimedOutAbnormally + ? "failed to terminate so far" : "was terminated"); + rcExit = EXITCODEEXEC_TIMEOUT; + } + else + { + if (pCtx->cVerbose) + RTPrintf("Process now is in status [%s] (unexpected)\n", gctlProcessStatusToText(procStatus)); + rcExit = RTEXITCODE_FAILURE; + } + } + else if (RT_FAILURE_NP(vrc)) + { + if (pCtx->cVerbose) + RTPrintf("Process monitor loop quit with vrc=%Rrc\n", vrc); + rcExit = RTEXITCODE_FAILURE; + } + else + { + if (pCtx->cVerbose) + RTPrintf("Process monitor loop timed out\n"); + rcExit = EXITCODEEXEC_TIMEOUT; + } + + } while (0); + } + catch (std::bad_alloc &) + { + rc = E_OUTOFMEMORY; + } + + /* + * Decide what to do with the guest session. + * + * If it's the 'start' command where detach the guest process after + * starting, don't close the guest session it is part of, except on + * failure or ctrl-c. + * + * For the 'run' command the guest process quits with us. + */ + if (!fRunCmd && SUCCEEDED(rc) && !g_fGuestCtrlCanceled) + pCtx->fDetachGuestSession = true; + + /* Make sure we return failure on failure. */ + if (FAILED(rc) && rcExit == RTEXITCODE_SUCCESS) + rcExit = RTEXITCODE_FAILURE; + return rcExit; +} + + +static DECLCALLBACK(RTEXITCODE) gctlHandleRun(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + return gctlHandleRunCommon(pCtx, argc, argv, true /*fRunCmd*/, USAGE_GSTCTRL_RUN); +} + + +static DECLCALLBACK(RTEXITCODE) gctlHandleStart(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + return gctlHandleRunCommon(pCtx, argc, argv, false /*fRunCmd*/, USAGE_GSTCTRL_START); +} + + +static RTEXITCODE gctlHandleCopy(PGCTLCMDCTX pCtx, int argc, char **argv, bool fHostToGuest) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + /** @todo r=bird: This command isn't very unix friendly in general. mkdir + * is much better (partly because it is much simpler of course). The main + * arguments against this is that (1) all but two options conflicts with + * what 'man cp' tells me on a GNU/Linux system, (2) wildchar matching is + * done windows CMD style (though not in a 100% compatible way), and (3) + * that only one source is allowed - efficiently sabotaging default + * wildcard expansion by a unix shell. The best solution here would be + * two different variant, one windowsy (xcopy) and one unixy (gnu cp). */ + + /* + * IGuest::CopyToGuest is kept as simple as possible to let the developer choose + * what and how to implement the file enumeration/recursive lookup, like VBoxManage + * does in here. + */ + enum GETOPTDEF_COPY + { + GETOPTDEF_COPY_FOLLOW = 1000, + GETOPTDEF_COPY_TARGETDIR + }; + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--follow", GETOPTDEF_COPY_FOLLOW, RTGETOPT_REQ_NOTHING }, + { "--recursive", 'R', RTGETOPT_REQ_NOTHING }, + { "--target-directory", GETOPTDEF_COPY_TARGETDIR, RTGETOPT_REQ_STRING } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + bool fDstMustBeDir = false; + const char *pszDst = NULL; + bool fFollow = false; + bool fRecursive = false; + uint32_t uUsage = fHostToGuest ? USAGE_GSTCTRL_COPYTO : USAGE_GSTCTRL_COPYFROM; + + int vrc = VINF_SUCCESS; + while ( (ch = RTGetOpt(&GetState, &ValueUnion)) != 0 + && ch != VINF_GETOPT_NOT_OPTION) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case GETOPTDEF_COPY_FOLLOW: + fFollow = true; + break; + + case 'R': /* Recursive processing */ + fRecursive = true; + break; + + case GETOPTDEF_COPY_TARGETDIR: + pszDst = ValueUnion.psz; + fDstMustBeDir = true; + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, uUsage, ch, &ValueUnion); + } + } + + char **papszSources = RTGetOptNonOptionArrayPtr(&GetState); + size_t cSources = &argv[argc] - papszSources; + + if (!cSources) + return errorSyntaxEx(USAGE_GUESTCONTROL, uUsage, "No sources specified!"); + + /* Unless a --target-directory is given, the last argument is the destination, so + bump it from the source list. */ + if (pszDst == NULL && cSources >= 2) + pszDst = papszSources[--cSources]; + + if (pszDst == NULL) + return errorSyntaxEx(USAGE_GUESTCONTROL, uUsage, "No destination specified!"); + + char szAbsDst[RTPATH_MAX]; + if (!fHostToGuest) + { + vrc = RTPathAbs(pszDst, szAbsDst, sizeof(szAbsDst)); + if (RT_SUCCESS(vrc)) + pszDst = szAbsDst; + else + return RTMsgErrorExitFailure("RTPathAbs failed on '%s': %Rrc", pszDst, vrc); + } + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + /* + * Done parsing arguments, do some more preparations. + */ + if (pCtx->cVerbose) + { + if (fHostToGuest) + RTPrintf("Copying from host to guest ...\n"); + else + RTPrintf("Copying from guest to host ...\n"); + } + + HRESULT rc = S_OK; + ComPtr<IProgress> pProgress; +/** @todo r=bird: This codes does nothing to handle the case where there are + * multiple sources. You need to do serveral thing before thats handled + * correctly. For starters the progress object handling needs to be moved + * inside the loop. Next you need to check what the destination is, because you + * can only copy multiple source files/directories to another directory. You + * actually need to check whether the target exists and is a directory + * regardless of you have 1 or 10 source files/dirs. + * + * Btw. the original approach to error handling here was APPALING. If some file + * couldn't be stat'ed or if it was a file/directory, you only spat out messages + * in verbose mode and never set the status code. + * + * The handling of the wildcard filtering expressions in sources was also just + * skipped. I've corrected this, but you still need to make up your mind wrt + * wildcards or not. + * + * Update: I've kicked out the whole SourceFileEntry/vecSources stuff as we can + * use argv directly without any unnecessary copying. You just have to + * look for the wildcards inside this loop instead. + */ + NOREF(fDstMustBeDir); + if (cSources != 1) + return RTMsgErrorExitFailure("Only one source file or directory at the moment."); + for (size_t iSrc = 0; iSrc < cSources; iSrc++) + { + const char *pszSource = papszSources[iSrc]; + + /* Check if the source contains any wilecards in the last component, if so we + don't know how to deal with it yet and refuse. */ + const char *pszSrcFinalComp = RTPathFilename(pszSource); + if (pszSrcFinalComp && strpbrk(pszSrcFinalComp, "*?")) + rcExit = RTMsgErrorExitFailure("Skipping '%s' because wildcard expansion isn't implemented yet\n", pszSource); + else if (fHostToGuest) + { + /* + * Source is host, destiation guest. + */ + char szAbsSrc[RTPATH_MAX]; + vrc = RTPathAbs(pszSource, szAbsSrc, sizeof(szAbsSrc)); + if (RT_SUCCESS(vrc)) + { + RTFSOBJINFO ObjInfo; + vrc = RTPathQueryInfo(szAbsSrc, &ObjInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(vrc)) + { + if (RTFS_IS_FILE(ObjInfo.Attr.fMode)) + { + if (pCtx->cVerbose) + RTPrintf("File '%s' -> '%s'\n", szAbsSrc, pszDst); + + SafeArray<FileCopyFlag_T> copyFlags; + rc = pCtx->pGuestSession->FileCopyToGuest(Bstr(szAbsSrc).raw(), Bstr(pszDst).raw(), + ComSafeArrayAsInParam(copyFlags), pProgress.asOutParam()); + } + else if (RTFS_IS_DIRECTORY(ObjInfo.Attr.fMode)) + { + if (pCtx->cVerbose) + RTPrintf("Directory '%s' -> '%s'\n", szAbsSrc, pszDst); + + SafeArray<DirectoryCopyFlag_T> copyFlags; + copyFlags.push_back(DirectoryCopyFlag_CopyIntoExisting); + rc = pCtx->pGuestSession->DirectoryCopyToGuest(Bstr(szAbsSrc).raw(), Bstr(pszDst).raw(), + ComSafeArrayAsInParam(copyFlags), pProgress.asOutParam()); + } + else + rcExit = RTMsgErrorExitFailure("Not a file or directory: %s\n", szAbsSrc); + } + else + rcExit = RTMsgErrorExitFailure("RTPathQueryInfo failed on '%s': %Rrc", szAbsSrc, vrc); + } + else + rcExit = RTMsgErrorExitFailure("RTPathAbs failed on '%s': %Rrc", pszSource, vrc); + } + else + { + /* + * Source guest, destination host. + */ + /* We need to query the source type on the guest first in order to know which copy flavor we need. */ + ComPtr<IGuestFsObjInfo> pFsObjInfo; + rc = pCtx->pGuestSession->FsObjQueryInfo(Bstr(pszSource).raw(), TRUE /* fFollowSymlinks */, pFsObjInfo.asOutParam()); + if (SUCCEEDED(rc)) + { + FsObjType_T enmObjType; + CHECK_ERROR(pFsObjInfo,COMGETTER(Type)(&enmObjType)); + if (SUCCEEDED(rc)) + { + /* Take action according to source file. */ + if (enmObjType == FsObjType_Directory) + { + if (pCtx->cVerbose) + RTPrintf("Directory '%s' -> '%s'\n", pszSource, pszDst); + + SafeArray<DirectoryCopyFlag_T> aCopyFlags; + aCopyFlags.push_back(DirectoryCopyFlag_CopyIntoExisting); + rc = pCtx->pGuestSession->DirectoryCopyFromGuest(Bstr(pszSource).raw(), Bstr(pszDst).raw(), + ComSafeArrayAsInParam(aCopyFlags), pProgress.asOutParam()); + } + else if (enmObjType == FsObjType_File) + { + if (pCtx->cVerbose) + RTPrintf("File '%s' -> '%s'\n", pszSource, pszDst); + + SafeArray<FileCopyFlag_T> aCopyFlags; + rc = pCtx->pGuestSession->FileCopyFromGuest(Bstr(pszSource).raw(), Bstr(pszDst).raw(), + ComSafeArrayAsInParam(aCopyFlags), pProgress.asOutParam()); + } + else + rcExit = RTMsgErrorExitFailure("Not a file or directory: %s\n", pszSource); + } + else + rcExit = RTEXITCODE_FAILURE; + } + else + rcExit = RTMsgErrorExitFailure("FsObjQueryInfo failed on '%s': %Rhrc", pszSource, rc); + } + } + + if (FAILED(rc)) + { + vrc = gctlPrintError(pCtx->pGuestSession, COM_IIDOF(IGuestSession)); + } + else if (pProgress.isNotNull()) + { + if (pCtx->cVerbose) + rc = showProgress(pProgress); + else + rc = pProgress->WaitForCompletion(-1 /* No timeout */); + if (SUCCEEDED(rc)) + CHECK_PROGRESS_ERROR(pProgress, ("File copy failed")); + vrc = gctlPrintProgressError(pProgress); + } + if (RT_FAILURE(vrc)) + rcExit = RTEXITCODE_FAILURE; + + return rcExit; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleCopyFrom(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + return gctlHandleCopy(pCtx, argc, argv, false /* Guest to host */); +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleCopyTo(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + return gctlHandleCopy(pCtx, argc, argv, true /* Host to guest */); +} + +static DECLCALLBACK(RTEXITCODE) gctrlHandleMkDir(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--mode", 'm', RTGETOPT_REQ_UINT32 }, + { "--parents", 'P', RTGETOPT_REQ_NOTHING } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + SafeArray<DirectoryCreateFlag_T> aDirCreateFlags; + uint32_t fDirMode = 0; /* Default mode. */ + uint32_t cDirsCreated = 0; + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 'm': /* Mode */ + fDirMode = ValueUnion.u32; + break; + + case 'P': /* Create parents */ + aDirCreateFlags.push_back(DirectoryCreateFlag_Parents); + break; + + case VINF_GETOPT_NOT_OPTION: + if (cDirsCreated == 0) + { + /* + * First non-option - no more options now. + */ + rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + if (pCtx->cVerbose) + RTPrintf("Creating %RU32 directories...\n", argc - GetState.iNext + 1); + } + if (g_fGuestCtrlCanceled) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "mkdir was interrupted by Ctrl-C (%u left)\n", + argc - GetState.iNext + 1); + + /* + * Create the specified directory. + * + * On failure we'll change the exit status to failure and + * continue with the next directory that needs creating. We do + * this because we only create new things, and because this is + * how /bin/mkdir works on unix. + */ + cDirsCreated++; + if (pCtx->cVerbose) + RTPrintf("Creating directory \"%s\" ...\n", ValueUnion.psz); + try + { + HRESULT rc; + CHECK_ERROR(pCtx->pGuestSession, DirectoryCreate(Bstr(ValueUnion.psz).raw(), + fDirMode, ComSafeArrayAsInParam(aDirCreateFlags))); + if (FAILED(rc)) + rcExit = RTEXITCODE_FAILURE; + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Out of memory\n"); + } + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MKDIR, ch, &ValueUnion); + } + } + + if (!cDirsCreated) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MKDIR, "No directory to create specified!"); + return rcExit; +} + + +static DECLCALLBACK(RTEXITCODE) gctlHandleRmDir(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--recursive", 'R', RTGETOPT_REQ_NOTHING }, + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + bool fRecursive = false; + uint32_t cDirRemoved = 0; + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 'R': + fRecursive = true; + break; + + case VINF_GETOPT_NOT_OPTION: + { + if (cDirRemoved == 0) + { + /* + * First non-option - no more options now. + */ + rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + if (pCtx->cVerbose) + RTPrintf("Removing %RU32 directorie%s(s)...\n", argc - GetState.iNext + 1, fRecursive ? "tree" : ""); + } + if (g_fGuestCtrlCanceled) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "rmdir was interrupted by Ctrl-C (%u left)\n", + argc - GetState.iNext + 1); + + cDirRemoved++; + HRESULT rc; + if (!fRecursive) + { + /* + * Remove exactly one directory. + */ + if (pCtx->cVerbose) + RTPrintf("Removing directory \"%s\" ...\n", ValueUnion.psz); + try + { + CHECK_ERROR(pCtx->pGuestSession, DirectoryRemove(Bstr(ValueUnion.psz).raw())); + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Out of memory\n"); + } + } + else + { + /* + * Remove the directory and anything under it, that means files + * and everything. This is in the tradition of the Windows NT + * CMD.EXE "rmdir /s" operation, a tradition which jpsoft's TCC + * strongly warns against (and half-ways questions the sense of). + */ + if (pCtx->cVerbose) + RTPrintf("Recursively removing directory \"%s\" ...\n", ValueUnion.psz); + try + { + /** @todo Make flags configurable. */ + com::SafeArray<DirectoryRemoveRecFlag_T> aRemRecFlags; + aRemRecFlags.push_back(DirectoryRemoveRecFlag_ContentAndDir); + + ComPtr<IProgress> ptrProgress; + CHECK_ERROR(pCtx->pGuestSession, DirectoryRemoveRecursive(Bstr(ValueUnion.psz).raw(), + ComSafeArrayAsInParam(aRemRecFlags), + ptrProgress.asOutParam())); + if (SUCCEEDED(rc)) + { + if (pCtx->cVerbose) + rc = showProgress(ptrProgress); + else + rc = ptrProgress->WaitForCompletion(-1 /* indefinitely */); + if (SUCCEEDED(rc)) + CHECK_PROGRESS_ERROR(ptrProgress, ("Directory deletion failed")); + ptrProgress.setNull(); + } + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Out of memory during recursive rmdir\n"); + } + } + + /* + * This command returns immediately on failure since it's destructive in nature. + */ + if (FAILED(rc)) + return RTEXITCODE_FAILURE; + break; + } + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_RMDIR, ch, &ValueUnion); + } + } + + if (!cDirRemoved) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_RMDIR, "No directory to remove specified!"); + return rcExit; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleRm(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--force", 'f', RTGETOPT_REQ_NOTHING, }, + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + uint32_t cFilesDeleted = 0; + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + bool fForce = true; + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case VINF_GETOPT_NOT_OPTION: + if (cFilesDeleted == 0) + { + /* + * First non-option - no more options now. + */ + rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + if (pCtx->cVerbose) + RTPrintf("Removing %RU32 file(s)...\n", argc - GetState.iNext + 1); + } + if (g_fGuestCtrlCanceled) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "rm was interrupted by Ctrl-C (%u left)\n", + argc - GetState.iNext + 1); + + /* + * Remove the specified file. + * + * On failure we will by default stop, however, the force option will + * by unix traditions force us to ignore errors and continue. + */ + cFilesDeleted++; + if (pCtx->cVerbose) + RTPrintf("Removing file \"%s\" ...\n", ValueUnion.psz); + try + { + /** @todo How does IGuestSession::FsObjRemove work with read-only files? Do we + * need to do some chmod or whatever to better emulate the --force flag? */ + HRESULT rc; + CHECK_ERROR(pCtx->pGuestSession, FsObjRemove(Bstr(ValueUnion.psz).raw())); + if (FAILED(rc) && !fForce) + return RTEXITCODE_FAILURE; + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Out of memory\n"); + } + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_RM, ch, &ValueUnion); + } + } + + if (!cFilesDeleted && !fForce) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_RM, "No file to remove specified!"); + return rcExit; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleMv(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() +/** @todo Missing --force/-f flag. */ + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + int vrc = VINF_SUCCESS; + + bool fDryrun = false; + std::vector< Utf8Str > vecSources; + const char *pszDst = NULL; + com::SafeArray<FsObjRenameFlag_T> aRenameFlags; + + try + { + /** @todo Make flags configurable. */ + aRenameFlags.push_back(FsObjRenameFlag_NoReplace); + + while ( (ch = RTGetOpt(&GetState, &ValueUnion)) + && RT_SUCCESS(vrc)) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + /** @todo Implement a --dryrun command. */ + /** @todo Implement rename flags. */ + + case VINF_GETOPT_NOT_OPTION: + vecSources.push_back(Utf8Str(ValueUnion.psz)); + pszDst = ValueUnion.psz; + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MV, ch, &ValueUnion); + } + } + } + catch (std::bad_alloc &) + { + vrc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(vrc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to initialize, rc=%Rrc\n", vrc); + + size_t cSources = vecSources.size(); + if (!cSources) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MV, + "No source(s) to move specified!"); + if (cSources < 2) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MV, + "No destination specified!"); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + /* Delete last element, which now is the destination. */ + vecSources.pop_back(); + cSources = vecSources.size(); + + HRESULT rc = S_OK; + + if (cSources > 1) + { + BOOL fExists = FALSE; + rc = pCtx->pGuestSession->DirectoryExists(Bstr(pszDst).raw(), FALSE /*followSymlinks*/, &fExists); + if (FAILED(rc) || !fExists) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Destination must be a directory when specifying multiple sources\n"); + } + + /* + * Rename (move) the entries. + */ + if (pCtx->cVerbose) + RTPrintf("Renaming %RU32 %s ...\n", cSources, cSources > 1 ? "entries" : "entry"); + + std::vector< Utf8Str >::iterator it = vecSources.begin(); + while ( it != vecSources.end() + && !g_fGuestCtrlCanceled) + { + Utf8Str strCurSource = (*it); + + ComPtr<IGuestFsObjInfo> pFsObjInfo; + FsObjType_T enmObjType = FsObjType_Unknown; /* Shut up MSC */ + rc = pCtx->pGuestSession->FsObjQueryInfo(Bstr(strCurSource).raw(), FALSE /*followSymlinks*/, pFsObjInfo.asOutParam()); + if (SUCCEEDED(rc)) + rc = pFsObjInfo->COMGETTER(Type)(&enmObjType); + if (FAILED(rc)) + { + if (pCtx->cVerbose) + RTPrintf("Warning: Cannot stat for element \"%s\": No such file or directory\n", strCurSource.c_str()); + ++it; + continue; /* Skip. */ + } + + if (pCtx->cVerbose) + RTPrintf("Renaming %s \"%s\" to \"%s\" ...\n", + enmObjType == FsObjType_Directory ? "directory" : "file", + strCurSource.c_str(), pszDst); + + if (!fDryrun) + { + if (enmObjType == FsObjType_Directory) + { + CHECK_ERROR_BREAK(pCtx->pGuestSession, FsObjRename(Bstr(strCurSource).raw(), + Bstr(pszDst).raw(), + ComSafeArrayAsInParam(aRenameFlags))); + + /* Break here, since it makes no sense to rename mroe than one source to + * the same directory. */ +/** @todo r=bird: You are being kind of windowsy (or just DOSish) about the 'sense' part here, + * while being totaly buggy about the behavior. 'VBoxManage guestcontrol ren dir1 dir2 dstdir' will + * stop after 'dir1' and SILENTLY ignore dir2. If you tried this on Windows, you'd see an error + * being displayed. If you 'man mv' on a nearby unixy system, you'd see that they've made perfect + * sense out of any situation with more than one source. */ + it = vecSources.end(); + break; + } + else + CHECK_ERROR_BREAK(pCtx->pGuestSession, FsObjRename(Bstr(strCurSource).raw(), + Bstr(pszDst).raw(), + ComSafeArrayAsInParam(aRenameFlags))); + } + + ++it; + } + + if ( (it != vecSources.end()) + && pCtx->cVerbose) + { + RTPrintf("Warning: Not all sources were renamed\n"); + } + + return FAILED(rc) ? RTEXITCODE_FAILURE : RTEXITCODE_SUCCESS; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleMkTemp(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--mode", 'm', RTGETOPT_REQ_UINT32 }, + { "--directory", 'D', RTGETOPT_REQ_NOTHING }, + { "--secure", 's', RTGETOPT_REQ_NOTHING }, + { "--tmpdir", 't', RTGETOPT_REQ_STRING } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + Utf8Str strTemplate; + uint32_t fMode = 0; /* Default mode. */ + bool fDirectory = false; + bool fSecure = false; + Utf8Str strTempDir; + + DESTDIRMAP mapDirs; + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 'm': /* Mode */ + fMode = ValueUnion.u32; + break; + + case 'D': /* Create directory */ + fDirectory = true; + break; + + case 's': /* Secure */ + fSecure = true; + break; + + case 't': /* Temp directory */ + strTempDir = ValueUnion.psz; + break; + + case VINF_GETOPT_NOT_OPTION: + if (strTemplate.isEmpty()) + strTemplate = ValueUnion.psz; + else + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MKTEMP, + "More than one template specified!\n"); + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MKTEMP, ch, &ValueUnion); + } + } + + if (strTemplate.isEmpty()) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MKTEMP, + "No template specified!"); + + if (!fDirectory) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_MKTEMP, + "Creating temporary files is currently not supported!"); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + /* + * Create the directories. + */ + if (pCtx->cVerbose) + { + if (fDirectory && !strTempDir.isEmpty()) + RTPrintf("Creating temporary directory from template '%s' in directory '%s' ...\n", + strTemplate.c_str(), strTempDir.c_str()); + else if (fDirectory) + RTPrintf("Creating temporary directory from template '%s' in default temporary directory ...\n", + strTemplate.c_str()); + else if (!fDirectory && !strTempDir.isEmpty()) + RTPrintf("Creating temporary file from template '%s' in directory '%s' ...\n", + strTemplate.c_str(), strTempDir.c_str()); + else if (!fDirectory) + RTPrintf("Creating temporary file from template '%s' in default temporary directory ...\n", + strTemplate.c_str()); + } + + HRESULT rc = S_OK; + if (fDirectory) + { + Bstr bstrDirectory; + CHECK_ERROR(pCtx->pGuestSession, DirectoryCreateTemp(Bstr(strTemplate).raw(), + fMode, Bstr(strTempDir).raw(), + fSecure, + bstrDirectory.asOutParam())); + if (SUCCEEDED(rc)) + RTPrintf("Directory name: %ls\n", bstrDirectory.raw()); + } + else + { + // else - temporary file not yet implemented + /** @todo implement temporary file creation (we fend it off above, no + * worries). */ + rc = E_FAIL; + } + + return FAILED(rc) ? RTEXITCODE_FAILURE : RTEXITCODE_SUCCESS; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleStat(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--dereference", 'L', RTGETOPT_REQ_NOTHING }, + { "--file-system", 'f', RTGETOPT_REQ_NOTHING }, + { "--format", 'c', RTGETOPT_REQ_STRING }, + { "--terse", 't', RTGETOPT_REQ_NOTHING } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + while ( (ch = RTGetOpt(&GetState, &ValueUnion)) != 0 + && ch != VINF_GETOPT_NOT_OPTION) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 'L': /* Dereference */ + case 'f': /* File-system */ + case 'c': /* Format */ + case 't': /* Terse */ + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_STAT, + "Command \"%s\" not implemented yet!", ValueUnion.psz); + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_STAT, ch, &ValueUnion); + } + } + + if (ch != VINF_GETOPT_NOT_OPTION) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_STAT, "Nothing to stat!"); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + + /* + * Do the file stat'ing. + */ + while (ch == VINF_GETOPT_NOT_OPTION) + { + if (pCtx->cVerbose) + RTPrintf("Checking for element \"%s\" ...\n", ValueUnion.psz); + + ComPtr<IGuestFsObjInfo> pFsObjInfo; + HRESULT hrc = pCtx->pGuestSession->FsObjQueryInfo(Bstr(ValueUnion.psz).raw(), FALSE /*followSymlinks*/, + pFsObjInfo.asOutParam()); + if (FAILED(hrc)) + { + /** @todo r=bird: There might be other reasons why we end up here than + * non-existing "element" (object or file, please, nobody calls it elements). */ + if (pCtx->cVerbose) + RTPrintf("Failed to stat '%s': No such file\n", ValueUnion.psz); + rcExit = RTEXITCODE_FAILURE; + } + else + { + RTPrintf(" File: '%s'\n", ValueUnion.psz); /** @todo escape this name. */ + + FsObjType_T enmType = FsObjType_Unknown; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(Type)(&enmType)); + LONG64 cbObject = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(ObjectSize)(&cbObject)); + LONG64 cbAllocated = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(AllocatedSize)(&cbAllocated)); + LONG uid = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(UID)(&uid)); + LONG gid = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(GID)(&gid)); + Bstr bstrUsername; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(UserName)(bstrUsername.asOutParam())); + Bstr bstrGroupName; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(GroupName)(bstrGroupName.asOutParam())); + Bstr bstrAttribs; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(FileAttributes)(bstrAttribs.asOutParam())); + LONG64 idNode = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(NodeId)(&idNode)); + ULONG uDevNode = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(NodeIdDevice)(&uDevNode)); + ULONG uDeviceNo = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(DeviceNumber)(&uDeviceNo)); + ULONG cHardLinks = 1; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(HardLinks)(&cHardLinks)); + LONG64 nsBirthTime = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(BirthTime)(&nsBirthTime)); + LONG64 nsChangeTime = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(ChangeTime)(&nsChangeTime)); + LONG64 nsModificationTime = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(ModificationTime)(&nsModificationTime)); + LONG64 nsAccessTime = 0; + CHECK_ERROR2I(pFsObjInfo, COMGETTER(AccessTime)(&nsAccessTime)); + + RTPrintf(" Size: %-17RU64 Alloc: %-19RU64 Type: %s\n", cbObject, cbAllocated, gctlFsObjTypeToName(enmType)); + RTPrintf("Device: %#-17RX32 INode: %-18RU64 Links: %u\n", uDevNode, idNode, cHardLinks); + + Utf8Str strAttrib(bstrAttribs); + char *pszMode = strAttrib.mutableRaw(); + char *pszAttribs = strchr(pszMode, ' '); + if (pszAttribs) + do *pszAttribs++ = '\0'; + while (*pszAttribs == ' '); + else + pszAttribs = strchr(pszMode, '\0'); + if (uDeviceNo != 0) + RTPrintf(" Mode: %-16s Attrib: %-17s Dev ID: %#RX32\n", pszMode, pszAttribs, uDeviceNo); + else + RTPrintf(" Mode: %-16s Attrib: %s\n", pszMode, pszAttribs); + + RTPrintf(" Owner: %4d/%-12ls Group: %4d/%ls\n", uid, bstrUsername.raw(), gid, bstrGroupName.raw()); + + RTTIMESPEC TimeSpec; + char szTmp[RTTIME_STR_LEN]; + RTPrintf(" Birth: %s\n", RTTimeSpecToString(RTTimeSpecSetNano(&TimeSpec, nsBirthTime), szTmp, sizeof(szTmp))); + RTPrintf("Change: %s\n", RTTimeSpecToString(RTTimeSpecSetNano(&TimeSpec, nsChangeTime), szTmp, sizeof(szTmp))); + RTPrintf("Modify: %s\n", RTTimeSpecToString(RTTimeSpecSetNano(&TimeSpec, nsModificationTime), szTmp, sizeof(szTmp))); + RTPrintf("Access: %s\n", RTTimeSpecToString(RTTimeSpecSetNano(&TimeSpec, nsAccessTime), szTmp, sizeof(szTmp))); + + /* Skiping: Generation ID - only the ISO9660 VFS sets this. FreeBSD user flags. */ + } + + /* Next file. */ + ch = RTGetOpt(&GetState, &ValueUnion); + } + + return rcExit; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleUpdateAdditions(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + /* + * Check the syntax. We can deduce the correct syntax from the number of + * arguments. + */ + Utf8Str strSource; + com::SafeArray<IN_BSTR> aArgs; + bool fWaitStartOnly = false; + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--source", 's', RTGETOPT_REQ_STRING }, + { "--wait-start", 'w', RTGETOPT_REQ_NOTHING } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + int vrc = VINF_SUCCESS; + while ( (ch = RTGetOpt(&GetState, &ValueUnion)) + && RT_SUCCESS(vrc)) + { + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 's': + strSource = ValueUnion.psz; + break; + + case 'w': + fWaitStartOnly = true; + break; + + case VINF_GETOPT_NOT_OPTION: + if (aArgs.size() == 0 && strSource.isEmpty()) + strSource = ValueUnion.psz; + else + aArgs.push_back(Bstr(ValueUnion.psz).raw()); + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_UPDATEGA, ch, &ValueUnion); + } + } + + if (pCtx->cVerbose) + RTPrintf("Updating Guest Additions ...\n"); + + HRESULT rc = S_OK; + while (strSource.isEmpty()) + { + ComPtr<ISystemProperties> pProperties; + CHECK_ERROR_BREAK(pCtx->pArg->virtualBox, COMGETTER(SystemProperties)(pProperties.asOutParam())); + Bstr strISO; + CHECK_ERROR_BREAK(pProperties, COMGETTER(DefaultAdditionsISO)(strISO.asOutParam())); + strSource = strISO; + break; + } + + /* Determine source if not set yet. */ + if (strSource.isEmpty()) + { + RTMsgError("No Guest Additions source found or specified, aborting\n"); + vrc = VERR_FILE_NOT_FOUND; + } + else if (!RTFileExists(strSource.c_str())) + { + RTMsgError("Source \"%s\" does not exist!\n", strSource.c_str()); + vrc = VERR_FILE_NOT_FOUND; + } + + if (RT_SUCCESS(vrc)) + { + if (pCtx->cVerbose) + RTPrintf("Using source: %s\n", strSource.c_str()); + + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + + com::SafeArray<AdditionsUpdateFlag_T> aUpdateFlags; + if (fWaitStartOnly) + { + aUpdateFlags.push_back(AdditionsUpdateFlag_WaitForUpdateStartOnly); + if (pCtx->cVerbose) + RTPrintf("Preparing and waiting for Guest Additions installer to start ...\n"); + } + + ComPtr<IProgress> pProgress; + CHECK_ERROR(pCtx->pGuest, UpdateGuestAdditions(Bstr(strSource).raw(), + ComSafeArrayAsInParam(aArgs), + /* Wait for whole update process to complete. */ + ComSafeArrayAsInParam(aUpdateFlags), + pProgress.asOutParam())); + if (FAILED(rc)) + vrc = gctlPrintError(pCtx->pGuest, COM_IIDOF(IGuest)); + else + { + if (pCtx->cVerbose) + rc = showProgress(pProgress); + else + rc = pProgress->WaitForCompletion(-1 /* No timeout */); + + if (SUCCEEDED(rc)) + CHECK_PROGRESS_ERROR(pProgress, ("Guest additions update failed")); + vrc = gctlPrintProgressError(pProgress); + if ( RT_SUCCESS(vrc) + && pCtx->cVerbose) + { + RTPrintf("Guest Additions update successful\n"); + } + } + } + + return RT_SUCCESS(vrc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleList(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + bool fSeenListArg = false; + bool fListAll = false; + bool fListSessions = false; + bool fListProcesses = false; + bool fListFiles = false; + + int vrc = VINF_SUCCESS; + while ( (ch = RTGetOpt(&GetState, &ValueUnion)) + && RT_SUCCESS(vrc)) + { + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case VINF_GETOPT_NOT_OPTION: + if ( !RTStrICmp(ValueUnion.psz, "sessions") + || !RTStrICmp(ValueUnion.psz, "sess")) + fListSessions = true; + else if ( !RTStrICmp(ValueUnion.psz, "processes") + || !RTStrICmp(ValueUnion.psz, "procs")) + fListSessions = fListProcesses = true; /* Showing processes implies showing sessions. */ + else if (!RTStrICmp(ValueUnion.psz, "files")) + fListSessions = fListFiles = true; /* Showing files implies showing sessions. */ + else if (!RTStrICmp(ValueUnion.psz, "all")) + fListAll = true; + else + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_LIST, + "Unknown list: '%s'", ValueUnion.psz); + fSeenListArg = true; + break; + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_UPDATEGA, ch, &ValueUnion); + } + } + + if (!fSeenListArg) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_LIST, "Missing list name"); + Assert(fListAll || fListSessions); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + + /** @todo Do we need a machine-readable output here as well? */ + + HRESULT rc; + size_t cTotalProcs = 0; + size_t cTotalFiles = 0; + + SafeIfaceArray <IGuestSession> collSessions; + CHECK_ERROR(pCtx->pGuest, COMGETTER(Sessions)(ComSafeArrayAsOutParam(collSessions))); + if (SUCCEEDED(rc)) + { + size_t const cSessions = collSessions.size(); + if (cSessions) + { + RTPrintf("Active guest sessions:\n"); + + /** @todo Make this output a bit prettier. No time now. */ + + for (size_t i = 0; i < cSessions; i++) + { + ComPtr<IGuestSession> pCurSession = collSessions[i]; + if (!pCurSession.isNull()) + { + do + { + ULONG uID; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(Id)(&uID)); + Bstr strName; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(Name)(strName.asOutParam())); + Bstr strUser; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(User)(strUser.asOutParam())); + GuestSessionStatus_T sessionStatus; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(Status)(&sessionStatus)); + RTPrintf("\n\tSession #%-3zu ID=%-3RU32 User=%-16ls Status=[%s] Name=%ls", + i, uID, strUser.raw(), gctlGuestSessionStatusToText(sessionStatus), strName.raw()); + } while (0); + + if ( fListAll + || fListProcesses) + { + SafeIfaceArray <IGuestProcess> collProcesses; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(Processes)(ComSafeArrayAsOutParam(collProcesses))); + for (size_t a = 0; a < collProcesses.size(); a++) + { + ComPtr<IGuestProcess> pCurProcess = collProcesses[a]; + if (!pCurProcess.isNull()) + { + do + { + ULONG uPID; + CHECK_ERROR_BREAK(pCurProcess, COMGETTER(PID)(&uPID)); + Bstr strExecPath; + CHECK_ERROR_BREAK(pCurProcess, COMGETTER(ExecutablePath)(strExecPath.asOutParam())); + ProcessStatus_T procStatus; + CHECK_ERROR_BREAK(pCurProcess, COMGETTER(Status)(&procStatus)); + + RTPrintf("\n\t\tProcess #%-03zu PID=%-6RU32 Status=[%s] Command=%ls", + a, uPID, gctlProcessStatusToText(procStatus), strExecPath.raw()); + } while (0); + } + } + + cTotalProcs += collProcesses.size(); + } + + if ( fListAll + || fListFiles) + { + SafeIfaceArray <IGuestFile> collFiles; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(Files)(ComSafeArrayAsOutParam(collFiles))); + for (size_t a = 0; a < collFiles.size(); a++) + { + ComPtr<IGuestFile> pCurFile = collFiles[a]; + if (!pCurFile.isNull()) + { + do + { + ULONG idFile; + CHECK_ERROR_BREAK(pCurFile, COMGETTER(Id)(&idFile)); + Bstr strName; + CHECK_ERROR_BREAK(pCurFile, COMGETTER(Filename)(strName.asOutParam())); + FileStatus_T fileStatus; + CHECK_ERROR_BREAK(pCurFile, COMGETTER(Status)(&fileStatus)); + + RTPrintf("\n\t\tFile #%-03zu ID=%-6RU32 Status=[%s] Name=%ls", + a, idFile, gctlFileStatusToText(fileStatus), strName.raw()); + } while (0); + } + } + + cTotalFiles += collFiles.size(); + } + } + } + + RTPrintf("\n\nTotal guest sessions: %zu\n", collSessions.size()); + if (fListAll || fListProcesses) + RTPrintf("Total guest processes: %zu\n", cTotalProcs); + if (fListAll || fListFiles) + RTPrintf("Total guest files: %zu\n", cTotalFiles); + } + else + RTPrintf("No active guest sessions found\n"); + } + + if (FAILED(rc)) /** @todo yeah, right... Only the last error? */ + rcExit = RTEXITCODE_FAILURE; + + return rcExit; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleCloseProcess(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--session-id", 'i', RTGETOPT_REQ_UINT32 }, + { "--session-name", 'n', RTGETOPT_REQ_STRING } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + std::vector < uint32_t > vecPID; + ULONG idSession = UINT32_MAX; + Utf8Str strSessionName; + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 'n': /* Session name (or pattern) */ + strSessionName = ValueUnion.psz; + break; + + case 'i': /* Session ID */ + idSession = ValueUnion.u32; + break; + + case VINF_GETOPT_NOT_OPTION: + { + /* Treat every else specified as a PID to kill. */ + uint32_t uPid; + int rc = RTStrToUInt32Ex(ValueUnion.psz, NULL, 0, &uPid); + if ( RT_SUCCESS(rc) + && rc != VWRN_TRAILING_CHARS + && rc != VWRN_NUMBER_TOO_BIG + && rc != VWRN_NEGATIVE_UNSIGNED) + { + if (uPid != 0) + { + try + { + vecPID.push_back(uPid); + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Out of memory"); + } + } + else + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSEPROCESS, "Invalid PID value: 0"); + } + else + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSEPROCESS, "Error parsing PID value: %Rrc", rc); + break; + } + + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSEPROCESS, ch, &ValueUnion); + } + } + + if (vecPID.empty()) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSEPROCESS, + "At least one PID must be specified to kill!"); + + if ( strSessionName.isEmpty() + && idSession == UINT32_MAX) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSEPROCESS, "No session ID specified!"); + + if ( strSessionName.isNotEmpty() + && idSession != UINT32_MAX) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSEPROCESS, + "Either session ID or name (pattern) must be specified"); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + HRESULT rc = S_OK; + + ComPtr<IGuestSession> pSession; + ComPtr<IGuestProcess> pProcess; + do + { + uint32_t uProcsTerminated = 0; + + SafeIfaceArray <IGuestSession> collSessions; + CHECK_ERROR_BREAK(pCtx->pGuest, COMGETTER(Sessions)(ComSafeArrayAsOutParam(collSessions))); + size_t cSessions = collSessions.size(); + + uint32_t cSessionsHandled = 0; + for (size_t i = 0; i < cSessions; i++) + { + pSession = collSessions[i]; + Assert(!pSession.isNull()); + + ULONG uID; /* Session ID */ + CHECK_ERROR_BREAK(pSession, COMGETTER(Id)(&uID)); + Bstr strName; + CHECK_ERROR_BREAK(pSession, COMGETTER(Name)(strName.asOutParam())); + Utf8Str strNameUtf8(strName); /* Session name */ + + bool fSessionFound; + if (strSessionName.isEmpty()) /* Search by ID. Slow lookup. */ + fSessionFound = uID == idSession; + else /* ... or by naming pattern. */ + fSessionFound = RTStrSimplePatternMatch(strSessionName.c_str(), strNameUtf8.c_str()); + if (fSessionFound) + { + AssertStmt(!pSession.isNull(), break); + cSessionsHandled++; + + SafeIfaceArray <IGuestProcess> collProcs; + CHECK_ERROR_BREAK(pSession, COMGETTER(Processes)(ComSafeArrayAsOutParam(collProcs))); + + size_t cProcs = collProcs.size(); + for (size_t p = 0; p < cProcs; p++) + { + pProcess = collProcs[p]; + Assert(!pProcess.isNull()); + + ULONG uPID; /* Process ID */ + CHECK_ERROR_BREAK(pProcess, COMGETTER(PID)(&uPID)); + + bool fProcFound = false; + for (size_t a = 0; a < vecPID.size(); a++) /* Slow, but works. */ + { + fProcFound = vecPID[a] == uPID; + if (fProcFound) + break; + } + + if (fProcFound) + { + if (pCtx->cVerbose) + RTPrintf("Terminating process (PID %RU32) (session ID %RU32) ...\n", + uPID, uID); + CHECK_ERROR_BREAK(pProcess, Terminate()); + uProcsTerminated++; + } + else + { + if (idSession != UINT32_MAX) + RTPrintf("No matching process(es) for session ID %RU32 found\n", + idSession); + } + + pProcess.setNull(); + } + + pSession.setNull(); + } + } + + if (!cSessionsHandled) + RTPrintf("No matching session(s) found\n"); + + if (uProcsTerminated) + RTPrintf("%RU32 %s terminated\n", + uProcsTerminated, uProcsTerminated == 1 ? "process" : "processes"); + + } while (0); + + pProcess.setNull(); + pSession.setNull(); + + return SUCCEEDED(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; +} + + +static DECLCALLBACK(RTEXITCODE) gctlHandleCloseSession(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + enum GETOPTDEF_SESSIONCLOSE + { + GETOPTDEF_SESSIONCLOSE_ALL = 2000 + }; + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--all", GETOPTDEF_SESSIONCLOSE_ALL, RTGETOPT_REQ_NOTHING }, + { "--session-id", 'i', RTGETOPT_REQ_UINT32 }, + { "--session-name", 'n', RTGETOPT_REQ_STRING } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + ULONG idSession = UINT32_MAX; + Utf8Str strSessionName; + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case 'n': /* Session name pattern */ + strSessionName = ValueUnion.psz; + break; + + case 'i': /* Session ID */ + idSession = ValueUnion.u32; + break; + + case GETOPTDEF_SESSIONCLOSE_ALL: + strSessionName = "*"; + break; + + case VINF_GETOPT_NOT_OPTION: + /** @todo Supply a CSV list of IDs or patterns to close? + * break; */ + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSESESSION, ch, &ValueUnion); + } + } + + if ( strSessionName.isEmpty() + && idSession == UINT32_MAX) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSESESSION, + "No session ID specified!"); + + if ( !strSessionName.isEmpty() + && idSession != UINT32_MAX) + return errorSyntaxEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_CLOSESESSION, + "Either session ID or name (pattern) must be specified"); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + HRESULT rc = S_OK; + + do + { + size_t cSessionsHandled = 0; + + SafeIfaceArray <IGuestSession> collSessions; + CHECK_ERROR_BREAK(pCtx->pGuest, COMGETTER(Sessions)(ComSafeArrayAsOutParam(collSessions))); + size_t cSessions = collSessions.size(); + + for (size_t i = 0; i < cSessions; i++) + { + ComPtr<IGuestSession> pSession = collSessions[i]; + Assert(!pSession.isNull()); + + ULONG uID; /* Session ID */ + CHECK_ERROR_BREAK(pSession, COMGETTER(Id)(&uID)); + Bstr strName; + CHECK_ERROR_BREAK(pSession, COMGETTER(Name)(strName.asOutParam())); + Utf8Str strNameUtf8(strName); /* Session name */ + + bool fSessionFound; + if (strSessionName.isEmpty()) /* Search by ID. Slow lookup. */ + fSessionFound = uID == idSession; + else /* ... or by naming pattern. */ + fSessionFound = RTStrSimplePatternMatch(strSessionName.c_str(), strNameUtf8.c_str()); + if (fSessionFound) + { + cSessionsHandled++; + + Assert(!pSession.isNull()); + if (pCtx->cVerbose) + RTPrintf("Closing guest session ID=#%RU32 \"%s\" ...\n", + uID, strNameUtf8.c_str()); + CHECK_ERROR_BREAK(pSession, Close()); + if (pCtx->cVerbose) + RTPrintf("Guest session successfully closed\n"); + + pSession.setNull(); + } + } + + if (!cSessionsHandled) + { + RTPrintf("No guest session(s) found\n"); + rc = E_ABORT; /* To set exit code accordingly. */ + } + + } while (0); + + return SUCCEEDED(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; +} + + +static DECLCALLBACK(RTEXITCODE) gctlHandleWatch(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + /* + * Parse arguments. + */ + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + /* For options that require an argument, ValueUnion has received the value. */ + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case VINF_GETOPT_NOT_OPTION: + default: + return errorGetOptEx(USAGE_GUESTCONTROL, USAGE_GSTCTRL_WATCH, ch, &ValueUnion); + } + } + + /** @todo Specify categories to watch for. */ + /** @todo Specify a --timeout for waiting only for a certain amount of time? */ + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + HRESULT rc; + + try + { + ComObjPtr<GuestEventListenerImpl> pGuestListener; + do + { + /* Listener creation. */ + pGuestListener.createObject(); + pGuestListener->init(new GuestEventListener()); + + /* Register for IGuest events. */ + ComPtr<IEventSource> es; + CHECK_ERROR_BREAK(pCtx->pGuest, COMGETTER(EventSource)(es.asOutParam())); + com::SafeArray<VBoxEventType_T> eventTypes; + eventTypes.push_back(VBoxEventType_OnGuestSessionRegistered); + /** @todo Also register for VBoxEventType_OnGuestUserStateChanged on demand? */ + CHECK_ERROR_BREAK(es, RegisterListener(pGuestListener, ComSafeArrayAsInParam(eventTypes), + true /* Active listener */)); + /* Note: All other guest control events have to be registered + * as their corresponding objects appear. */ + + } while (0); + + if (pCtx->cVerbose) + RTPrintf("Waiting for events ...\n"); + +/** @todo r=bird: This are-we-there-yet approach to things could easily be + * replaced by a global event semaphore that gets signalled from the + * signal handler and the callback event. Please fix! */ + while (!g_fGuestCtrlCanceled) + { + /** @todo Timeout handling (see above)? */ + RTThreadSleep(10); + } + + if (pCtx->cVerbose) + RTPrintf("Signal caught, exiting ...\n"); + + if (!pGuestListener.isNull()) + { + /* Guest callback unregistration. */ + ComPtr<IEventSource> pES; + CHECK_ERROR(pCtx->pGuest, COMGETTER(EventSource)(pES.asOutParam())); + if (!pES.isNull()) + CHECK_ERROR(pES, UnregisterListener(pGuestListener)); + pGuestListener.setNull(); + } + } + catch (std::bad_alloc &) + { + rc = E_OUTOFMEMORY; + } + + return SUCCEEDED(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; +} + +/** + * Access the guest control store. + * + * @returns program exit code. + * @note see the command line API description for parameters + */ +RTEXITCODE handleGuestControl(HandlerArg *pArg) +{ + AssertPtr(pArg); + +#ifdef DEBUG_andy_disabled + if (RT_FAILURE(tstTranslatePath())) + return RTEXITCODE_FAILURE; +#endif + + /* + * Command definitions. + */ + static const GCTLCMDDEF s_aCmdDefs[] = + { + { "run", gctlHandleRun, USAGE_GSTCTRL_RUN, 0, }, + { "start", gctlHandleStart, USAGE_GSTCTRL_START, 0, }, + { "copyfrom", gctlHandleCopyFrom, USAGE_GSTCTRL_COPYFROM, 0, }, + { "copyto", gctlHandleCopyTo, USAGE_GSTCTRL_COPYTO, 0, }, + + { "mkdir", gctrlHandleMkDir, USAGE_GSTCTRL_MKDIR, 0, }, + { "md", gctrlHandleMkDir, USAGE_GSTCTRL_MKDIR, 0, }, + { "createdirectory", gctrlHandleMkDir, USAGE_GSTCTRL_MKDIR, 0, }, + { "createdir", gctrlHandleMkDir, USAGE_GSTCTRL_MKDIR, 0, }, + + { "rmdir", gctlHandleRmDir, USAGE_GSTCTRL_RMDIR, 0, }, + { "removedir", gctlHandleRmDir, USAGE_GSTCTRL_RMDIR, 0, }, + { "removedirectory", gctlHandleRmDir, USAGE_GSTCTRL_RMDIR, 0, }, + + { "rm", gctlHandleRm, USAGE_GSTCTRL_RM, 0, }, + { "removefile", gctlHandleRm, USAGE_GSTCTRL_RM, 0, }, + { "erase", gctlHandleRm, USAGE_GSTCTRL_RM, 0, }, + { "del", gctlHandleRm, USAGE_GSTCTRL_RM, 0, }, + { "delete", gctlHandleRm, USAGE_GSTCTRL_RM, 0, }, + + { "mv", gctlHandleMv, USAGE_GSTCTRL_MV, 0, }, + { "move", gctlHandleMv, USAGE_GSTCTRL_MV, 0, }, + { "ren", gctlHandleMv, USAGE_GSTCTRL_MV, 0, }, + { "rename", gctlHandleMv, USAGE_GSTCTRL_MV, 0, }, + + { "mktemp", gctlHandleMkTemp, USAGE_GSTCTRL_MKTEMP, 0, }, + { "createtemp", gctlHandleMkTemp, USAGE_GSTCTRL_MKTEMP, 0, }, + { "createtemporary", gctlHandleMkTemp, USAGE_GSTCTRL_MKTEMP, 0, }, + + { "stat", gctlHandleStat, USAGE_GSTCTRL_STAT, 0, }, + + { "closeprocess", gctlHandleCloseProcess, USAGE_GSTCTRL_CLOSEPROCESS, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER, }, + { "closesession", gctlHandleCloseSession, USAGE_GSTCTRL_CLOSESESSION, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER, }, + { "list", gctlHandleList, USAGE_GSTCTRL_LIST, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER, }, + { "watch", gctlHandleWatch, USAGE_GSTCTRL_WATCH, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER, }, + + {"updateguestadditions",gctlHandleUpdateAdditions, USAGE_GSTCTRL_UPDATEGA, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER, }, + { "updateadditions", gctlHandleUpdateAdditions, USAGE_GSTCTRL_UPDATEGA, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER, }, + { "updatega", gctlHandleUpdateAdditions, USAGE_GSTCTRL_UPDATEGA, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER, }, + }; + + /* + * VBoxManage guestcontrol [common-options] <VM> [common-options] <sub-command> ... + * + * Parse common options and VM name until we find a sub-command. Allowing + * the user to put the user and password related options before the + * sub-command makes it easier to edit the command line when doing several + * operations with the same guest user account. (Accidentally, it also + * makes the syntax diagram shorter and easier to read.) + */ + GCTLCMDCTX CmdCtx; + RTEXITCODE rcExit = gctrCmdCtxInit(&CmdCtx, pArg); + if (rcExit == RTEXITCODE_SUCCESS) + { + static const RTGETOPTDEF s_CommonOptions[] = { GCTLCMD_COMMON_OPTION_DEFS() }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, pArg->argc, pArg->argv, s_CommonOptions, RT_ELEMENTS(s_CommonOptions), 0, 0 /* No sorting! */); + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(&CmdCtx, ch, &ValueUnion); + + case VINF_GETOPT_NOT_OPTION: + /* First comes the VM name or UUID. */ + if (!CmdCtx.pszVmNameOrUuid) + CmdCtx.pszVmNameOrUuid = ValueUnion.psz; + /* + * The sub-command is next. Look it up and invoke it. + * Note! Currently no warnings about user/password options (like we'll do later on) + * for GCTLCMDCTX_F_SESSION_ANONYMOUS commands. No reason to be too pedantic. + */ + else + { + const char *pszCmd = ValueUnion.psz; + uint32_t iCmd; + for (iCmd = 0; iCmd < RT_ELEMENTS(s_aCmdDefs); iCmd++) + if (strcmp(s_aCmdDefs[iCmd].pszName, pszCmd) == 0) + { + CmdCtx.pCmdDef = &s_aCmdDefs[iCmd]; + + rcExit = s_aCmdDefs[iCmd].pfnHandler(&CmdCtx, pArg->argc - GetState.iNext + 1, + &pArg->argv[GetState.iNext - 1]); + + gctlCtxTerm(&CmdCtx); + return rcExit; + } + return errorSyntax(USAGE_GUESTCONTROL, "Unknown sub-command: '%s'", pszCmd); + } + break; + + default: + return errorGetOpt(USAGE_GUESTCONTROL, ch, &ValueUnion); + } + } + if (CmdCtx.pszVmNameOrUuid) + rcExit = errorSyntax(USAGE_GUESTCONTROL, "Missing sub-command"); + else + rcExit = errorSyntax(USAGE_GUESTCONTROL, "Missing VM name and sub-command"); + } + return rcExit; +} +#endif /* !VBOX_ONLY_DOCS */ + |