From f215e02bf85f68d3a6106c2a1f4f7f063f819064 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:17:27 +0200 Subject: Adding upstream version 7.0.14-dfsg. Signed-off-by: Daniel Baumann --- .../Frontends/VBoxManage/VBoxManageGuestCtrl.cpp | 3707 ++++++++++++++++++++ 1 file changed, 3707 insertions(+) create mode 100644 src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp (limited to 'src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp') diff --git a/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp b/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp new file mode 100644 index 00000000..f5a1e446 --- /dev/null +++ b/src/VBox/Frontends/VBoxManage/VBoxManageGuestCtrl.cpp @@ -0,0 +1,3707 @@ +/* $Id: VBoxManageGuestCtrl.cpp $ */ +/** @file + * VBoxManage - Implementation of guestcontrol command. + */ + +/* + * Copyright (C) 2010-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, in version 3 of the + * License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include "VBoxManage.h" +#include "VBoxManageGuestCtrl.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include /* For RTProcSelf(). */ +#include +#include +#include + +#include + +#include +#include + +#ifdef USE_XPCOM_QUEUE +# include +# include +#endif + +#include + +#ifdef RT_OS_DARWIN +# include +#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() \ + { "--user", GCTLCMD_COMMON_OPT_USER, RTGETOPT_REQ_STRING }, \ + { "--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; +/** Event semaphore used for wait notifications. + * Also being used for the listener implementations in VBoxManageGuestCtrlListener.cpp. */ + RTSEMEVENT g_SemEventGuestCtrlCanceled = NIL_RTSEMEVENT; + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Listener declarations. + */ +VBOX_LISTENER_DECLARE(GuestFileEventListenerImpl) +VBOX_LISTENER_DECLARE(GuestProcessEventListenerImpl) +VBOX_LISTENER_DECLARE(GuestSessionEventListenerImpl) +VBOX_LISTENER_DECLARE(GuestEventListenerImpl) +VBOX_LISTENER_DECLARE(GuestAdditionsRunlevelListener) + +/** + * 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 sub-command scope flags. */ + uint64_t fSubcommandScope; + /** 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 pGuest; + /** Pointer to the to be used guest session. */ + ComPtr 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 > DESTDIRMAP, *PDESTDIRMAP; +typedef std::map< Utf8Str, std::vector >::iterator DESTDIRMAPITER, *PDESTDIRMAPITER; + + +enum kStreamTransform +{ + kStreamTransform_None = 0, + kStreamTransform_Dos2Unix, + kStreamTransform_Unix2Dos +}; + + +DECLARE_TRANSLATION_CONTEXT(GuestCtrl); + + +#ifdef RT_OS_WINDOWS +static BOOL WINAPI gctlSignalHandler(DWORD dwCtrlType) RT_NOTHROW_DEF +{ + 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); + RTSemEventSignal(g_SemEventGuestCtrlCanceled); + 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) RT_NOTHROW_DEF +{ + RT_NOREF(iSignal); + ASMAtomicWriteBool(&g_fGuestCtrlCanceled, true); + RTSemEventSignal(g_SemEventGuestCtrlCanceled); +} +#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 vrc = VINF_SUCCESS; +#ifdef RT_OS_WINDOWS + if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)gctlSignalHandler, TRUE /* Add handler */)) + { + vrc = RTErrConvertFromWin32(GetLastError()); + RTMsgError(GuestCtrl::tr("Unable to install console control handler, vrc=%Rrc\n"), vrc); + } +#else + signal(SIGINT, gctlSignalHandler); + signal(SIGTERM, gctlSignalHandler); +# ifdef SIGBREAK + signal(SIGBREAK, gctlSignalHandler); +# endif +#endif + + if (RT_SUCCESS(vrc)) + vrc = RTSemEventCreate(&g_SemEventGuestCtrlCanceled); + + return vrc; +} + + +/** + * Uninstalls a previously installed signal handler. + */ +static int gctlSignalHandlerUninstall(void) +{ + int vrc = VINF_SUCCESS; +#ifdef RT_OS_WINDOWS + if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)NULL, FALSE /* Remove handler */)) + { + vrc = RTErrConvertFromWin32(GetLastError()); + RTMsgError(GuestCtrl::tr("Unable to uninstall console control handler, vrc=%Rrc\n"), vrc); + } +#else + signal(SIGINT, SIG_DFL); + signal(SIGTERM, SIG_DFL); +# ifdef SIGBREAK + signal(SIGBREAK, SIG_DFL); +# endif +#endif + + if (g_SemEventGuestCtrlCanceled != NIL_RTSEMEVENT) + { + RTSemEventDestroy(g_SemEventGuestCtrlCanceled); + g_SemEventGuestCtrlCanceled = NIL_RTSEMEVENT; + } + return vrc; +} + + +/** + * Translates a process status to a human readable string. + * + * @sa GuestProcess::i_statusToString() + */ +const char *gctlProcessStatusToText(ProcessStatus_T enmStatus) +{ + switch (enmStatus) + { + case ProcessStatus_Starting: + return GuestCtrl::tr("starting"); + case ProcessStatus_Started: + return GuestCtrl::tr("started"); + case ProcessStatus_Paused: + return GuestCtrl::tr("paused"); + case ProcessStatus_Terminating: + return GuestCtrl::tr("terminating"); + case ProcessStatus_TerminatedNormally: + return GuestCtrl::tr("successfully terminated"); + case ProcessStatus_TerminatedSignal: + return GuestCtrl::tr("terminated by signal"); + case ProcessStatus_TerminatedAbnormally: + return GuestCtrl::tr("abnormally aborted"); + case ProcessStatus_TimedOutKilled: + return GuestCtrl::tr("timed out"); + case ProcessStatus_TimedOutAbnormally: + return GuestCtrl::tr("timed out, hanging"); + case ProcessStatus_Down: + return GuestCtrl::tr("killed"); + case ProcessStatus_Error: + return GuestCtrl::tr("error"); + default: + break; + } + return GuestCtrl::tr("unknown"); +} + +/** + * Translates a guest process wait result to a human readable string. + */ +static const char *gctlProcessWaitResultToText(ProcessWaitResult_T enmWaitResult) +{ + switch (enmWaitResult) + { + case ProcessWaitResult_Start: + return GuestCtrl::tr("started"); + case ProcessWaitResult_Terminate: + return GuestCtrl::tr("terminated"); + case ProcessWaitResult_Status: + return GuestCtrl::tr("status changed"); + case ProcessWaitResult_Error: + return GuestCtrl::tr("error"); + case ProcessWaitResult_Timeout: + return GuestCtrl::tr("timed out"); + case ProcessWaitResult_StdIn: + return GuestCtrl::tr("stdin ready"); + case ProcessWaitResult_StdOut: + return GuestCtrl::tr("data on stdout"); + case ProcessWaitResult_StdErr: + return GuestCtrl::tr("data on stderr"); + case ProcessWaitResult_WaitFlagNotSupported: + return GuestCtrl::tr("waiting flag not supported"); + default: + break; + } + return GuestCtrl::tr("unknown"); +} + +/** + * Translates a guest session status to a human readable string. + */ +const char *gctlGuestSessionStatusToText(GuestSessionStatus_T enmStatus) +{ + switch (enmStatus) + { + case GuestSessionStatus_Starting: + return GuestCtrl::tr("starting"); + case GuestSessionStatus_Started: + return GuestCtrl::tr("started"); + case GuestSessionStatus_Terminating: + return GuestCtrl::tr("terminating"); + case GuestSessionStatus_Terminated: + return GuestCtrl::tr("terminated"); + case GuestSessionStatus_TimedOutKilled: + return GuestCtrl::tr("timed out"); + case GuestSessionStatus_TimedOutAbnormally: + return GuestCtrl::tr("timed out, hanging"); + case GuestSessionStatus_Down: + return GuestCtrl::tr("killed"); + case GuestSessionStatus_Error: + return GuestCtrl::tr("error"); + default: + break; + } + return GuestCtrl::tr("unknown"); +} + +/** + * Translates a guest file status to a human readable string. + */ +const char *gctlFileStatusToText(FileStatus_T enmStatus) +{ + switch (enmStatus) + { + case FileStatus_Opening: + return GuestCtrl::tr("opening"); + case FileStatus_Open: + return GuestCtrl::tr("open"); + case FileStatus_Closing: + return GuestCtrl::tr("closing"); + case FileStatus_Closed: + return GuestCtrl::tr("closed"); + case FileStatus_Down: + return GuestCtrl::tr("killed"); + case FileStatus_Error: + return GuestCtrl::tr("error"); + default: + break; + } + return GuestCtrl::tr("unknown"); +} + +/** + * Translates a file system objec type to a string. + */ +const char *gctlFsObjTypeToName(FsObjType_T enmType) +{ + switch (enmType) + { + case FsObjType_Unknown: return GuestCtrl::tr("unknown"); + case FsObjType_Fifo: return GuestCtrl::tr("fifo"); + case FsObjType_DevChar: return GuestCtrl::tr("char-device"); + case FsObjType_Directory: return GuestCtrl::tr("directory"); + case FsObjType_DevBlock: return GuestCtrl::tr("block-device"); + case FsObjType_File: return GuestCtrl::tr("file"); + case FsObjType_Symlink: return GuestCtrl::tr("symlink"); + case FsObjType_Socket: return GuestCtrl::tr("socket"); + case FsObjType_WhiteOut: return GuestCtrl::tr("white-out"); +#ifdef VBOX_WITH_XPCOM_CPP_ENUM_HACK + case FsObjType_32BitHack: break; +#endif + } + return GuestCtrl::tr("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(GuestCtrl::tr("Error details:")); + GluePrintErrorInfo(errorInfo); + } + return VERR_GENERAL_FAILURE; /** @todo */ + } + AssertMsgFailedReturn((GuestCtrl::tr("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 pProgress) +{ + int vrc = VINF_SUCCESS; + HRESULT hrc; + + 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(hrc), (GuestCtrl::tr("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) +{ + pCtx->pArg = pArg; + pCtx->pCmdDef = NULL; + pCtx->pszVmNameOrUuid = NULL; + pCtx->fPostOptionParsingInited = false; + pCtx->fLockedVmSession = false; + pCtx->fDetachGuestSession = false; + pCtx->fInstalledSignalHandler = false; + pCtx->cVerbose = 0; + pCtx->strUsername.setNull(); + pCtx->strPassword.setNull(); + pCtx->strDomain.setNull(); + pCtx->pGuest.setNull(); + pCtx->pGuestSession.setNull(); + pCtx->uSessionID = 0; + + /* + * The user name defaults to the host one, if we can get at it. + */ + char szUser[1024]; + int vrc = RTProcQueryUsername(RTProcSelf(), szUser, sizeof(szUser), NULL); + if ( RT_SUCCESS(vrc) + && RTStrIsValidEncoding(szUser)) /* paranoia was required on posix at some point, not needed any more! */ + { + try + { + pCtx->strUsername = szUser; + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("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(GuestCtrl::tr("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(GuestCtrl::tr("Password is given more than once.")); + pCtx->strPassword = pValueUnion->psz; + } + else + RTMsgWarning(GuestCtrl::tr("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(GuestCtrl::tr("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(GuestCtrl::tr("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 hrc; + AssertPtr(pCtx); + AssertPtr(pCtx->pArg); + + /* + * Find the VM and check if it's running. + */ + ComPtr machine; + CHECK_ERROR(pCtx->pArg->virtualBox, FindMachine(Bstr(pCtx->pszVmNameOrUuid).raw(), machine.asOutParam())); + if (SUCCEEDED(hrc)) + { + MachineState_T enmMachineState; + CHECK_ERROR(machine, COMGETTER(State)(&enmMachineState)); + if ( SUCCEEDED(hrc) + && 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(hrc)) + { + pCtx->fLockedVmSession = true; + ComPtr ptrConsole; + CHECK_ERROR(pCtx->pArg->session, COMGETTER(Console)(ptrConsole.asOutParam())); + if (SUCCEEDED(hrc)) + { + if (ptrConsole.isNotNull()) + { + CHECK_ERROR(ptrConsole, COMGETTER(Guest)(pCtx->pGuest.asOutParam())); + if (SUCCEEDED(hrc)) + return RTEXITCODE_SUCCESS; + } + else + RTMsgError(GuestCtrl::tr("Failed to get a IConsole pointer for the machine. Is it still running?\n")); + } + } + } + else if (SUCCEEDED(hrc)) + RTMsgError(GuestCtrl::tr("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 hrc; + 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, + GuestCtrl::tr("[%RU32] VBoxManage Guest Control [%s] - %s"), + RTProcSelf(), pCtx->pszVmNameOrUuid, pCtx->pCmdDef->pszName) < 0) + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("No enough memory for session name")); + + /* + * Create a guest session. + */ + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("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(GuestCtrl::tr("Out of memory setting up IGuest::CreateSession call")); + hrc = E_OUTOFMEMORY; + } + if (SUCCEEDED(hrc)) + { + /* + * Wait for guest session to start. + */ + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Waiting for guest session to start...\n")); + GuestSessionWaitResult_T enmWaitResult = GuestSessionWaitResult_None; /* Shut up MSC */ + try + { + com::SafeArray 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(GuestCtrl::tr("Out of memory setting up IGuestSession::WaitForArray call")); + hrc = E_OUTOFMEMORY; + } + if (SUCCEEDED(hrc)) + { + /* 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(hrc)) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("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(GuestCtrl::tr("Error starting guest session (current status is: %s)\n"), + SUCCEEDED(hrc) ? gctlGuestSessionStatusToText(enmSessionStatus) : GuestCtrl::tr("")); + } + } + } + + 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 vrc = gctlSignalHandlerInstall(); + pCtx->fInstalledSignalHandler = RT_SUCCESS(vrc); + } + } + } + else + rcExit = errorSyntax(GuestCtrl::tr("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 hrc; + 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(GuestCtrl::tr("Closing guest session ...\n")); + + CHECK_ERROR(pCtx->pGuestSession, Close()); + } + else if ( pCtx->fDetachGuestSession + && pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("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. Can be the bit bucket or a (valid [std]) handle. + * @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 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(GuestCtrl::tr("Unable to write output, vrc=%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 to std handles, + * or going to the bit bucket instead. + * @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(GuestCtrl::tr("Unsupported %s line ending conversion"), pszName); + /** @todo Implement dos2unix and unix2dos stream filters. */ + } + return true; + } + RTMsgWarning(GuestCtrl::tr("Error getting %s handle: %Rrc"), pszName, vrc); + } + else /* If disabled, all goes to / gets fed to/from the bit bucket. */ + { + RTFILE hFile; + int vrc = RTFileOpenBitBucket(&hFile, enmHandle == RTHANDLESTD_INPUT ? RTFILE_O_READ : RTFILE_O_WRITE); + if (RT_SUCCESS(vrc)) + { + vrc = RTVfsIoStrmFromRTFile(hFile, 0 /* fOpen */, false /* fLeaveOpen */, phVfsIos); + if (RT_SUCCESS(vrc)) + return true; + } + } + + 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'. + */ +static RTEXITCODE gctlHandleRunCommon(PGCTLCMDCTX pCtx, int argc, char **argv, bool fRunCmd) +{ + 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() + { "--arg0", '0', RTGETOPT_REQ_STRING }, + { "--putenv", 'E', RTGETOPT_REQ_STRING }, + { "--exe", 'e', RTGETOPT_REQ_STRING }, + { "--timeout", 't', RTGETOPT_REQ_UINT32 }, + { "--unquoted-args", 'u', RTGETOPT_REQ_NOTHING }, + { "--ignore-orphaned-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 aCreateFlags; + com::SafeArray aWaitFlags; + com::SafeArray aArgs; + com::SafeArray aEnv; + const char * pszImage = NULL; + const char * pszArg0 = NULL; /* Argument 0 to use. pszImage will be used if not specified. */ + 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 errorSyntax(GuestCtrl::tr("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(GuestCtrl::tr("Warning: Deprecated option \"--no-profile\" specified\n")); + break; + + case kGstCtrlRunOpt_Profile: + aCreateFlags.push_back(ProcessCreateFlag_Profile); + break; + + case '0': + pszArg0 = ValueUnion.psz; + 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: + /* VINF_GETOPT_NOT_OPTION comes after all options have been specified; + * so if pszImage still is zero at this stage, we use the first non-option found + * as the image being executed. */ + if (!pszImage) + pszImage = ValueUnion.psz; + else /* Add anything else to the arguments vector. */ + aArgs.push_back(Bstr(ValueUnion.psz).raw()); + break; + + default: + return errorGetOpt(ch, &ValueUnion); + + } /* switch */ + } /* while RTGetOpt */ + + /* Must have something to execute. */ + if (!pszImage || !*pszImage) + return errorSyntax(GuestCtrl::tr("No executable specified!")); + + /* Set the arg0 argument (descending precedence): + * - If an argument 0 is explicitly specified (via "--arg0"), use this as argument 0. + * - When an image is specified explicitly (via "--exe "), use as argument 0. + * Note: This is (and ever was) the default behavior users expect, so don't change this! */ + if (pszArg0) + aArgs.push_front(Bstr(pszArg0).raw()); + else + aArgs.push_front(Bstr(pszImage).raw()); + + if (pCtx->cVerbose) /* Print the final execution parameters in verbose mode. */ + { + RTPrintf(GuestCtrl::tr("Executing:\n Image : %s\n"), pszImage); + for (size_t i = 0; i < aArgs.size(); i++) + RTPrintf(GuestCtrl::tr(" arg[%d]: %ls\n"), i, aArgs[i]); + } + /* No altering of aArgs and/or pszImage after this point! */ + + /* + * 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 hrc; + + 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(GuestCtrl::tr("Starting guest process ...\n")); + else + RTPrintf(GuestCtrl::tr("Starting guest process (within %ums)\n"), cMsTimeout); + } + ComPtr 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 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(GuestCtrl::tr("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(GuestCtrl::tr("[%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)); + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Wait result is '%s' (%d)\n"), gctlProcessWaitResultToText(waitResult), waitResult); + switch (waitResult) + { + case ProcessWaitResult_Start: /** @todo you always wait for 'start', */ + fCompletedStartCmd = fCompleted = !fRunCmd; /* Only wait for startup if the 'start' command. */ + if (!fCompleted && aWaitFlags[0] == ProcessWaitForFlag_Start) + aWaitFlags[0] = ProcessWaitForFlag_Terminate; + break; + case ProcessWaitResult_StdOut: + fReadStdOut = true; + break; + case ProcessWaitResult_StdErr: + fReadStdErr = true; + break; + case ProcessWaitResult_Terminate: + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("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; + /* Note: In case the user specified explicitly not wanting to wait for stdout / stderr, + * the configured VFS handle goes to / will be fed from the bit bucket. */ + 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(GuestCtrl::tr("Process execution aborted!\n")); + rcExit = EXITCODEEXEC_CANCELED; + } + else if (fCompletedStartCmd) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("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(GuestCtrl::tr("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(GuestCtrl::tr("Process timed out (guest side) and %s\n"), + procStatus == ProcessStatus_TimedOutAbnormally + ? GuestCtrl::tr("failed to terminate so far") : GuestCtrl::tr("was terminated")); + rcExit = EXITCODEEXEC_TIMEOUT; + } + else + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Process now is in status [%s] (unexpected)\n"), + gctlProcessStatusToText(procStatus)); + rcExit = RTEXITCODE_FAILURE; + } + } + else if (RT_FAILURE_NP(vrc)) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Process monitor loop quit with vrc=%Rrc\n"), vrc); + rcExit = RTEXITCODE_FAILURE; + } + else + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Process monitor loop timed out\n")); + rcExit = EXITCODEEXEC_TIMEOUT; + } + + } while (0); + } + catch (std::bad_alloc &) + { + hrc = 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(hrc) && !g_fGuestCtrlCanceled) + pCtx->fDetachGuestSession = true; + + /* Make sure we return failure on failure. */ + if (FAILED(hrc) && 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*/); +} + + +static DECLCALLBACK(RTEXITCODE) gctlHandleStart(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + return gctlHandleRunCommon(pCtx, argc, argv, false /*fRunCmd*/); +} + + +static RTEXITCODE gctlHandleCopy(PGCTLCMDCTX pCtx, int argc, char **argv, bool fHostToGuest) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + /* + * 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. + */ + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--follow", 'L', RTGETOPT_REQ_NOTHING }, /* Kept for backwards-compatibility (VBox < 7.0). */ + { "--dereference", 'L', RTGETOPT_REQ_NOTHING }, + { "--no-replace", 'n', RTGETOPT_REQ_NOTHING }, /* like "-n" via cp. */ + { "--recursive", 'R', RTGETOPT_REQ_NOTHING }, + { "--target-directory", 't', RTGETOPT_REQ_STRING }, + { "--update", 'u', RTGETOPT_REQ_NOTHING } /* like "-u" via cp. */ + }; + + 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; + bool fUpdate = false; /* Whether to copy the file only if it's newer than the target. */ + bool fNoReplace = false; /* Only copy the file if it does not exist yet. */ + + 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 'L': + if (!RTStrICmp(ValueUnion.pDef->pszLong, "--follow")) + RTMsgWarning("--follow is deprecated; use --dereference instead."); + fFollow = true; + break; + + case 'n': + fNoReplace = true; + break; + + case 'R': + fRecursive = true; + break; + + case 't': + pszDst = ValueUnion.psz; + fDstMustBeDir = true; + break; + + case 'u': + fUpdate = true; + break; + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + char **papszSources = RTGetOptNonOptionArrayPtr(&GetState); + size_t cSources = &argv[argc] - papszSources; + + if (!cSources) + return errorSyntax(GuestCtrl::tr("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 errorSyntax(GuestCtrl::tr("No destination specified!")); + + char szAbsDst[RTPATH_MAX]; + if (!fHostToGuest) + { + vrc = RTPathAbs(pszDst, szAbsDst, sizeof(szAbsDst)); + if (RT_SUCCESS(vrc)) + pszDst = szAbsDst; + else + return RTMsgErrorExitFailure(GuestCtrl::tr("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(GuestCtrl::tr("Copying from host to guest ...\n")); + else + RTPrintf(GuestCtrl::tr("Copying from guest to host ...\n")); + } + + HRESULT hrc = S_OK; + + com::SafeArray aSources; + com::SafeArray aFilters; /** @todo Populate those? For now we use caller-based globbing. */ + com::SafeArray aCopyFlags; + + size_t iSrc = 0; + for (; iSrc < cSources; iSrc++) + { + aSources.push_back(Bstr(papszSources[iSrc]).raw()); + aFilters.push_back(Bstr("").raw()); /* Empty for now. See @todo above. */ + + /* Compile the comma-separated list of flags. + * Certain flags are only available for specific file system objects, e.g. directories. */ + bool fIsDir = false; + if (fHostToGuest) + { + RTFSOBJINFO ObjInfo; + vrc = RTPathQueryInfo(papszSources[iSrc], &ObjInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(vrc)) + fIsDir = RTFS_IS_DIRECTORY(ObjInfo.Attr.fMode); + + if (RT_FAILURE(vrc)) + break; + } + else /* Guest to host. */ + { + ComPtr pFsObjInfo; + hrc = pCtx->pGuestSession->FsObjQueryInfo(Bstr(papszSources[iSrc]).raw(), RT_BOOL(fFollow) /* fFollowSymlinks */, + pFsObjInfo.asOutParam()); + if (SUCCEEDED(hrc)) + { + FsObjType_T enmObjType; + CHECK_ERROR(pFsObjInfo,COMGETTER(Type)(&enmObjType)); + if (SUCCEEDED(hrc)) + { + /* Take action according to source file. */ + fIsDir = enmObjType == FsObjType_Directory; + } + } + + if (FAILED(hrc)) + { + vrc = gctlPrintError(pCtx->pGuestSession, COM_IIDOF(IGuestSession)); + break; + } + } + + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Source '%s' is a %s\n"), papszSources[iSrc], fIsDir ? "directory" : "file"); + + Utf8Str strCopyFlags; + if (fRecursive && fIsDir) /* Only available for directories. Just ignore otherwise. */ + strCopyFlags += "Recursive,"; + if (fFollow) + strCopyFlags += "FollowLinks,"; + if (fUpdate) /* Only copy source files which are newer than the destination file. */ + strCopyFlags += "Update,"; + if (fNoReplace) /* Do not overwrite files. */ + strCopyFlags += "NoReplace,"; + else if (!fNoReplace && fIsDir) + strCopyFlags += "CopyIntoExisting,"; /* Only copy into existing directories if "--no-replace" isn't specified. */ + aCopyFlags.push_back(Bstr(strCopyFlags).raw()); + } + + if (RT_FAILURE(vrc)) + return RTMsgErrorExitFailure(GuestCtrl::tr("Error looking file system information for source '%s', vrc=%Rrc"), + papszSources[iSrc], vrc); + + ComPtr pProgress; + if (fHostToGuest) + { + hrc = pCtx->pGuestSession->CopyToGuest(ComSafeArrayAsInParam(aSources), + ComSafeArrayAsInParam(aFilters), ComSafeArrayAsInParam(aCopyFlags), + Bstr(pszDst).raw(), pProgress.asOutParam()); + } + else /* Guest to host. */ + { + hrc = pCtx->pGuestSession->CopyFromGuest(ComSafeArrayAsInParam(aSources), + ComSafeArrayAsInParam(aFilters), ComSafeArrayAsInParam(aCopyFlags), + Bstr(pszDst).raw(), pProgress.asOutParam()); + } + + if (FAILED(hrc)) + { + vrc = gctlPrintError(pCtx->pGuestSession, COM_IIDOF(IGuestSession)); + } + else if (pProgress.isNotNull()) + { + if (pCtx->cVerbose) + hrc = showProgress(pProgress); + else + hrc = pProgress->WaitForCompletion(-1 /* No timeout */); + if (SUCCEEDED(hrc)) + CHECK_PROGRESS_ERROR(pProgress, (GuestCtrl::tr("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 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(GuestCtrl::tr("Creating %RU32 directories...\n", "", argc - GetState.iNext + 1), + argc - GetState.iNext + 1); + } + if (g_fGuestCtrlCanceled) + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("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(GuestCtrl::tr("Creating directory \"%s\" ...\n"), ValueUnion.psz); + try + { + HRESULT hrc; + CHECK_ERROR(pCtx->pGuestSession, DirectoryCreate(Bstr(ValueUnion.psz).raw(), + fDirMode, ComSafeArrayAsInParam(aDirCreateFlags))); + if (FAILED(hrc)) + rcExit = RTEXITCODE_FAILURE; + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("Out of memory\n")); + } + break; + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + if (!cDirsCreated) + return errorSyntax(GuestCtrl::tr("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) + { + if (fRecursive) + RTPrintf(GuestCtrl::tr("Removing %RU32 directory tree(s)...\n", "", argc - GetState.iNext + 1), + argc - GetState.iNext + 1); + else + RTPrintf(GuestCtrl::tr("Removing %RU32 directorie(s)...\n", "", argc - GetState.iNext + 1), + argc - GetState.iNext + 1); + } + } + if (g_fGuestCtrlCanceled) + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("rmdir was interrupted by Ctrl-C (%u left)\n"), + argc - GetState.iNext + 1); + + cDirRemoved++; + HRESULT hrc; + if (!fRecursive) + { + /* + * Remove exactly one directory. + */ + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Removing directory \"%s\" ...\n"), ValueUnion.psz); + try + { + CHECK_ERROR(pCtx->pGuestSession, DirectoryRemove(Bstr(ValueUnion.psz).raw())); + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("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(GuestCtrl::tr("Recursively removing directory \"%s\" ...\n"), ValueUnion.psz); + try + { + /** @todo Make flags configurable. */ + com::SafeArray aRemRecFlags; + aRemRecFlags.push_back(DirectoryRemoveRecFlag_ContentAndDir); + + ComPtr ptrProgress; + CHECK_ERROR(pCtx->pGuestSession, DirectoryRemoveRecursive(Bstr(ValueUnion.psz).raw(), + ComSafeArrayAsInParam(aRemRecFlags), + ptrProgress.asOutParam())); + if (SUCCEEDED(hrc)) + { + if (pCtx->cVerbose) + hrc = showProgress(ptrProgress); + else + hrc = ptrProgress->WaitForCompletion(-1 /* indefinitely */); + if (SUCCEEDED(hrc)) + CHECK_PROGRESS_ERROR(ptrProgress, (GuestCtrl::tr("Directory deletion failed"))); + ptrProgress.setNull(); + } + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("Out of memory during recursive rmdir\n")); + } + } + + /* + * This command returns immediately on failure since it's destructive in nature. + */ + if (FAILED(hrc)) + return RTEXITCODE_FAILURE; + break; + } + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + if (!cDirRemoved) + return errorSyntax(GuestCtrl::tr("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(GuestCtrl::tr("Removing %RU32 file(s)...\n", "", argc - GetState.iNext + 1), + argc - GetState.iNext + 1); + } + if (g_fGuestCtrlCanceled) + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("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(GuestCtrl::tr("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 hrc; + CHECK_ERROR(pCtx->pGuestSession, FsObjRemove(Bstr(ValueUnion.psz).raw())); + if (FAILED(hrc) && !fForce) + return RTEXITCODE_FAILURE; + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("Out of memory\n")); + } + break; + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + if (!cFilesDeleted && !fForce) + return errorSyntax(GuestCtrl::tr("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 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 errorGetOpt(ch, &ValueUnion); + } + } + } + catch (std::bad_alloc &) + { + vrc = VERR_NO_MEMORY; + } + + if (RT_FAILURE(vrc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("Failed to initialize, vrc=%Rrc\n"), vrc); + + size_t cSources = vecSources.size(); + if (!cSources) + return errorSyntax(GuestCtrl::tr("No source(s) to move specified!")); + if (cSources < 2) + return errorSyntax(GuestCtrl::tr("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 hrc = S_OK; + + /* Destination must be a directory when specifying multiple sources. */ + if (cSources > 1) + { + ComPtr pFsObjInfo; + hrc = pCtx->pGuestSession->FsObjQueryInfo(Bstr(pszDst).raw(), FALSE /*followSymlinks*/, pFsObjInfo.asOutParam()); + if (FAILED(hrc)) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("Destination does not exist\n")); + } + else + { + FsObjType_T enmObjType = FsObjType_Unknown; /* Shut up MSC */ + hrc = pFsObjInfo->COMGETTER(Type)(&enmObjType); + if (SUCCEEDED(hrc)) + { + if (enmObjType != FsObjType_Directory) + return RTMsgErrorExit(RTEXITCODE_FAILURE, + GuestCtrl::tr("Destination must be a directory when specifying multiple sources\n")); + } + else + return RTMsgErrorExit(RTEXITCODE_FAILURE, + GuestCtrl::tr("Unable to determine destination type: %Rhrc\n"), + hrc); + } + } + + /* + * Rename (move) the entries. + */ + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Renaming %RU32 %s ...\n"), cSources, + cSources > 1 ? GuestCtrl::tr("sources", "", cSources) : GuestCtrl::tr("source")); + + std::vector< Utf8Str >::iterator it = vecSources.begin(); + while ( it != vecSources.end() + && !g_fGuestCtrlCanceled) + { + Utf8Str strSrcCur = (*it); + + ComPtr pFsObjInfo; + FsObjType_T enmObjType = FsObjType_Unknown; /* Shut up MSC */ + hrc = pCtx->pGuestSession->FsObjQueryInfo(Bstr(strSrcCur).raw(), FALSE /*followSymlinks*/, pFsObjInfo.asOutParam()); + if (SUCCEEDED(hrc)) + hrc = pFsObjInfo->COMGETTER(Type)(&enmObjType); + if (FAILED(hrc)) + { + RTPrintf(GuestCtrl::tr("Cannot stat \"%s\": No such file or directory\n"), strSrcCur.c_str()); + ++it; + continue; /* Skip. */ + } + + char *pszDstCur = NULL; + + if (cSources > 1) + { + pszDstCur = RTPathJoinA(pszDst, RTPathFilename(strSrcCur.c_str())); + } + else + pszDstCur = RTStrDup(pszDst); + + AssertPtrBreakStmt(pszDstCur, VERR_NO_MEMORY); + + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Renaming %s \"%s\" to \"%s\" ...\n"), + enmObjType == FsObjType_Directory ? GuestCtrl::tr("directory", "object") : GuestCtrl::tr("file","object"), + strSrcCur.c_str(), pszDstCur); + + if (!fDryrun) + { + CHECK_ERROR(pCtx->pGuestSession, FsObjRename(Bstr(strSrcCur).raw(), + Bstr(pszDstCur).raw(), + ComSafeArrayAsInParam(aRenameFlags))); + /* Keep going with next item in case of errors. */ + } + + RTStrFree(pszDstCur); + + ++it; + } + + if ( (it != vecSources.end()) + && pCtx->cVerbose) + { + RTPrintf(GuestCtrl::tr("Warning: Not all sources were renamed\n")); + } + + return FAILED(hrc) ? 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 errorSyntax(GuestCtrl::tr("More than one template specified!\n")); + break; + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + if (strTemplate.isEmpty()) + return errorSyntax(GuestCtrl::tr("No template specified!")); + + if (!fDirectory) + return errorSyntax(GuestCtrl::tr("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(GuestCtrl::tr("Creating temporary directory from template '%s' in directory '%s' ...\n"), + strTemplate.c_str(), strTempDir.c_str()); + else if (fDirectory) + RTPrintf(GuestCtrl::tr("Creating temporary directory from template '%s' in default temporary directory ...\n"), + strTemplate.c_str()); + else if (!fDirectory && !strTempDir.isEmpty()) + RTPrintf(GuestCtrl::tr("Creating temporary file from template '%s' in directory '%s' ...\n"), + strTemplate.c_str(), strTempDir.c_str()); + else if (!fDirectory) + RTPrintf(GuestCtrl::tr("Creating temporary file from template '%s' in default temporary directory ...\n"), + strTemplate.c_str()); + } + + HRESULT hrc = S_OK; + if (fDirectory) + { + Bstr bstrDirectory; + CHECK_ERROR(pCtx->pGuestSession, DirectoryCreateTemp(Bstr(strTemplate).raw(), + fMode, Bstr(strTempDir).raw(), + fSecure, + bstrDirectory.asOutParam())); + if (SUCCEEDED(hrc)) + RTPrintf(GuestCtrl::tr("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). */ + hrc = E_FAIL; + } + + return FAILED(hrc) ? 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 errorSyntax(GuestCtrl::tr("Command \"%s\" not implemented yet!"), ValueUnion.psz); + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + if (ch != VINF_GETOPT_NOT_OPTION) + return errorSyntax(GuestCtrl::tr("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(GuestCtrl::tr("Checking for element \"%s\" ...\n"), ValueUnion.psz); + + ComPtr 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(GuestCtrl::tr("Failed to stat '%s': No such file\n"), ValueUnion.psz); + rcExit = RTEXITCODE_FAILURE; + } + else + { + RTPrintf(GuestCtrl::tr(" 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(GuestCtrl::tr(" Size: %-17RU64 Alloc: %-19RU64 Type: %s\n"), + cbObject, cbAllocated, gctlFsObjTypeToName(enmType)); + RTPrintf(GuestCtrl::tr("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(GuestCtrl::tr(" Mode: %-16s Attrib: %-17s Dev ID: %#RX32\n"), pszMode, pszAttribs, uDeviceNo); + else + RTPrintf(GuestCtrl::tr(" Mode: %-16s Attrib: %s\n"), pszMode, pszAttribs); + + RTPrintf(GuestCtrl::tr(" Owner: %4d/%-12ls Group: %4d/%ls\n"), uid, bstrUsername.raw(), gid, bstrGroupName.raw()); + + RTTIMESPEC TimeSpec; + char szTmp[RTTIME_STR_LEN]; + RTPrintf(GuestCtrl::tr(" Birth: %s\n"), RTTimeSpecToString(RTTimeSpecSetNano(&TimeSpec, nsBirthTime), + szTmp, sizeof(szTmp))); + RTPrintf(GuestCtrl::tr("Change: %s\n"), RTTimeSpecToString(RTTimeSpecSetNano(&TimeSpec, nsChangeTime), + szTmp, sizeof(szTmp))); + RTPrintf(GuestCtrl::tr("Modify: %s\n"), RTTimeSpecToString(RTTimeSpecSetNano(&TimeSpec, nsModificationTime), + szTmp, sizeof(szTmp))); + RTPrintf(GuestCtrl::tr("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; +} + +/** + * Waits for a Guest Additions run level being reached. + * + * @returns VBox status code. + * Returns VERR_CANCELLED if waiting for cancelled due to signal handling, e.g. when CTRL+C or some sort was pressed. + * @param pCtx The guest control command context. + * @param enmRunLevel Run level to wait for. + * @param cMsTimeout Timeout (in ms) for waiting. + */ +static int gctlWaitForRunLevel(PGCTLCMDCTX pCtx, AdditionsRunLevelType_T enmRunLevel, RTMSINTERVAL cMsTimeout) +{ + int vrc = VINF_SUCCESS; /* Shut up MSVC. */ + + try + { + HRESULT hrc = S_OK; + /** Whether we need to actually wait for the run level or if we already reached it. */ + bool fWait = false; + + /* Install an event handler first to catch any runlevel changes. */ + ComObjPtr pGuestListener; + do + { + /* Listener creation. */ + pGuestListener.createObject(); + pGuestListener->init(new GuestAdditionsRunlevelListener(enmRunLevel)); + + /* Register for IGuest events. */ + ComPtr es; + CHECK_ERROR_BREAK(pCtx->pGuest, COMGETTER(EventSource)(es.asOutParam())); + com::SafeArray eventTypes; + eventTypes.push_back(VBoxEventType_OnGuestAdditionsStatusChanged); + CHECK_ERROR_BREAK(es, RegisterListener(pGuestListener, ComSafeArrayAsInParam(eventTypes), + true /* Active listener */)); + + AdditionsRunLevelType_T enmRunLevelCur = AdditionsRunLevelType_None; + CHECK_ERROR_BREAK(pCtx->pGuest, COMGETTER(AdditionsRunLevel)(&enmRunLevelCur)); + fWait = enmRunLevelCur != enmRunLevel; + + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Current run level is %RU32\n"), enmRunLevelCur); + + } while (0); + + if (fWait) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Waiting for run level %RU32 ...\n"), enmRunLevel); + + RTMSINTERVAL tsStart = RTTimeMilliTS(); + while (RTTimeMilliTS() - tsStart < cMsTimeout) + { + /* Wait for the global signal semaphore getting signalled. */ + vrc = RTSemEventWait(g_SemEventGuestCtrlCanceled, 100 /* ms */); + if (RT_FAILURE(vrc)) + { + if (vrc == VERR_TIMEOUT) + continue; + else + { + RTPrintf(GuestCtrl::tr("Waiting failed with %Rrc\n"), vrc); + break; + } + } + else if (pCtx->cVerbose) + { + RTPrintf(GuestCtrl::tr("Run level %RU32 reached\n"), enmRunLevel); + break; + } + + NativeEventQueue::getMainEventQueue()->processEventQueue(0); + } + + if ( vrc == VERR_TIMEOUT + && pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Run level %RU32 not reached within time\n"), enmRunLevel); + } + + if (!pGuestListener.isNull()) + { + /* Guest callback unregistration. */ + ComPtr pES; + CHECK_ERROR(pCtx->pGuest, COMGETTER(EventSource)(pES.asOutParam())); + if (!pES.isNull()) + CHECK_ERROR(pES, UnregisterListener(pGuestListener)); + pGuestListener.setNull(); + } + + if (g_fGuestCtrlCanceled) + vrc = VERR_CANCELLED; + } + catch (std::bad_alloc &) + { + vrc = VERR_NO_MEMORY; + } + + return vrc; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleUpdateAdditions(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + /** Timeout to wait for the whole updating procedure to complete. */ + uint32_t cMsTimeout = RT_INDEFINITE_WAIT; + /** Source path to .ISO Guest Additions file to use. */ + Utf8Str strSource; + com::SafeArray aArgs; + /** Whether to reboot the guest automatically when the update process has finished successfully. */ + bool fRebootOnFinish = false; + /** Whether to only wait for getting the update process started instead of waiting until it finishes. */ + bool fWaitStartOnly = false; + /** Whether to wait for the VM being ready to start the update. Needs Guest Additions facility reporting. */ + bool fWaitReady = false; + /** Whether to verify if the Guest Additions were successfully updated on the guest. */ + bool fVerify = false; + + /* + * Parse arguments. + */ + enum KGSTCTRLUPDATEADDITIONSOPT + { + KGSTCTRLUPDATEADDITIONSOPT_REBOOT = 1000, + KGSTCTRLUPDATEADDITIONSOPT_SOURCE, + KGSTCTRLUPDATEADDITIONSOPT_TIMEOUT, + KGSTCTRLUPDATEADDITIONSOPT_VERIFY, + KGSTCTRLUPDATEADDITIONSOPT_WAITREADY, + KGSTCTRLUPDATEADDITIONSOPT_WAITSTART + }; + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--reboot", KGSTCTRLUPDATEADDITIONSOPT_REBOOT, RTGETOPT_REQ_NOTHING }, + { "--source", KGSTCTRLUPDATEADDITIONSOPT_SOURCE, RTGETOPT_REQ_STRING }, + { "--timeout", KGSTCTRLUPDATEADDITIONSOPT_TIMEOUT, RTGETOPT_REQ_UINT32 }, + { "--verify", KGSTCTRLUPDATEADDITIONSOPT_VERIFY, RTGETOPT_REQ_NOTHING }, + { "--wait-ready", KGSTCTRLUPDATEADDITIONSOPT_WAITREADY, RTGETOPT_REQ_NOTHING }, + { "--wait-start", KGSTCTRLUPDATEADDITIONSOPT_WAITSTART, 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 KGSTCTRLUPDATEADDITIONSOPT_REBOOT: + fRebootOnFinish = true; + break; + + case KGSTCTRLUPDATEADDITIONSOPT_SOURCE: + vrc = RTPathAbsCxx(strSource, ValueUnion.psz); + if (RT_FAILURE(vrc)) + return RTMsgErrorExitFailure(GuestCtrl::tr("RTPathAbsCxx failed on '%s': %Rrc"), ValueUnion.psz, vrc); + break; + + case KGSTCTRLUPDATEADDITIONSOPT_WAITSTART: + fWaitStartOnly = true; + break; + + case KGSTCTRLUPDATEADDITIONSOPT_WAITREADY: + fWaitReady = true; + break; + + case KGSTCTRLUPDATEADDITIONSOPT_VERIFY: + fVerify = true; + fRebootOnFinish = true; /* Verification needs a mandatory reboot after successful update. */ + 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 errorGetOpt(ch, &ValueUnion); + } + } + + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Updating Guest Additions ...\n")); + + HRESULT hrc = S_OK; + while (strSource.isEmpty()) + { + ComPtr 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(GuestCtrl::tr("No Guest Additions source found or specified, aborting\n")); + vrc = VERR_FILE_NOT_FOUND; + } + else if (!RTFileExists(strSource.c_str())) + { + RTMsgError(GuestCtrl::tr("Source \"%s\" does not exist!\n"), strSource.c_str()); + vrc = VERR_FILE_NOT_FOUND; + } + + + +#if 0 + ComPtr guest; + HRESULT hrc = pConsole->COMGETTER(Guest)(guest.asOutParam()); + if (SUCCEEDED(hrc) && !guest.isNull()) + { + SHOW_STRING_PROP_NOT_EMPTY(guest, OSTypeId, "GuestOSType", GuestCtrl::tr("OS type:")); + + AdditionsRunLevelType_T guestRunLevel; /** @todo Add a runlevel-to-string (e.g. 0 = "None") method? */ + hrc = guest->COMGETTER(AdditionsRunLevel)(&guestRunLevel); + if (SUCCEEDED(hrc)) + SHOW_ULONG_VALUE("GuestAdditionsRunLevel", GuestCtrl::tr("Additions run level:"), (ULONG)guestRunLevel, ""); + + Bstr guestString; + hrc = guest->COMGETTER(AdditionsVersion)(guestString.asOutParam()); + if ( SUCCEEDED(hrc) + && !guestString.isEmpty()) + { + ULONG uRevision; + hrc = guest->COMGETTER(AdditionsRevision)(&uRevision); + if (FAILED(hrc)) + uRevision = 0; + RTStrPrintf(szValue, sizeof(szValue), "%ls r%u", guestString.raw(), uRevision); + SHOW_UTF8_STRING("GuestAdditionsVersion", GuestCtrl::tr("Additions version:"), szValue); + } + } +#endif + + if (RT_SUCCESS(vrc)) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Using source: %s\n"), strSource.c_str()); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + if (fWaitReady) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Waiting for current Guest Additions inside VM getting ready for updating ...\n")); + + const uint64_t uTsStart = RTTimeMilliTS(); + vrc = gctlWaitForRunLevel(pCtx, AdditionsRunLevelType_Userland, cMsTimeout); + if (RT_SUCCESS(vrc)) + cMsTimeout = cMsTimeout != RT_INDEFINITE_WAIT ? cMsTimeout - (RTTimeMilliTS() - uTsStart) : cMsTimeout; + } + + if (RT_SUCCESS(vrc)) + { + /* Get current Guest Additions version / revision. */ + Bstr strGstVerCur; + ULONG uGstRevCur = 0; + hrc = pCtx->pGuest->COMGETTER(AdditionsVersion)(strGstVerCur.asOutParam()); + if ( SUCCEEDED(hrc) + && !strGstVerCur.isEmpty()) + { + hrc = pCtx->pGuest->COMGETTER(AdditionsRevision)(&uGstRevCur); + if (SUCCEEDED(hrc)) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Guest Additions %lsr%RU64 currently installed, waiting for Guest Additions installer to start ...\n"), + strGstVerCur.raw(), uGstRevCur); + } + } + + com::SafeArray aUpdateFlags; + if (fWaitStartOnly) + aUpdateFlags.push_back(AdditionsUpdateFlag_WaitForUpdateStartOnly); + + ComPtr pProgress; + CHECK_ERROR(pCtx->pGuest, UpdateGuestAdditions(Bstr(strSource).raw(), + ComSafeArrayAsInParam(aArgs), + ComSafeArrayAsInParam(aUpdateFlags), + pProgress.asOutParam())); + if (FAILED(hrc)) + vrc = gctlPrintError(pCtx->pGuest, COM_IIDOF(IGuest)); + else + { + if (pCtx->cVerbose) + hrc = showProgress(pProgress); + else + hrc = pProgress->WaitForCompletion((int32_t)cMsTimeout); + + if (SUCCEEDED(hrc)) + CHECK_PROGRESS_ERROR(pProgress, (GuestCtrl::tr("Guest Additions update failed"))); + vrc = gctlPrintProgressError(pProgress); + if (RT_SUCCESS(vrc)) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Guest Additions update successful.\n")); + + if (fRebootOnFinish) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Rebooting guest ...\n")); + com::SafeArray aShutdownFlags; + aShutdownFlags.push_back(GuestShutdownFlag_Reboot); + CHECK_ERROR(pCtx->pGuest, Shutdown(ComSafeArrayAsInParam(aShutdownFlags))); + if (FAILED(hrc)) + { + if (hrc == VBOX_E_NOT_SUPPORTED) + { + RTPrintf(GuestCtrl::tr("Current installed Guest Additions don't support automatic rebooting. " + "Please reboot manually.\n")); + vrc = VERR_NOT_SUPPORTED; + } + else + vrc = gctlPrintError(pCtx->pGuest, COM_IIDOF(IGuest)); + } + else + { + if (fWaitReady) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Waiting for new Guest Additions inside VM getting ready ...\n")); + + vrc = gctlWaitForRunLevel(pCtx, AdditionsRunLevelType_Userland, cMsTimeout); + if (RT_SUCCESS(vrc)) + { + if (fVerify) + { + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Verifying Guest Additions update ...\n")); + + /* Get new Guest Additions version / revision. */ + Bstr strGstVerNew; + ULONG uGstRevNew = 0; + hrc = pCtx->pGuest->COMGETTER(AdditionsVersion)(strGstVerNew.asOutParam()); + if ( SUCCEEDED(hrc) + && !strGstVerNew.isEmpty()) + { + hrc = pCtx->pGuest->COMGETTER(AdditionsRevision)(&uGstRevNew); + if (FAILED(hrc)) + uGstRevNew = 0; + } + + /** @todo Do more verification here. */ + vrc = uGstRevNew > uGstRevCur ? VINF_SUCCESS : VERR_NO_CHANGE; + + if (pCtx->cVerbose) + { + RTPrintf(GuestCtrl::tr("Old Guest Additions: %ls%RU64\n"), strGstVerCur.raw(), + uGstRevCur); + RTPrintf(GuestCtrl::tr("New Guest Additions: %ls%RU64\n"), strGstVerNew.raw(), + uGstRevNew); + + if (RT_FAILURE(vrc)) + { + RTPrintf(GuestCtrl::tr("\nError updating Guest Additions, please check guest installer log\n")); + } + else + { + if (uGstRevNew < uGstRevCur) + RTPrintf(GuestCtrl::tr("\nWARNING: Guest Additions were downgraded\n")); + } + } + } + } + } + else if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("The guest needs to be restarted in order to make use of the updated Guest Additions.\n")); + } + } + } + } + } + } + + return RT_SUCCESS(vrc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; +} + +/** + * Returns a Guest Additions run level from a string. + * + * @returns Run level if found, or AdditionsRunLevelType_None if not found / invalid. + * @param pcszStr String to return run level for. + */ +static AdditionsRunLevelType_T gctlGetRunLevelFromStr(const char *pcszStr) +{ + AssertPtrReturn(pcszStr, AdditionsRunLevelType_None); + + if (RTStrICmp(pcszStr, "system") == 0) return AdditionsRunLevelType_System; + else if (RTStrICmp(pcszStr, "userland") == 0) return AdditionsRunLevelType_Userland; + else if (RTStrICmp(pcszStr, "desktop") == 0) return AdditionsRunLevelType_Desktop; + + return AdditionsRunLevelType_None; +} + +static DECLCALLBACK(RTEXITCODE) gctlHandleWaitRunLevel(PGCTLCMDCTX pCtx, int argc, char **argv) +{ + AssertPtrReturn(pCtx, RTEXITCODE_FAILURE); + + /** Timeout to wait for run level being reached. + * By default we wait until it's reached. */ + uint32_t cMsTimeout = RT_INDEFINITE_WAIT; + + /* + * Parse arguments. + */ + enum KGSTCTRLWAITRUNLEVELOPT + { + KGSTCTRLWAITRUNLEVELOPT_TIMEOUT = 1000 + }; + + static const RTGETOPTDEF s_aOptions[] = + { + GCTLCMD_COMMON_OPTION_DEFS() + { "--timeout", KGSTCTRLWAITRUNLEVELOPT_TIMEOUT, RTGETOPT_REQ_UINT32 } + }; + + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + + AdditionsRunLevelType_T enmRunLevel = AdditionsRunLevelType_None; + + int vrc = VINF_SUCCESS; + while ( (ch = RTGetOpt(&GetState, &ValueUnion)) + && RT_SUCCESS(vrc)) + { + switch (ch) + { + GCTLCMD_COMMON_OPTION_CASES(pCtx, ch, &ValueUnion); + + case KGSTCTRLWAITRUNLEVELOPT_TIMEOUT: + cMsTimeout = ValueUnion.u32; + break; + + case VINF_GETOPT_NOT_OPTION: + { + enmRunLevel = gctlGetRunLevelFromStr(ValueUnion.psz); + if (enmRunLevel == AdditionsRunLevelType_None) + return errorSyntax(GuestCtrl::tr("Invalid run level specified. Valid values are: system, userland, desktop")); + break; + } + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + if (enmRunLevel == AdditionsRunLevelType_None) + return errorSyntax(GuestCtrl::tr("Missing run level to wait for")); + + vrc = gctlWaitForRunLevel(pCtx, enmRunLevel, cMsTimeout); + + 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 errorSyntax(GuestCtrl::tr("Unknown list: '%s'"), ValueUnion.psz); + fSeenListArg = true; + break; + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + if (!fSeenListArg) + return errorSyntax(GuestCtrl::tr("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 hrc; + size_t cTotalProcs = 0; + size_t cTotalFiles = 0; + + SafeIfaceArray collSessions; + CHECK_ERROR(pCtx->pGuest, COMGETTER(Sessions)(ComSafeArrayAsOutParam(collSessions))); + if (SUCCEEDED(hrc)) + { + size_t const cSessions = collSessions.size(); + if (cSessions) + { + RTPrintf(GuestCtrl::tr("Active guest sessions:\n")); + + /** @todo Make this output a bit prettier. No time now. */ + + for (size_t i = 0; i < cSessions; i++) + { + ComPtr 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(GuestCtrl::tr("\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 collProcesses; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(Processes)(ComSafeArrayAsOutParam(collProcesses))); + for (size_t a = 0; a < collProcesses.size(); a++) + { + ComPtr 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(GuestCtrl::tr("\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 collFiles; + CHECK_ERROR_BREAK(pCurSession, COMGETTER(Files)(ComSafeArrayAsOutParam(collFiles))); + for (size_t a = 0; a < collFiles.size(); a++) + { + ComPtr 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(GuestCtrl::tr("\n\t\tFile #%-03zu ID=%-6RU32 Status=[%s] Name=%ls"), + a, idFile, gctlFileStatusToText(fileStatus), strName.raw()); + } while (0); + } + } + + cTotalFiles += collFiles.size(); + } + } + } + + RTPrintf(GuestCtrl::tr("\n\nTotal guest sessions: %zu\n"), collSessions.size()); + if (fListAll || fListProcesses) + RTPrintf(GuestCtrl::tr("Total guest processes: %zu\n"), cTotalProcs); + if (fListAll || fListFiles) + RTPrintf(GuestCtrl::tr("Total guest files: %zu\n"), cTotalFiles); + } + else + RTPrintf(GuestCtrl::tr("No active guest sessions found\n")); + } + + if (FAILED(hrc)) /** @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; + int vrc = RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, RTGETOPTINIT_FLAGS_OPTS_FIRST); + AssertRCReturn(vrc, RTEXITCODE_FAILURE); + + 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; + vrc = RTStrToUInt32Ex(ValueUnion.psz, NULL, 0, &uPid); + if ( RT_SUCCESS(vrc) + && vrc != VWRN_TRAILING_CHARS + && vrc != VWRN_NUMBER_TOO_BIG + && vrc != VWRN_NEGATIVE_UNSIGNED) + { + if (uPid != 0) + { + try + { + vecPID.push_back(uPid); + } + catch (std::bad_alloc &) + { + return RTMsgErrorExit(RTEXITCODE_FAILURE, GuestCtrl::tr("Out of memory")); + } + } + else + return errorSyntax(GuestCtrl::tr("Invalid PID value: 0")); + } + else + return errorSyntax(GuestCtrl::tr("Error parsing PID value: %Rrc"), vrc); + break; + } + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + if (vecPID.empty()) + return errorSyntax(GuestCtrl::tr("At least one PID must be specified to kill!")); + + if ( strSessionName.isEmpty() + && idSession == UINT32_MAX) + return errorSyntax(GuestCtrl::tr("No session ID specified!")); + + if ( strSessionName.isNotEmpty() + && idSession != UINT32_MAX) + return errorSyntax(GuestCtrl::tr("Either session ID or name (pattern) must be specified")); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + HRESULT hrc = S_OK; + + ComPtr pSession; + ComPtr pProcess; + do + { + uint32_t uProcsTerminated = 0; + + SafeIfaceArray 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 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(GuestCtrl::tr("Terminating process (PID %RU32) (session ID %RU32) ...\n"), + uPID, uID); + CHECK_ERROR_BREAK(pProcess, Terminate()); + uProcsTerminated++; + } + else + { + if (idSession != UINT32_MAX) + RTPrintf(GuestCtrl::tr("No matching process(es) for session ID %RU32 found\n"), + idSession); + } + + pProcess.setNull(); + } + + pSession.setNull(); + } + } + + if (!cSessionsHandled) + RTPrintf(GuestCtrl::tr("No matching session(s) found\n")); + + if (uProcsTerminated) + RTPrintf(GuestCtrl::tr("%RU32 process(es) terminated\n", "", uProcsTerminated), uProcsTerminated); + + } while (0); + + pProcess.setNull(); + pSession.setNull(); + + return SUCCEEDED(hrc) ? 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 errorGetOpt(ch, &ValueUnion); + } + } + + if ( strSessionName.isEmpty() + && idSession == UINT32_MAX) + return errorSyntax(GuestCtrl::tr("No session ID specified!")); + + if ( !strSessionName.isEmpty() + && idSession != UINT32_MAX) + return errorSyntax(GuestCtrl::tr("Either session ID or name (pattern) must be specified")); + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + HRESULT hrc = S_OK; + + do + { + size_t cSessionsHandled = 0; + + SafeIfaceArray collSessions; + CHECK_ERROR_BREAK(pCtx->pGuest, COMGETTER(Sessions)(ComSafeArrayAsOutParam(collSessions))); + size_t cSessions = collSessions.size(); + + for (size_t i = 0; i < cSessions; i++) + { + ComPtr 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(GuestCtrl::tr("Closing guest session ID=#%RU32 \"%s\" ...\n"), + uID, strNameUtf8.c_str()); + CHECK_ERROR_BREAK(pSession, Close()); + if (pCtx->cVerbose) + RTPrintf(GuestCtrl::tr("Guest session successfully closed\n")); + + pSession.setNull(); + } + } + + if (!cSessionsHandled) + { + RTPrintf(GuestCtrl::tr("No guest session(s) found\n")); + hrc = E_ABORT; /* To set exit code accordingly. */ + } + + } while (0); + + return SUCCEEDED(hrc) ? 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() + { "--timeout", 't', RTGETOPT_REQ_UINT32 } + }; + + uint32_t cMsTimeout = RT_INDEFINITE_WAIT; + + 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 't': /* Timeout */ + cMsTimeout = ValueUnion.u32; + break; + + case VINF_GETOPT_NOT_OPTION: + default: + return errorGetOpt(ch, &ValueUnion); + } + } + + /** @todo Specify categories to watch for. */ + + RTEXITCODE rcExit = gctlCtxPostOptionParsingInit(pCtx); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + HRESULT hrc; + + try + { + ComObjPtr pGuestListener; + do + { + /* Listener creation. */ + pGuestListener.createObject(); + pGuestListener->init(new GuestEventListener()); + + /* Register for IGuest events. */ + ComPtr es; + CHECK_ERROR_BREAK(pCtx->pGuest, COMGETTER(EventSource)(es.asOutParam())); + com::SafeArray 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(GuestCtrl::tr("Waiting for events ...\n")); + + RTMSINTERVAL tsStart = RTTimeMilliTS(); + while (RTTimeMilliTS() - tsStart < cMsTimeout) + { + /* Wait for the global signal semaphore getting signalled. */ + int vrc = RTSemEventWait(g_SemEventGuestCtrlCanceled, 100 /* ms */); + if (RT_FAILURE(vrc)) + { + if (vrc != VERR_TIMEOUT) + { + RTPrintf(GuestCtrl::tr("Waiting failed with %Rrc\n"), vrc); + break; + } + } + else + break; + + /* We need to process the event queue, otherwise our registered listeners won't get any events. */ + NativeEventQueue::getMainEventQueue()->processEventQueue(0); + } + + if (!pGuestListener.isNull()) + { + /* Guest callback unregistration. */ + ComPtr pES; + CHECK_ERROR(pCtx->pGuest, COMGETTER(EventSource)(pES.asOutParam())); + if (!pES.isNull()) + CHECK_ERROR(pES, UnregisterListener(pGuestListener)); + pGuestListener.setNull(); + } + } + catch (std::bad_alloc &) + { + hrc = E_OUTOFMEMORY; + } + + return SUCCEEDED(hrc) ? 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); + + /* + * Command definitions. + */ + static const GCTLCMDDEF s_aCmdDefs[] = + { + { "run", gctlHandleRun, HELP_SCOPE_GUESTCONTROL_RUN, 0 }, + { "start", gctlHandleStart, HELP_SCOPE_GUESTCONTROL_START, 0 }, + { "copyfrom", gctlHandleCopyFrom, HELP_SCOPE_GUESTCONTROL_COPYFROM, 0 }, + { "copyto", gctlHandleCopyTo, HELP_SCOPE_GUESTCONTROL_COPYTO, 0 }, + + { "mkdir", gctrlHandleMkDir, HELP_SCOPE_GUESTCONTROL_MKDIR, 0 }, + { "md", gctrlHandleMkDir, HELP_SCOPE_GUESTCONTROL_MKDIR, 0 }, + { "createdirectory", gctrlHandleMkDir, HELP_SCOPE_GUESTCONTROL_MKDIR, 0 }, + { "createdir", gctrlHandleMkDir, HELP_SCOPE_GUESTCONTROL_MKDIR, 0 }, + + { "rmdir", gctlHandleRmDir, HELP_SCOPE_GUESTCONTROL_RMDIR, 0 }, + { "removedir", gctlHandleRmDir, HELP_SCOPE_GUESTCONTROL_RMDIR, 0 }, + { "removedirectory", gctlHandleRmDir, HELP_SCOPE_GUESTCONTROL_RMDIR, 0 }, + + { "rm", gctlHandleRm, HELP_SCOPE_GUESTCONTROL_RM, 0 }, + { "removefile", gctlHandleRm, HELP_SCOPE_GUESTCONTROL_RM, 0 }, + { "erase", gctlHandleRm, HELP_SCOPE_GUESTCONTROL_RM, 0 }, + { "del", gctlHandleRm, HELP_SCOPE_GUESTCONTROL_RM, 0 }, + { "delete", gctlHandleRm, HELP_SCOPE_GUESTCONTROL_RM, 0 }, + + { "mv", gctlHandleMv, HELP_SCOPE_GUESTCONTROL_MV, 0 }, + { "move", gctlHandleMv, HELP_SCOPE_GUESTCONTROL_MV, 0 }, + { "ren", gctlHandleMv, HELP_SCOPE_GUESTCONTROL_MV, 0 }, + { "rename", gctlHandleMv, HELP_SCOPE_GUESTCONTROL_MV, 0 }, + + { "mktemp", gctlHandleMkTemp, HELP_SCOPE_GUESTCONTROL_MKTEMP, 0 }, + { "createtemp", gctlHandleMkTemp, HELP_SCOPE_GUESTCONTROL_MKTEMP, 0 }, + { "createtemporary", gctlHandleMkTemp, HELP_SCOPE_GUESTCONTROL_MKTEMP, 0 }, + + { "stat", gctlHandleStat, HELP_SCOPE_GUESTCONTROL_STAT, 0 }, + + { "closeprocess", gctlHandleCloseProcess, HELP_SCOPE_GUESTCONTROL_CLOSEPROCESS, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER }, + { "closesession", gctlHandleCloseSession, HELP_SCOPE_GUESTCONTROL_CLOSESESSION, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER }, + { "list", gctlHandleList, HELP_SCOPE_GUESTCONTROL_LIST, GCTLCMDCTX_F_SESSION_ANONYMOUS | GCTLCMDCTX_F_NO_SIGNAL_HANDLER }, + { "watch", gctlHandleWatch, HELP_SCOPE_GUESTCONTROL_WATCH, GCTLCMDCTX_F_SESSION_ANONYMOUS }, + + {"updateguestadditions",gctlHandleUpdateAdditions, HELP_SCOPE_GUESTCONTROL_UPDATEGA, GCTLCMDCTX_F_SESSION_ANONYMOUS }, + { "updateadditions", gctlHandleUpdateAdditions, HELP_SCOPE_GUESTCONTROL_UPDATEGA, GCTLCMDCTX_F_SESSION_ANONYMOUS }, + { "updatega", gctlHandleUpdateAdditions, HELP_SCOPE_GUESTCONTROL_UPDATEGA, GCTLCMDCTX_F_SESSION_ANONYMOUS }, + + { "waitrunlevel", gctlHandleWaitRunLevel, HELP_SCOPE_GUESTCONTROL_WAITRUNLEVEL, GCTLCMDCTX_F_SESSION_ANONYMOUS }, + { "waitforrunlevel", gctlHandleWaitRunLevel, HELP_SCOPE_GUESTCONTROL_WAITRUNLEVEL, GCTLCMDCTX_F_SESSION_ANONYMOUS }, + }; + + /* + * VBoxManage guestcontrol [common-options] [common-options] ... + * + * 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]; + + setCurrentSubcommand(s_aCmdDefs[iCmd].fSubcommandScope); + rcExit = s_aCmdDefs[iCmd].pfnHandler(&CmdCtx, pArg->argc - GetState.iNext + 1, + &pArg->argv[GetState.iNext - 1]); + + gctlCtxTerm(&CmdCtx); + return rcExit; + } + return errorSyntax(GuestCtrl::tr("Unknown sub-command: '%s'"), pszCmd); + } + break; + + default: + return errorGetOpt(ch, &ValueUnion); + } + } + if (CmdCtx.pszVmNameOrUuid) + rcExit = errorSyntax(GuestCtrl::tr("Missing sub-command")); + else + rcExit = errorSyntax(GuestCtrl::tr("Missing VM name and sub-command")); + } + return rcExit; +} -- cgit v1.2.3