diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:17:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:17:27 +0000 |
commit | f215e02bf85f68d3a6106c2a1f4f7f063f819064 (patch) | |
tree | 6bb5b92c046312c4e95ac2620b10ddf482d3fa8b /src/VBox/ValidationKit/utils | |
parent | Initial commit. (diff) | |
download | virtualbox-f215e02bf85f68d3a6106c2a1f4f7f063f819064.tar.xz virtualbox-f215e02bf85f68d3a6106c2a1f4f7f063f819064.zip |
Adding upstream version 7.0.14-dfsg.upstream/7.0.14-dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/VBox/ValidationKit/utils')
84 files changed, 43615 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/utils/Makefile.kmk b/src/VBox/ValidationKit/utils/Makefile.kmk new file mode 100644 index 00000000..b527a640 --- /dev/null +++ b/src/VBox/ValidationKit/utils/Makefile.kmk @@ -0,0 +1,78 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Utilities. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Include sub-makefiles. +# +include $(PATH_SUB_CURRENT)/TestExecServ/Makefile.kmk +include $(PATH_SUB_CURRENT)/audio/Makefile.kmk +ifeq ($(KBUILD_TARGET),win) + include $(PATH_SUB_CURRENT)/clipboard/Makefile.kmk +endif +include $(PATH_SUB_CURRENT)/cpu/Makefile.kmk +include $(PATH_SUB_CURRENT)/fs/Makefile.kmk +include $(PATH_SUB_CURRENT)/misc/Makefile.kmk +include $(PATH_SUB_CURRENT)/network/Makefile.kmk +ifeq ($(KBUILD_TARGET),win) + include $(PATH_SUB_CURRENT)/nt/Makefile.kmk +endif +include $(PATH_SUB_CURRENT)/serial/Makefile.kmk +include $(PATH_SUB_CURRENT)/storage/Makefile.kmk +ifeq ($(KBUILD_TARGET),linux) + include $(PATH_SUB_CURRENT)/usb/Makefile.kmk +endif + +# +# On OS/2 the binaries requires the libc DLLs +# (no official static linking support). +# +INSTALLS.os2 += ValidationKitOs2LibC +ValidationKitOs2LibC_TEMPLATE = VBoxValidationKitR3 +ValidationKitOs2LibC_SOURCES = \ + $(KBUILD_BIN_PATH)/libc06.dll \ + $(KBUILD_BIN_PATH)/libc061.dll \ + $(KBUILD_BIN_PATH)/libc062.dll \ + $(KBUILD_BIN_PATH)/libc063.dll \ + $(KBUILD_BIN_PATH)/libc064.dll \ + $(KBUILD_BIN_PATH)/libc065.dll \ + $(KBUILD_BIN_PATH)/libc066.dll + + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/Makefile.kmk b/src/VBox/ValidationKit/utils/TestExecServ/Makefile.kmk new file mode 100644 index 00000000..7fbf9c47 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/Makefile.kmk @@ -0,0 +1,87 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - The Basic Remote Execution Service. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + + +PROGRAMS += TestExecService +TestExecService_TEMPLATE = VBoxValidationKitR3 +ifdef VBOX_WITH_AUTOMATIC_DEFS_QUOTING + TestExecService_DEFS = \ + KBUILD_TARGET="$(KBUILD_TARGET)" \ + KBUILD_TARGET_ARCH="$(KBUILD_TARGET_ARCH)" +else + TestExecService_DEFS = \ + KBUILD_TARGET=\"$(KBUILD_TARGET)\" \ + KBUILD_TARGET_ARCH=\"$(KBUILD_TARGET_ARCH)\" +endif +TestExecService_SOURCES = \ + TestExecService.cpp \ + TestExecServiceTcp.cpp + +ifn1of ($(KBUILD_TARGET), os2) + TestExecService_SOURCES += \ + TestExecServiceSerial.cpp +endif + +INSTALLS += TestExecServiceFiles +TestExecServiceFiles_TEMPLATE = VBoxValidationKitR3 +TestExecServiceFiles_INST = $(INST_VALIDATIONKIT) +TestExecServiceFiles_SOURCES := \ + vboxtxs-readme.txt + +TestExecServiceFiles_EXEC_SOURCES.linux := \ + $(PATH_SUB_CURRENT)/linux/vboxtxs.sh=>linux/vboxtxs \ + $(PATH_SUB_CURRENT)/linux/vboxtxs.service=>linux/vboxtxs.service \ + $(PATH_SUB_CURRENT)/linux/vboxtxs-nat.sh=>linux/vboxtxs-nat + +TestExecServiceFiles_SOURCES.solaris := \ + $(PATH_SUB_CURRENT)/solaris/vboxtxs.xml=>solaris/vboxtxs.xml \ + $(PATH_SUB_CURRENT)/solaris/vboxtxs-sol10.xml=>solaris/vboxtxs-sol10.xml +TestExecServiceFiles_EXEC_SOURCES.solaris := \ + $(PATH_SUB_CURRENT)/solaris/vboxtxs.sh=>solaris/vboxtxs.sh + +TestExecServiceFiles_SOURCES.win := \ + $(PATH_SUB_CURRENT)/win/vboxtxs.reg=>win/vboxtxs.reg \ + $(PATH_SUB_CURRENT)/win/vboxtxs-nat.reg=>win/vboxtxs-nat.reg +TestExecServiceFiles_EXEC_SOURCES.win := \ + $(PATH_SUB_CURRENT)/win/vboxtxs.cmd=>win/vboxtxs.cmd \ + $(PATH_SUB_CURRENT)/win/vboxtxs-nat.cmd=>win/vboxtxs-nat.cmd + + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/TestExecService.cpp b/src/VBox/ValidationKit/utils/TestExecServ/TestExecService.cpp new file mode 100644 index 00000000..002d746b --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/TestExecService.cpp @@ -0,0 +1,4037 @@ +/* $Id: TestExecService.cpp $ */ +/** @file + * TestExecServ - Basic Remote Execution Service. + */ + +/* + * 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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_DEFAULT +#include <iprt/alloca.h> +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/buildconfig.h> +#include <iprt/cdrom.h> +#include <iprt/critsect.h> +#include <iprt/crc.h> +#include <iprt/ctype.h> +#include <iprt/dir.h> +#include <iprt/env.h> +#include <iprt/err.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/handle.h> +#include <iprt/initterm.h> +#include <iprt/log.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/param.h> +#include <iprt/path.h> +#include <iprt/pipe.h> +#include <iprt/poll.h> +#include <iprt/process.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/system.h> +#include <iprt/thread.h> +#include <iprt/time.h> +#include <iprt/uuid.h> +#include <iprt/zip.h> + +#include <package-generated.h> +#include "product-generated.h" + +#include <VBox/version.h> +#include <VBox/log.h> + +#include "product-generated.h" +#include "TestExecServiceInternal.h" + + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Handle IDs used by txsDoExec for the poll set. + */ +typedef enum TXSEXECHNDID +{ + TXSEXECHNDID_STDIN = 0, + TXSEXECHNDID_STDOUT, + TXSEXECHNDID_STDERR, + TXSEXECHNDID_TESTPIPE, + TXSEXECHNDID_STDIN_WRITABLE, + TXSEXECHNDID_TRANSPORT, + TXSEXECHNDID_THREAD +} TXSEXECHNDID; + + +/** + * For buffering process input supplied by the client. + */ +typedef struct TXSEXECSTDINBUF +{ + /** The mount of buffered data. */ + size_t cb; + /** The current data offset. */ + size_t off; + /** The data buffer. */ + char *pch; + /** The amount of allocated buffer space. */ + size_t cbAllocated; + /** Send further input into the bit bucket (stdin is dead). */ + bool fBitBucket; + /** The CRC-32 for standard input (received part). */ + uint32_t uCrc32; +} TXSEXECSTDINBUF; +/** Pointer to a standard input buffer. */ +typedef TXSEXECSTDINBUF *PTXSEXECSTDINBUF; + +/** + * TXS child process info. + */ +typedef struct TXSEXEC +{ + PCTXSPKTHDR pPktHdr; + RTMSINTERVAL cMsTimeout; + int rcReplySend; + + RTPOLLSET hPollSet; + RTPIPE hStdInW; + RTPIPE hStdOutR; + RTPIPE hStdErrR; + RTPIPE hTestPipeR; + RTPIPE hWakeUpPipeR; + RTTHREAD hThreadWaiter; + + /** @name For the setup phase + * @{ */ + struct StdPipe + { + RTHANDLE hChild; + PRTHANDLE phChild; + } StdIn, + StdOut, + StdErr; + RTPIPE hTestPipeW; + RTENV hEnv; + /** @} */ + + /** For serializating some access. */ + RTCRITSECT CritSect; + /** @name Members protected by the critical section. + * @{ */ + RTPROCESS hProcess; + /** The process status. Only valid when fProcessAlive is cleared. */ + RTPROCSTATUS ProcessStatus; + /** Set when the process is alive, clear when dead. */ + bool volatile fProcessAlive; + /** The end of the pipe that hThreadWaiter writes to. */ + RTPIPE hWakeUpPipeW; + /** @} */ +} TXSEXEC; +/** Pointer to a the TXS child process info. */ +typedef TXSEXEC *PTXSEXEC; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** + * Transport layers. + */ +static const PCTXSTRANSPORT g_apTransports[] = +{ + &g_TcpTransport, +#ifndef RT_OS_OS2 + &g_SerialTransport, +#endif + //&g_FileSysTransport, + //&g_GuestPropTransport, + //&g_TestDevTransport, +}; + +/** The release logger. */ +static PRTLOGGER g_pRelLogger; +/** The select transport layer. */ +static PCTXSTRANSPORT g_pTransport; +/** The scratch path. */ +static char g_szScratchPath[RTPATH_MAX]; +/** The default scratch path. */ +static char g_szDefScratchPath[RTPATH_MAX]; +/** The CD/DVD-ROM path. */ +static char g_szCdRomPath[RTPATH_MAX]; +/** The default CD/DVD-ROM path. */ +static char g_szDefCdRomPath[RTPATH_MAX]; +/** The directory containing the TXS executable. */ +static char g_szTxsDir[RTPATH_MAX]; +/** The current working directory for TXS (doesn't change). */ +static char g_szCwd[RTPATH_MAX]; +/** The operating system short name. */ +static char g_szOsShortName[16]; +/** The CPU architecture short name. */ +static char g_szArchShortName[16]; +/** The combined "OS.arch" name. */ +static char g_szOsDotArchShortName[32]; +/** The combined "OS/arch" name. */ +static char g_szOsSlashArchShortName[32]; +/** The executable suffix. */ +static char g_szExeSuff[8]; +/** The shell script suffix. */ +static char g_szScriptSuff[8]; +/** UUID identifying this TXS instance. This can be used to see if TXS + * has been restarted or not. */ +static RTUUID g_InstanceUuid; +/** Whether to display the output of the child process or not. */ +static bool g_fDisplayOutput = true; +/** Whether to terminate or not. + * @todo implement signals and stuff. */ +static bool volatile g_fTerminate = false; +/** Verbosity level. */ +uint32_t g_cVerbose = 1; + + +/** + * Calculates the checksum value, zero any padding space and send the packet. + * + * @returns IPRT status code. + * @param pPkt The packet to send. Must point to a correctly + * aligned buffer. + */ +static int txsSendPkt(PTXSPKTHDR pPkt) +{ + Assert(pPkt->cb >= sizeof(*pPkt)); + pPkt->uCrc32 = RTCrc32(pPkt->achOpcode, pPkt->cb - RT_UOFFSETOF(TXSPKTHDR, achOpcode)); + if (pPkt->cb != RT_ALIGN_32(pPkt->cb, TXSPKT_ALIGNMENT)) + memset((uint8_t *)pPkt + pPkt->cb, '\0', RT_ALIGN_32(pPkt->cb, TXSPKT_ALIGNMENT) - pPkt->cb); + + Log(("txsSendPkt: cb=%#x opcode=%.8s\n", pPkt->cb, pPkt->achOpcode)); + Log2(("%.*Rhxd\n", RT_MIN(pPkt->cb, 256), pPkt)); + int rc = g_pTransport->pfnSendPkt(pPkt); + while (RT_UNLIKELY(rc == VERR_INTERRUPTED) && !g_fTerminate) + rc = g_pTransport->pfnSendPkt(pPkt); + if (RT_FAILURE(rc)) + Log(("txsSendPkt: rc=%Rrc\n", rc)); + + return rc; +} + +/** + * Sends a babble reply and disconnects the client (if applicable). + * + * @param pszOpcode The BABBLE opcode. + */ +static void txsReplyBabble(const char *pszOpcode) +{ + TXSPKTHDR Reply; + Reply.cb = sizeof(Reply); + Reply.uCrc32 = 0; + memcpy(Reply.achOpcode, pszOpcode, sizeof(Reply.achOpcode)); + + g_pTransport->pfnBabble(&Reply, 20*1000); +} + +/** + * Receive and validate a packet. + * + * Will send bable responses to malformed packets that results in a error status + * code. + * + * @returns IPRT status code. + * @param ppPktHdr Where to return the packet on success. Free + * with RTMemFree. + * @param fAutoRetryOnFailure Whether to retry on error. + */ +static int txsRecvPkt(PPTXSPKTHDR ppPktHdr, bool fAutoRetryOnFailure) +{ + for (;;) + { + PTXSPKTHDR pPktHdr; + int rc = g_pTransport->pfnRecvPkt(&pPktHdr); + if (RT_SUCCESS(rc)) + { + /* validate the packet. */ + if ( pPktHdr->cb >= sizeof(TXSPKTHDR) + && pPktHdr->cb < TXSPKT_MAX_SIZE) + { + Log2(("txsRecvPkt: pPktHdr=%p cb=%#x crc32=%#x opcode=%.8s\n" + "%.*Rhxd\n", + pPktHdr, pPktHdr->cb, pPktHdr->uCrc32, pPktHdr->achOpcode, RT_MIN(pPktHdr->cb, 256), pPktHdr)); + uint32_t uCrc32Calc = pPktHdr->uCrc32 != 0 + ? RTCrc32(&pPktHdr->achOpcode[0], pPktHdr->cb - RT_UOFFSETOF(TXSPKTHDR, achOpcode)) + : 0; + if (pPktHdr->uCrc32 == uCrc32Calc) + { + AssertCompileMemberSize(TXSPKTHDR, achOpcode, 8); + if ( RT_C_IS_UPPER(pPktHdr->achOpcode[0]) + && RT_C_IS_UPPER(pPktHdr->achOpcode[1]) + && (RT_C_IS_UPPER(pPktHdr->achOpcode[2]) || pPktHdr->achOpcode[2] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[3]) || pPktHdr->achOpcode[3] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[4]) || pPktHdr->achOpcode[4] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[5]) || pPktHdr->achOpcode[5] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[6]) || pPktHdr->achOpcode[6] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[7]) || pPktHdr->achOpcode[7] == ' ') + ) + { + Log(("txsRecvPkt: cb=%#x opcode=%.8s\n", pPktHdr->cb, pPktHdr->achOpcode)); + *ppPktHdr = pPktHdr; + return rc; + } + + rc = VERR_IO_BAD_COMMAND; + } + else + { + Log(("txsRecvPkt: cb=%#x opcode=%.8s crc32=%#x actual=%#x\n", + pPktHdr->cb, pPktHdr->achOpcode, pPktHdr->uCrc32, uCrc32Calc)); + rc = VERR_IO_CRC; + } + } + else + rc = VERR_IO_BAD_LENGTH; + + /* Send babble reply and disconnect the client if the transport is + connection oriented. */ + if (rc == VERR_IO_BAD_LENGTH) + txsReplyBabble("BABBLE L"); + else if (rc == VERR_IO_CRC) + txsReplyBabble("BABBLE C"); + else if (rc == VERR_IO_BAD_COMMAND) + txsReplyBabble("BABBLE O"); + else + txsReplyBabble("BABBLE "); + RTMemFree(pPktHdr); + } + + /* Try again or return failure? */ + if ( g_fTerminate + || rc != VERR_INTERRUPTED + || !fAutoRetryOnFailure + ) + { + Log(("txsRecvPkt: rc=%Rrc\n", rc)); + return rc; + } + } +} + +/** + * Make a simple reply, only status opcode. + * + * @returns IPRT status code of the send. + * @param pReply The reply packet. + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param cbExtra Bytes in addition to the header. + */ +static int txsReplyInternal(PTXSPKTHDR pReply, const char *pszOpcode, size_t cbExtra) +{ + /* copy the opcode, don't be too strict in case of a padding screw up. */ + size_t cchOpcode = strlen(pszOpcode); + if (RT_LIKELY(cchOpcode == sizeof(pReply->achOpcode))) + memcpy(pReply->achOpcode, pszOpcode, sizeof(pReply->achOpcode)); + else + { + Assert(cchOpcode == sizeof(pReply->achOpcode)); + while (cchOpcode > 0 && pszOpcode[cchOpcode - 1] == ' ') + cchOpcode--; + AssertMsgReturn(cchOpcode < sizeof(pReply->achOpcode), ("%d/'%.8s'\n", cchOpcode, pszOpcode), VERR_INTERNAL_ERROR_4); + memcpy(pReply->achOpcode, pszOpcode, cchOpcode); + memset(&pReply->achOpcode[cchOpcode], ' ', sizeof(pReply->achOpcode) - cchOpcode); + } + + pReply->cb = (uint32_t)sizeof(TXSPKTHDR) + (uint32_t)cbExtra; + pReply->uCrc32 = 0; /* (txsSendPkt sets it) */ + + return txsSendPkt(pReply); +} + +/** + * Make a simple reply, only status opcode. + * + * @returns IPRT status code of the send. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + */ +static int txsReplySimple(PCTXSPKTHDR pPktHdr, const char *pszOpcode) +{ + TXSPKTHDR Pkt; + NOREF(pPktHdr); + return txsReplyInternal(&Pkt, pszOpcode, 0); +} + +/** + * Acknowledges a packet with success. + * + * @returns IPRT status code of the send. + * @param pPktHdr The original packet (for future use). + */ +static int txsReplyAck(PCTXSPKTHDR pPktHdr) +{ + return txsReplySimple(pPktHdr, "ACK "); +} + +/** + * Replies with a failure. + * + * @returns IPRT status code of the send. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param pszDetailFmt Longer description of the problem (format + * string). + * @param va Format arguments. + */ +static int txsReplyFailureV(PCTXSPKTHDR pPktHdr, const char *pszOpcode, const char *pszDetailFmt, va_list va) +{ + NOREF(pPktHdr); + union + { + TXSPKTHDR Hdr; + char ach[256]; + } uPkt; + + size_t cchDetail = RTStrPrintfV(&uPkt.ach[sizeof(TXSPKTHDR)], + sizeof(uPkt) - sizeof(TXSPKTHDR), + pszDetailFmt, va); + return txsReplyInternal(&uPkt.Hdr, pszOpcode, cchDetail + 1); +} + +/** + * Replies with a failure. + * + * @returns IPRT status code of the send. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param pszDetailFmt Longer description of the problem (format + * string). + * @param ... Format arguments. + */ +static int txsReplyFailure(PCTXSPKTHDR pPktHdr, const char *pszOpcode, const char *pszDetailFmt, ...) +{ + va_list va; + va_start(va, pszDetailFmt); + int rc = txsReplyFailureV(pPktHdr, pszOpcode, pszDetailFmt, va); + va_end(va); + return rc; +} + +/** + * Replies according to the return code. + * + * @returns IPRT status code of the send. + * @param pPktHdr The packet to reply to. + * @param rcOperation The status code to report. + * @param pszOperationFmt The operation that failed. Typically giving the + * function call with important arguments. + * @param ... Arguments to the format string. + */ +static int txsReplyRC(PCTXSPKTHDR pPktHdr, int rcOperation, const char *pszOperationFmt, ...) +{ + if (RT_SUCCESS(rcOperation)) + return txsReplyAck(pPktHdr); + + char szOperation[128]; + va_list va; + va_start(va, pszOperationFmt); + RTStrPrintfV(szOperation, sizeof(szOperation), pszOperationFmt, va); + va_end(va); + + return txsReplyFailure(pPktHdr, "FAILED ", "%s failed with rc=%Rrc (opcode '%.8s')", + szOperation, rcOperation, pPktHdr->achOpcode); +} + +/** + * Signal a bad packet minum size. + * + * @returns IPRT status code of the send. + * @param pPktHdr The packet to reply to. + * @param cbMin The minimum size. + */ +static int txsReplyBadMinSize(PCTXSPKTHDR pPktHdr, size_t cbMin) +{ + return txsReplyFailure(pPktHdr, "BAD SIZE", "Expected at least %zu bytes, got %u (opcode '%.8s')", + cbMin, pPktHdr->cb, pPktHdr->achOpcode); +} + +/** + * Signal a bad packet exact size. + * + * @returns IPRT status code of the send. + * @param pPktHdr The packet to reply to. + * @param cb The wanted size. + */ +static int txsReplyBadSize(PCTXSPKTHDR pPktHdr, size_t cb) +{ + return txsReplyFailure(pPktHdr, "BAD SIZE", "Expected at %zu bytes, got %u (opcode '%.8s')", + cb, pPktHdr->cb, pPktHdr->achOpcode); +} + +/** + * Deals with a command that isn't implemented yet. + * @returns IPRT status code of the send. + * @param pPktHdr The packet which opcode isn't implemented. + */ +static int txsReplyNotImplemented(PCTXSPKTHDR pPktHdr) +{ + return txsReplyFailure(pPktHdr, "NOT IMPL", "Opcode '%.8s' is not implemented", pPktHdr->achOpcode); +} + +/** + * Deals with a unknown command. + * @returns IPRT status code of the send. + * @param pPktHdr The packet to reply to. + */ +static int txsReplyUnknown(PCTXSPKTHDR pPktHdr) +{ + return txsReplyFailure(pPktHdr, "UNKNOWN ", "Opcode '%.8s' is not known", pPktHdr->achOpcode); +} + +/** + * Replaces a variable with its value. + * + * @returns VINF_SUCCESS or VERR_NO_STR_MEMORY. + * @param ppszNew In/Out. + * @param pcchNew In/Out. (Messed up on failure.) + * @param offVar Variable offset. + * @param cchVar Variable length. + * @param pszValue The value. + * @param cchValue Value length. + */ +static int txsReplaceStringVariable(char **ppszNew, size_t *pcchNew, size_t offVar, size_t cchVar, + const char *pszValue, size_t cchValue) +{ + size_t const cchAfter = *pcchNew - offVar - cchVar; + if (cchVar < cchValue) + { + *pcchNew += cchValue - cchVar; + int rc = RTStrRealloc(ppszNew, *pcchNew + 1); + if (RT_FAILURE(rc)) + return rc; + } + + char *pszNew = *ppszNew; + memmove(&pszNew[offVar + cchValue], &pszNew[offVar + cchVar], cchAfter + 1); + memcpy(&pszNew[offVar], pszValue, cchValue); + return VINF_SUCCESS; +} + +/** + * Replace the variables found in the source string, returning a new string that + * lives on the string heap. + * + * @returns Boolean success indicator. Will reply to the client with all the + * gory detail on failure. + * @param pPktHdr The packet the string relates to. For replying + * on error. + * @param pszSrc The source string. + * @param ppszNew Where to return the new string. + * @param prcSend Where to return the status code of the send on + * failure. + */ +static int txsReplaceStringVariables(PCTXSPKTHDR pPktHdr, const char *pszSrc, char **ppszNew, int *prcSend) +{ + /* Lazy approach that employs memmove. */ + size_t cchNew = strlen(pszSrc); + char *pszNew = RTStrDup(pszSrc); + char *pszDollar = pszNew; + while (pszDollar && (pszDollar = strchr(pszDollar, '$')) != NULL) + { + if (pszDollar[1] == '{') + { + char *pszEnd = strchr(&pszDollar[2], '}'); + if (pszEnd) + { +#define IF_VARIABLE_DO(pszDollar, szVarExpr, pszValue) \ + if ( cchVar == sizeof(szVarExpr) - 1 \ + && !memcmp(pszDollar, szVarExpr, sizeof(szVarExpr) - 1) ) \ + { \ + size_t const cchValue = strlen(pszValue); \ + rc = txsReplaceStringVariable(&pszNew, &cchNew, offDollar, \ + sizeof(szVarExpr) - 1, pszValue, cchValue); \ + offDollar += cchValue; \ + } + int rc; + size_t const cchVar = pszEnd - pszDollar + 1; /* includes "${}" */ + size_t offDollar = pszDollar - pszNew; + IF_VARIABLE_DO(pszDollar, "${CDROM}", g_szCdRomPath) + else IF_VARIABLE_DO(pszDollar, "${SCRATCH}", g_szScratchPath) + else IF_VARIABLE_DO(pszDollar, "${ARCH}", g_szArchShortName) + else IF_VARIABLE_DO(pszDollar, "${OS}", g_szOsShortName) + else IF_VARIABLE_DO(pszDollar, "${OS.ARCH}", g_szOsDotArchShortName) + else IF_VARIABLE_DO(pszDollar, "${OS/ARCH}", g_szOsSlashArchShortName) + else IF_VARIABLE_DO(pszDollar, "${EXESUFF}", g_szExeSuff) + else IF_VARIABLE_DO(pszDollar, "${SCRIPTSUFF}", g_szScriptSuff) + else IF_VARIABLE_DO(pszDollar, "${TXSDIR}", g_szTxsDir) + else IF_VARIABLE_DO(pszDollar, "${CWD}", g_szCwd) + else if ( cchVar >= sizeof("${env.") + 1 + && memcmp(pszDollar, RT_STR_TUPLE("${env.")) == 0) + { + const char *pszEnvVar = pszDollar + 6; + size_t cchValue = 0; + char szValue[RTPATH_MAX]; + *pszEnd = '\0'; + rc = RTEnvGetEx(RTENV_DEFAULT, pszEnvVar, szValue, sizeof(szValue), &cchValue); + if (RT_SUCCESS(rc)) + { + *pszEnd = '}'; + rc = txsReplaceStringVariable(&pszNew, &cchNew, offDollar, cchVar, szValue, cchValue); + offDollar += cchValue; + } + else + { + if (rc == VERR_ENV_VAR_NOT_FOUND) + *prcSend = txsReplyFailure(pPktHdr, "UNKN VAR", "Environment variable '%s' encountered in '%s'", + pszEnvVar, pszSrc); + else + *prcSend = txsReplyFailure(pPktHdr, "FAILDENV", + "RTEnvGetEx(,'%s',,,) failed with %Rrc (opcode '%.8s')", + pszEnvVar, rc, pPktHdr->achOpcode); + RTStrFree(pszNew); + *ppszNew = NULL; + return false; + } + } + else + { + RTStrFree(pszNew); + *prcSend = txsReplyFailure(pPktHdr, "UNKN VAR", "Unknown variable '%.*s' encountered in '%s'", + cchVar, pszDollar, pszSrc); + *ppszNew = NULL; + return false; + } + pszDollar = &pszNew[offDollar]; + + if (RT_FAILURE(rc)) + { + RTStrFree(pszNew); + *prcSend = txsReplyRC(pPktHdr, rc, "RTStrRealloc"); + *ppszNew = NULL; + return false; + } +#undef IF_VARIABLE_DO + } + } + /* Undo dollar escape sequences: $$ -> $ */ + else if (pszDollar[1] == '$') + { + size_t cchLeft = cchNew - (&pszDollar[1] - pszNew); + memmove(pszDollar, &pszDollar[1], cchLeft); + pszDollar[cchLeft] = '\0'; + cchNew -= 1; + } + else /* No match, move to next char to avoid endless looping. */ + pszDollar++; + } + + *ppszNew = pszNew; + *prcSend = VINF_SUCCESS; + return true; +} + +/** + * Checks if the string is valid and returns the expanded version. + * + * @returns true if valid, false if invalid. + * @param pPktHdr The packet being unpacked. + * @param pszArgName The argument name. + * @param psz Pointer to the string within pPktHdr. + * @param ppszExp Where to return the expanded string. Must be + * freed by calling RTStrFree(). + * @param ppszNext Where to return the pointer to the next field. + * If NULL, then we assume this string is at the + * end of the packet and will make sure it has the + * advertised length. + * @param prcSend Where to return the status code of the send on + * failure. + */ +static bool txsIsStringValid(PCTXSPKTHDR pPktHdr, const char *pszArgName, const char *psz, + char **ppszExp, const char **ppszNext, int *prcSend) +{ + *ppszExp = NULL; + if (ppszNext) + *ppszNext = NULL; + + size_t const off = psz - (const char *)pPktHdr; + if (pPktHdr->cb <= off) + { + *prcSend = txsReplyFailure(pPktHdr, "STR MISS", "Missing string argument '%s' in '%.8s'", + pszArgName, pPktHdr->achOpcode); + return false; + } + + size_t const cchMax = pPktHdr->cb - off; + const char *pszEnd = RTStrEnd(psz, cchMax); + if (!pszEnd) + { + *prcSend = txsReplyFailure(pPktHdr, "STR TERM", "The string argument '%s' in '%.8s' is unterminated", + pszArgName, pPktHdr->achOpcode); + return false; + } + + if (!ppszNext && (size_t)(pszEnd - psz) != cchMax - 1) + { + *prcSend = txsReplyFailure(pPktHdr, "STR SHRT", "The string argument '%s' in '%.8s' is shorter than advertised", + pszArgName, pPktHdr->achOpcode); + return false; + } + + if (!txsReplaceStringVariables(pPktHdr, psz, ppszExp, prcSend)) + return false; + if (ppszNext) + *ppszNext = pszEnd + 1; + return true; +} + +/** + * Validates a packet with a single string after the header. + * + * @returns true if valid, false if invalid. + * @param pPktHdr The packet. + * @param pszArgName The argument name. + * @param ppszExp Where to return the string pointer. Variables + * will be replaced and it must therefore be freed + * by calling RTStrFree(). + * @param prcSend Where to return the status code of the send on + * failure. + */ +static bool txsIsStringPktValid(PCTXSPKTHDR pPktHdr, const char *pszArgName, char **ppszExp, int *prcSend) +{ + if (pPktHdr->cb < sizeof(TXSPKTHDR) + 2) + { + *ppszExp = NULL; + *prcSend = txsReplyBadMinSize(pPktHdr, sizeof(TXSPKTHDR) + 2); + return false; + } + + return txsIsStringValid(pPktHdr, pszArgName, (const char *)(pPktHdr + 1), ppszExp, NULL, prcSend); +} + +/** + * Checks if the two opcodes match. + * + * @returns true on match, false on mismatch. + * @param pPktHdr The packet header. + * @param pszOpcode2 The opcode we're comparing with. Does not have + * to be the whole 8 chars long. + */ +DECLINLINE(bool) txsIsSameOpcode(PCTXSPKTHDR pPktHdr, const char *pszOpcode2) +{ + if (pPktHdr->achOpcode[0] != pszOpcode2[0]) + return false; + if (pPktHdr->achOpcode[1] != pszOpcode2[1]) + return false; + + unsigned i = 2; + while ( i < RT_SIZEOFMEMB(TXSPKTHDR, achOpcode) + && pszOpcode2[i] != '\0') + { + if (pPktHdr->achOpcode[i] != pszOpcode2[i]) + break; + i++; + } + + if ( i < RT_SIZEOFMEMB(TXSPKTHDR, achOpcode) + && pszOpcode2[i] == '\0') + { + while ( i < RT_SIZEOFMEMB(TXSPKTHDR, achOpcode) + && pPktHdr->achOpcode[i] == ' ') + i++; + } + + return i == RT_SIZEOFMEMB(TXSPKTHDR, achOpcode); +} + +/** + * Used by txsDoGetFile to wait for a reply ACK from the client. + * + * @returns VINF_SUCCESS on ACK, VERR_GENERAL_FAILURE on NACK, + * VERR_NET_NOT_CONNECTED on unknown response (sending a bable reply), + * or whatever txsRecvPkt returns. + * @param pPktHdr The original packet (for future use). + */ +static int txsWaitForAck(PCTXSPKTHDR pPktHdr) +{ + NOREF(pPktHdr); + /** @todo timeout? */ + PTXSPKTHDR pReply; + int rc = txsRecvPkt(&pReply, false /*fAutoRetryOnFailure*/); + if (RT_SUCCESS(rc)) + { + if (txsIsSameOpcode(pReply, "ACK")) + rc = VINF_SUCCESS; + else if (txsIsSameOpcode(pReply, "NACK")) + rc = VERR_GENERAL_FAILURE; + else + { + txsReplyBabble("BABBLE "); + rc = VERR_NET_NOT_CONNECTED; + } + RTMemFree(pReply); + } + return rc; +} + +/** + * Expands the variables in the string and sends it back to the host. + * + * @returns IPRT status code from send. + * @param pPktHdr The expand string packet. + */ +static int txsDoExpandString(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszExpanded; + if (!txsIsStringPktValid(pPktHdr, "string", &pszExpanded, &rc)) + return rc; + + struct + { + TXSPKTHDR Hdr; + char szString[_64K]; + char abPadding[TXSPKT_ALIGNMENT]; + } Pkt; + + size_t const cbExpanded = strlen(pszExpanded) + 1; + if (cbExpanded <= sizeof(Pkt.szString)) + { + memcpy(Pkt.szString, pszExpanded, cbExpanded); + rc = txsReplyInternal(&Pkt.Hdr, "STRING ", cbExpanded); + } + else + { + memcpy(Pkt.szString, pszExpanded, sizeof(Pkt.szString)); + Pkt.szString[0] = '\0'; + rc = txsReplyInternal(&Pkt.Hdr, "SHORTSTR", sizeof(Pkt.szString)); + } + + RTStrFree(pszExpanded); + return rc; +} + +/** + * Packs a tar file / directory. + * + * @returns IPRT status code from send. + * @param pPktHdr The pack file packet. + */ +static int txsDoPackFile(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszFile = NULL; + char *pszSource = NULL; + + /* Packet cursor. */ + const char *pch = (const char *)(pPktHdr + 1); + + if (txsIsStringValid(pPktHdr, "file", pch, &pszFile, &pch, &rc)) + { + if (txsIsStringValid(pPktHdr, "source", pch, &pszSource, &pch, &rc)) + { + char *pszSuff = RTPathSuffix(pszFile); + + const char *apszArgs[7]; + unsigned cArgs = 0; + + apszArgs[cArgs++] = "RTTar"; + apszArgs[cArgs++] = "--create"; + + apszArgs[cArgs++] = "--file"; + apszArgs[cArgs++] = pszFile; + + if ( pszSuff + && ( !RTStrICmp(pszSuff, ".gz") + || !RTStrICmp(pszSuff, ".tgz"))) + apszArgs[cArgs++] = "--gzip"; + + apszArgs[cArgs++] = pszSource; + + RTEXITCODE rcExit = RTZipTarCmd(cArgs, (char **)apszArgs); + if (rcExit != RTEXITCODE_SUCCESS) + rc = VERR_GENERAL_FAILURE; /** @todo proper return code. */ + else + rc = VINF_SUCCESS; + + rc = txsReplyRC(pPktHdr, rc, "RTZipTarCmd(\"%s\",\"%s\")", + pszFile, pszSource); + + RTStrFree(pszSource); + } + RTStrFree(pszFile); + } + + return rc; +} + +/** + * Unpacks a tar file. + * + * @returns IPRT status code from send. + * @param pPktHdr The unpack file packet. + */ +static int txsDoUnpackFile(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszFile = NULL; + char *pszDirectory = NULL; + + /* Packet cursor. */ + const char *pch = (const char *)(pPktHdr + 1); + + if (txsIsStringValid(pPktHdr, "file", pch, &pszFile, &pch, &rc)) + { + if (txsIsStringValid(pPktHdr, "directory", pch, &pszDirectory, &pch, &rc)) + { + char *pszSuff = RTPathSuffix(pszFile); + + const char *apszArgs[7]; + unsigned cArgs = 0; + + apszArgs[cArgs++] = "RTTar"; + apszArgs[cArgs++] = "--extract"; + + apszArgs[cArgs++] = "--file"; + apszArgs[cArgs++] = pszFile; + + apszArgs[cArgs++] = "--directory"; + apszArgs[cArgs++] = pszDirectory; + + if ( pszSuff + && ( !RTStrICmp(pszSuff, ".gz") + || !RTStrICmp(pszSuff, ".tgz"))) + apszArgs[cArgs++] = "--gunzip"; + + RTEXITCODE rcExit = RTZipTarCmd(cArgs, (char **)apszArgs); + if (rcExit != RTEXITCODE_SUCCESS) + rc = VERR_GENERAL_FAILURE; /** @todo proper return code. */ + else + rc = VINF_SUCCESS; + + rc = txsReplyRC(pPktHdr, rc, "RTZipTarCmd(\"%s\",\"%s\")", + pszFile, pszDirectory); + + RTStrFree(pszDirectory); + } + RTStrFree(pszFile); + } + + return rc; +} + +/** + * Downloads a file to the client. + * + * The transfer sends a stream of DATA packets (0 or more) and ends it all with + * a ACK packet. If an error occurs, a FAILURE packet is sent and the transfer + * aborted. + * + * @returns IPRT status code from send. + * @param pPktHdr The get file packet. + */ +static int txsDoGetFile(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "file", &pszPath, &rc)) + return rc; + + RTFILE hFile; + rc = RTFileOpen(&hFile, pszPath, RTFILE_O_READ | RTFILE_O_DENY_NONE | RTFILE_O_OPEN); + if (RT_SUCCESS(rc)) + { + uint32_t uMyCrc32 = RTCrc32Start(); + for (;;) + { + struct + { + TXSPKTHDR Hdr; + uint32_t uCrc32; + char ab[_64K]; + char abPadding[TXSPKT_ALIGNMENT]; + } Pkt; + size_t cbRead; + rc = RTFileRead(hFile, &Pkt.ab[0], _64K, &cbRead); + if (RT_FAILURE(rc) || cbRead == 0) + { + if (rc == VERR_EOF || (RT_SUCCESS(rc) && cbRead == 0)) + { + Pkt.uCrc32 = RTCrc32Finish(uMyCrc32); + rc = txsReplyInternal(&Pkt.Hdr, "DATA EOF", sizeof(uint32_t)); + if (RT_SUCCESS(rc)) + rc = txsWaitForAck(&Pkt.Hdr); + } + else + rc = txsReplyRC(pPktHdr, rc, "RTFileRead"); + break; + } + + uMyCrc32 = RTCrc32Process(uMyCrc32, &Pkt.ab[0], cbRead); + Pkt.uCrc32 = RTCrc32Finish(uMyCrc32); + rc = txsReplyInternal(&Pkt.Hdr, "DATA ", cbRead + sizeof(uint32_t)); + if (RT_FAILURE(rc)) + break; + rc = txsWaitForAck(&Pkt.Hdr); + if (RT_FAILURE(rc)) + break; + } + + RTFileClose(hFile); + } + else + rc = txsReplyRC(pPktHdr, rc, "RTFileOpen(,\"%s\",)", pszPath); + + RTStrFree(pszPath); + return rc; +} + +/** + * Copies a file from the source to the destination locally. + * + * @returns IPRT status code from send. + * @param pPktHdr The copy file packet. + */ +static int txsDoCopyFile(PCTXSPKTHDR pPktHdr) +{ + /* After the packet header follows a 32-bit file mode, + * the remainder of the packet are two zero terminated paths. */ + size_t const cbMin = sizeof(TXSPKTHDR) + sizeof(RTFMODE) + 2; + if (pPktHdr->cb < cbMin) + return txsReplyBadMinSize(pPktHdr, cbMin); + + /* Packet cursor. */ + const char *pch = (const char *)(pPktHdr + 1); + + int rc; + + RTFMODE const fMode = *(RTFMODE const *)pch; + + char *pszSrc; + if (txsIsStringValid(pPktHdr, "source", (const char *)pch + sizeof(RTFMODE), &pszSrc, &pch, &rc)) + { + char *pszDst; + if (txsIsStringValid(pPktHdr, "dest", pch, &pszDst, NULL /* Check for string termination */, &rc)) + { + rc = RTFileCopy(pszSrc, pszDst); + if (RT_SUCCESS(rc)) + { + if (fMode) /* Do we need to set the file mode? */ + { + rc = RTPathSetMode(pszDst, fMode); + if (RT_FAILURE(rc)) + rc = txsReplyRC(pPktHdr, rc, "RTPathSetMode(\"%s\", %#x)", pszDst, fMode); + } + + if (RT_SUCCESS(rc)) + rc = txsReplyAck(pPktHdr); + } + else + rc = txsReplyRC(pPktHdr, rc, "RTFileCopy"); + RTStrFree(pszDst); + } + + RTStrFree(pszSrc); + } + + return rc; +} + +/** + * Uploads a file from the client. + * + * The transfer sends a stream of DATA packets (0 or more) and ends it all with + * a DATA EOF packet. We ACK each of these, so that if a write error occurs we + * can abort the transfer straight away. + * + * @returns IPRT status code from send. + * @param pPktHdr The put file packet. + * @param fHasMode Set if the packet starts with a mode field. + */ +static int txsDoPutFile(PCTXSPKTHDR pPktHdr, bool fHasMode) +{ + int rc; + RTFMODE fMode = 0; + char *pszPath; + if (!fHasMode) + { + if (!txsIsStringPktValid(pPktHdr, "file", &pszPath, &rc)) + return rc; + } + else + { + /* After the packet header follows a mode mask and the remainder of + the packet is the zero terminated file name. */ + size_t const cbMin = sizeof(TXSPKTHDR) + sizeof(RTFMODE) + 2; + if (pPktHdr->cb < cbMin) + return txsReplyBadMinSize(pPktHdr, cbMin); + if (!txsIsStringValid(pPktHdr, "file", (const char *)(pPktHdr + 1) + sizeof(RTFMODE), &pszPath, NULL, &rc)) + return rc; + fMode = *(RTFMODE const *)(pPktHdr + 1); + fMode <<= RTFILE_O_CREATE_MODE_SHIFT; + fMode &= RTFILE_O_CREATE_MODE_MASK; + } + + RTFILE hFile; + rc = RTFileOpen(&hFile, pszPath, RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_CREATE_REPLACE | fMode); + if (RT_SUCCESS(rc)) + { + bool fSuccess = false; + rc = txsReplyAck(pPktHdr); + if (RT_SUCCESS(rc)) + { + if (fMode) + RTFileSetMode(hFile, fMode); + + /* + * Read client command packets and process them. + */ + uint32_t uMyCrc32 = RTCrc32Start(); + for (;;) + { + PTXSPKTHDR pDataPktHdr; + rc = txsRecvPkt(&pDataPktHdr, false /*fAutoRetryOnFailure*/); + if (RT_FAILURE(rc)) + break; + + if (txsIsSameOpcode(pDataPktHdr, "DATA")) + { + size_t const cbMin = sizeof(TXSPKTHDR) + sizeof(uint32_t); + if (pDataPktHdr->cb >= cbMin) + { + size_t cbData = pDataPktHdr->cb - cbMin; + const void *pvData = (const char *)pDataPktHdr + cbMin; + uint32_t uCrc32 = *(uint32_t const *)(pDataPktHdr + 1); + + uMyCrc32 = RTCrc32Process(uMyCrc32, pvData, cbData); + if (RTCrc32Finish(uMyCrc32) == uCrc32) + { + rc = RTFileWrite(hFile, pvData, cbData, NULL); + if (RT_SUCCESS(rc)) + { + rc = txsReplyAck(pDataPktHdr); + RTMemFree(pDataPktHdr); + continue; + } + + rc = txsReplyRC(pDataPktHdr, rc, "RTFileWrite"); + } + else + rc = txsReplyFailure(pDataPktHdr, "BAD DCRC", "mycrc=%#x your=%#x", uMyCrc32, uCrc32); + } + else + rc = txsReplyBadMinSize(pPktHdr, cbMin); + } + else if (txsIsSameOpcode(pDataPktHdr, "DATA EOF")) + { + if (pDataPktHdr->cb == sizeof(TXSPKTHDR) + sizeof(uint32_t)) + { + uint32_t uCrc32 = *(uint32_t const *)(pDataPktHdr + 1); + if (RTCrc32Finish(uMyCrc32) == uCrc32) + { + rc = txsReplyAck(pDataPktHdr); + fSuccess = RT_SUCCESS(rc); + } + else + rc = txsReplyFailure(pDataPktHdr, "BAD DCRC", "mycrc=%#x your=%#x", uMyCrc32, uCrc32); + } + else + rc = txsReplyAck(pDataPktHdr); + } + else if (txsIsSameOpcode(pDataPktHdr, "ABORT")) + rc = txsReplyAck(pDataPktHdr); + else + rc = txsReplyFailure(pDataPktHdr, "UNKNOWN ", "Opcode '%.8s' is not known or not recognized during PUT FILE", pDataPktHdr->achOpcode); + RTMemFree(pDataPktHdr); + break; + } + } + + RTFileClose(hFile); + + /* + * Delete the file on failure. + */ + if (!fSuccess) + RTFileDelete(pszPath); + } + else + rc = txsReplyRC(pPktHdr, rc, "RTFileOpen(,\"%s\",)", pszPath); + + RTStrFree(pszPath); + return rc; +} + +/** + * List the entries in the specified directory. + * + * @returns IPRT status code from send. + * @param pPktHdr The list packet. + */ +static int txsDoList(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "dir", &pszPath, &rc)) + return rc; + + rc = txsReplyNotImplemented(pPktHdr); + + RTStrFree(pszPath); + return rc; +} + +/** + * Worker for STAT and LSTAT for packing down the file info reply. + * + * @returns IPRT status code from send. + * @param pInfo The info to pack down. + */ +static int txsReplyObjInfo(PCRTFSOBJINFO pInfo) +{ + struct + { + TXSPKTHDR Hdr; + int64_t cbObject; + int64_t cbAllocated; + int64_t nsAccessTime; + int64_t nsModificationTime; + int64_t nsChangeTime; + int64_t nsBirthTime; + uint32_t fMode; + uint32_t uid; + uint32_t gid; + uint32_t cHardLinks; + uint64_t INodeIdDevice; + uint64_t INodeId; + uint64_t Device; + char abPadding[TXSPKT_ALIGNMENT]; + } Pkt; + + Pkt.cbObject = pInfo->cbObject; + Pkt.cbAllocated = pInfo->cbAllocated; + Pkt.nsAccessTime = RTTimeSpecGetNano(&pInfo->AccessTime); + Pkt.nsModificationTime = RTTimeSpecGetNano(&pInfo->ModificationTime); + Pkt.nsChangeTime = RTTimeSpecGetNano(&pInfo->ChangeTime); + Pkt.nsBirthTime = RTTimeSpecGetNano(&pInfo->BirthTime); + Pkt.fMode = pInfo->Attr.fMode; + Pkt.uid = pInfo->Attr.u.Unix.uid; + Pkt.gid = pInfo->Attr.u.Unix.gid; + Pkt.cHardLinks = pInfo->Attr.u.Unix.cHardlinks; + Pkt.INodeIdDevice = pInfo->Attr.u.Unix.INodeIdDevice; + Pkt.INodeId = pInfo->Attr.u.Unix.INodeId; + Pkt.Device = pInfo->Attr.u.Unix.Device; + + return txsReplyInternal(&Pkt.Hdr, "FILEINFO", sizeof(Pkt) - TXSPKT_ALIGNMENT - sizeof(TXSPKTHDR)); +} + +/** + * Get info about a file system object, following all but the symbolic links + * except in the final path component. + * + * @returns IPRT status code from send. + * @param pPktHdr The lstat packet. + */ +static int txsDoLStat(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "path", &pszPath, &rc)) + return rc; + + RTFSOBJINFO Info; + rc = RTPathQueryInfoEx(pszPath, &Info, RTFSOBJATTRADD_UNIX, RTPATH_F_ON_LINK); + if (RT_SUCCESS(rc)) + rc = txsReplyObjInfo(&Info); + else + rc = txsReplyRC(pPktHdr, rc, "RTPathQueryInfoEx(\"%s\",,UNIX,ON_LINK)", pszPath); + + RTStrFree(pszPath); + return rc; +} + +/** + * Get info about a file system object, following all symbolic links. + * + * @returns IPRT status code from send. + * @param pPktHdr The stat packet. + */ +static int txsDoStat(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "path", &pszPath, &rc)) + return rc; + + RTFSOBJINFO Info; + rc = RTPathQueryInfoEx(pszPath, &Info, RTFSOBJATTRADD_UNIX, RTPATH_F_FOLLOW_LINK); + if (RT_SUCCESS(rc)) + rc = txsReplyObjInfo(&Info); + else + rc = txsReplyRC(pPktHdr, rc, "RTPathQueryInfoEx(\"%s\",,UNIX,FOLLOW_LINK)", pszPath); + + RTStrFree(pszPath); + return rc; +} + +/** + * Checks if the specified path is a symbolic link. + * + * @returns IPRT status code from send. + * @param pPktHdr The issymlnk packet. + */ +static int txsDoIsSymlnk(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "symlink", &pszPath, &rc)) + return rc; + + RTFSOBJINFO Info; + rc = RTPathQueryInfoEx(pszPath, &Info, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); + if (RT_SUCCESS(rc) && RTFS_IS_SYMLINK(Info.Attr.fMode)) + rc = txsReplySimple(pPktHdr, "TRUE "); + else + rc = txsReplySimple(pPktHdr, "FALSE "); + + RTStrFree(pszPath); + return rc; +} + +/** + * Checks if the specified path is a file or not. + * + * If the final path element is a symbolic link to a file, we'll return + * FALSE. + * + * @returns IPRT status code from send. + * @param pPktHdr The isfile packet. + */ +static int txsDoIsFile(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "dir", &pszPath, &rc)) + return rc; + + RTFSOBJINFO Info; + rc = RTPathQueryInfoEx(pszPath, &Info, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); + if (RT_SUCCESS(rc) && RTFS_IS_FILE(Info.Attr.fMode)) + rc = txsReplySimple(pPktHdr, "TRUE "); + else + rc = txsReplySimple(pPktHdr, "FALSE "); + + RTStrFree(pszPath); + return rc; +} + +/** + * Checks if the specified path is a directory or not. + * + * If the final path element is a symbolic link to a directory, we'll return + * FALSE. + * + * @returns IPRT status code from send. + * @param pPktHdr The isdir packet. + */ +static int txsDoIsDir(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "dir", &pszPath, &rc)) + return rc; + + RTFSOBJINFO Info; + rc = RTPathQueryInfoEx(pszPath, &Info, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); + if (RT_SUCCESS(rc) && RTFS_IS_DIRECTORY(Info.Attr.fMode)) + rc = txsReplySimple(pPktHdr, "TRUE "); + else + rc = txsReplySimple(pPktHdr, "FALSE "); + + RTStrFree(pszPath); + return rc; +} + +/** + * Changes the owner of a file, directory or symbolic link. + * + * @returns IPRT status code from send. + * @param pPktHdr The chmod packet. + */ +static int txsDoChOwn(PCTXSPKTHDR pPktHdr) +{ +#ifdef RT_OS_WINDOWS + return txsReplyNotImplemented(pPktHdr); +#else + /* After the packet header follows a 32-bit UID and 32-bit GID, while the + remainder of the packet is the zero terminated path. */ + size_t const cbMin = sizeof(TXSPKTHDR) + sizeof(RTFMODE) + 2; + if (pPktHdr->cb < cbMin) + return txsReplyBadMinSize(pPktHdr, cbMin); + + int rc; + char *pszPath; + if (!txsIsStringValid(pPktHdr, "path", (const char *)(pPktHdr + 1) + sizeof(uint32_t) * 2, &pszPath, NULL, &rc)) + return rc; + + uint32_t uid = ((uint32_t const *)(pPktHdr + 1))[0]; + uint32_t gid = ((uint32_t const *)(pPktHdr + 1))[1]; + + rc = RTPathSetOwnerEx(pszPath, uid, gid, RTPATH_F_ON_LINK); + + rc = txsReplyRC(pPktHdr, rc, "RTPathSetOwnerEx(\"%s\", %u, %u)", pszPath, uid, gid); + RTStrFree(pszPath); + return rc; +#endif +} + +/** + * Changes the mode of a file or directory. + * + * @returns IPRT status code from send. + * @param pPktHdr The chmod packet. + */ +static int txsDoChMod(PCTXSPKTHDR pPktHdr) +{ + /* After the packet header follows a mode mask and the remainder of + the packet is the zero terminated file name. */ + size_t const cbMin = sizeof(TXSPKTHDR) + sizeof(RTFMODE) + 2; + if (pPktHdr->cb < cbMin) + return txsReplyBadMinSize(pPktHdr, cbMin); + + int rc; + char *pszPath; + if (!txsIsStringValid(pPktHdr, "path", (const char *)(pPktHdr + 1) + sizeof(RTFMODE), &pszPath, NULL, &rc)) + return rc; + + RTFMODE fMode = *(RTFMODE const *)(pPktHdr + 1); + + rc = RTPathSetMode(pszPath, fMode); + + rc = txsReplyRC(pPktHdr, rc, "RTPathSetMode(\"%s\", %o)", pszPath, fMode); + RTStrFree(pszPath); + return rc; +} + +/** + * Removes a directory tree. + * + * @returns IPRT status code from send. + * @param pPktHdr The rmtree packet. + */ +static int txsDoRmTree(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "dir", &pszPath, &rc)) + return rc; + + rc = RTDirRemoveRecursive(pszPath, 0 /*fFlags*/); + + rc = txsReplyRC(pPktHdr, rc, "RTDirRemoveRecusive(\"%s\",0)", pszPath); + RTStrFree(pszPath); + return rc; +} + +/** + * Removes a symbolic link. + * + * @returns IPRT status code from send. + * @param pPktHdr The rmsymlink packet. + */ +static int txsDoRmSymlnk(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "symlink", &pszPath, &rc)) + return rc; + + rc = RTSymlinkDelete(pszPath, 0); + + rc = txsReplyRC(pPktHdr, rc, "RTSymlinkDelete(\"%s\")", pszPath); + RTStrFree(pszPath); + return rc; +} + +/** + * Removes a file. + * + * @returns IPRT status code from send. + * @param pPktHdr The rmfile packet. + */ +static int txsDoRmFile(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "file", &pszPath, &rc)) + return rc; + + rc = RTFileDelete(pszPath); + + rc = txsReplyRC(pPktHdr, rc, "RTFileDelete(\"%s\")", pszPath); + RTStrFree(pszPath); + return rc; +} + +/** + * Removes a directory. + * + * @returns IPRT status code from send. + * @param pPktHdr The rmdir packet. + */ +static int txsDoRmDir(PCTXSPKTHDR pPktHdr) +{ + int rc; + char *pszPath; + if (!txsIsStringPktValid(pPktHdr, "dir", &pszPath, &rc)) + return rc; + + rc = RTDirRemove(pszPath); + + rc = txsReplyRC(pPktHdr, rc, "RTDirRemove(\"%s\")", pszPath); + RTStrFree(pszPath); + return rc; +} + +/** + * Creates a symbolic link. + * + * @returns IPRT status code from send. + * @param pPktHdr The mksymlnk packet. + */ +static int txsDoMkSymlnk(PCTXSPKTHDR pPktHdr) +{ + return txsReplyNotImplemented(pPktHdr); +} + +/** + * Creates a directory and all its parents. + * + * @returns IPRT status code from send. + * @param pPktHdr The mkdir -p packet. + */ +static int txsDoMkDrPath(PCTXSPKTHDR pPktHdr) +{ + /* The same format as the MKDIR command. */ + if (pPktHdr->cb < sizeof(TXSPKTHDR) + sizeof(RTFMODE) + 2) + return txsReplyBadMinSize(pPktHdr, sizeof(TXSPKTHDR) + sizeof(RTFMODE) + 2); + + int rc; + char *pszPath; + if (!txsIsStringValid(pPktHdr, "dir", (const char *)(pPktHdr + 1) + sizeof(RTFMODE), &pszPath, NULL, &rc)) + return rc; + + RTFMODE fMode = *(RTFMODE const *)(pPktHdr + 1); + + rc = RTDirCreateFullPathEx(pszPath, fMode, RTDIRCREATE_FLAGS_IGNORE_UMASK); + + rc = txsReplyRC(pPktHdr, rc, "RTDirCreateFullPath(\"%s\", %#x)", pszPath, fMode); + RTStrFree(pszPath); + return rc; +} + +/** + * Creates a directory. + * + * @returns IPRT status code from send. + * @param pPktHdr The mkdir packet. + */ +static int txsDoMkDir(PCTXSPKTHDR pPktHdr) +{ + /* After the packet header follows a mode mask and the remainder of + the packet is the zero terminated directory name. */ + size_t const cbMin = sizeof(TXSPKTHDR) + sizeof(RTFMODE) + 2; + if (pPktHdr->cb < cbMin) + return txsReplyBadMinSize(pPktHdr, cbMin); + + int rc; + char *pszPath; + if (!txsIsStringValid(pPktHdr, "dir", (const char *)(pPktHdr + 1) + sizeof(RTFMODE), &pszPath, NULL, &rc)) + return rc; + + RTFMODE fMode = *(RTFMODE const *)(pPktHdr + 1); + rc = RTDirCreate(pszPath, fMode, RTDIRCREATE_FLAGS_IGNORE_UMASK); + + rc = txsReplyRC(pPktHdr, rc, "RTDirCreate(\"%s\", %#x)", pszPath, fMode); + RTStrFree(pszPath); + return rc; +} + +/** + * Cleans up the scratch area. + * + * @returns IPRT status code from send. + * @param pPktHdr The shutdown packet. + */ +static int txsDoCleanup(PCTXSPKTHDR pPktHdr) +{ + int rc = RTDirRemoveRecursive(g_szScratchPath, RTDIRRMREC_F_CONTENT_ONLY); + return txsReplyRC(pPktHdr, rc, "RTDirRemoveRecursive(\"%s\", CONTENT_ONLY)", g_szScratchPath); +} + +/** + * Ejects the specified DVD/CD drive. + * + * @returns IPRT status code from send. + * @param pPktHdr The eject packet. + */ +static int txsDoCdEject(PCTXSPKTHDR pPktHdr) +{ + /* After the packet header follows a uint32_t ordinal. */ + size_t const cbExpected = sizeof(TXSPKTHDR) + sizeof(uint32_t); + if (pPktHdr->cb != cbExpected) + return txsReplyBadSize(pPktHdr, cbExpected); + uint32_t iOrdinal = *(uint32_t const *)(pPktHdr + 1); + + RTCDROM hCdrom; + int rc = RTCdromOpenByOrdinal(iOrdinal, RTCDROM_O_CONTROL, &hCdrom); + if (RT_FAILURE(rc)) + return txsReplyRC(pPktHdr, rc, "RTCdromOpenByOrdinal(%u, RTCDROM_O_CONTROL, )", iOrdinal); + rc = RTCdromEject(hCdrom, true /*fForce*/); + RTCdromRelease(hCdrom); + + return txsReplyRC(pPktHdr, rc, "RTCdromEject(ord=%u, fForce=true)", iOrdinal); +} + +/** + * Common worker for txsDoShutdown and txsDoReboot. + * + * @returns IPRT status code from send. + * @param pPktHdr The reboot packet. + * @param fAction Which action to take. + */ +static int txsCommonShutdownReboot(PCTXSPKTHDR pPktHdr, uint32_t fAction) +{ + /* + * We ACK the reboot & shutdown before actually performing them, then we + * terminate the transport layer. + * + * This is to make sure the client isn't stuck with a dead connection. The + * transport layer termination also make sure we won't accept new + * connections in case the client is too eager to reconnect to a rebooted + * test victim. On the down side, we cannot easily report RTSystemShutdown + * failures failures this way. But the client can kind of figure it out by + * reconnecting and seeing that our UUID was unchanged. + */ + int rc; + if (pPktHdr->cb != sizeof(TXSPKTHDR)) + return txsReplyBadSize(pPktHdr, sizeof(TXSPKTHDR)); + g_pTransport->pfnNotifyReboot(); + rc = txsReplyAck(pPktHdr); + RTThreadSleep(2560); /* fudge factor */ + g_pTransport->pfnTerm(); + + /* + * Do the job, if it fails we'll restart the transport layer. + */ +#if 0 + rc = VINF_SUCCESS; +#else + rc = RTSystemShutdown(0 /*cMsDelay*/, + fAction | RTSYSTEM_SHUTDOWN_PLANNED | RTSYSTEM_SHUTDOWN_FORCE, + "Test Execution Service"); +#endif + if (RT_SUCCESS(rc)) + { + RTMsgInfo(fAction == RTSYSTEM_SHUTDOWN_REBOOT ? "Rebooting...\n" : "Shutting down...\n"); + g_fTerminate = true; + } + else + { + RTMsgError("RTSystemShutdown w/ fAction=%#x failed: %Rrc", fAction, rc); + + int rc2 = g_pTransport->pfnInit(); + if (RT_FAILURE(rc2)) + { + g_fTerminate = true; + rc = rc2; + } + } + return rc; +} + +/** + * Shuts down the machine, powering it off if possible. + * + * @returns IPRT status code from send. + * @param pPktHdr The shutdown packet. + */ +static int txsDoShutdown(PCTXSPKTHDR pPktHdr) +{ + return txsCommonShutdownReboot(pPktHdr, RTSYSTEM_SHUTDOWN_POWER_OFF_HALT); +} + +/** + * Reboots the machine. + * + * @returns IPRT status code from send. + * @param pPktHdr The reboot packet. + */ +static int txsDoReboot(PCTXSPKTHDR pPktHdr) +{ + return txsCommonShutdownReboot(pPktHdr, RTSYSTEM_SHUTDOWN_REBOOT); +} + +/** + * Verifies and acknowledges a "UUID" request. + * + * @returns IPRT status code. + * @param pPktHdr The UUID packet. + */ +static int txsDoUuid(PCTXSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(TXSPKTHDR)) + return txsReplyBadSize(pPktHdr, sizeof(TXSPKTHDR)); + + struct + { + TXSPKTHDR Hdr; + char szUuid[RTUUID_STR_LENGTH]; + char abPadding[TXSPKT_ALIGNMENT]; + } Pkt; + + int rc = RTUuidToStr(&g_InstanceUuid, Pkt.szUuid, sizeof(Pkt.szUuid)); + if (RT_FAILURE(rc)) + return txsReplyRC(pPktHdr, rc, "RTUuidToStr"); + return txsReplyInternal(&Pkt.Hdr, "ACK UUID", strlen(Pkt.szUuid) + 1); +} + +/** + * Verifies and acknowledges a "BYE" request. + * + * @returns IPRT status code. + * @param pPktHdr The bye packet. + */ +static int txsDoBye(PCTXSPKTHDR pPktHdr) +{ + int rc; + if (pPktHdr->cb == sizeof(TXSPKTHDR)) + rc = txsReplyAck(pPktHdr); + else + rc = txsReplyBadSize(pPktHdr, sizeof(TXSPKTHDR)); + g_pTransport->pfnNotifyBye(); + return rc; +} + +/** + * Verifies and acknowledges a "VER" request. + * + * @returns IPRT status code. + * @param pPktHdr The version packet. + */ +static int txsDoVer(PCTXSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(TXSPKTHDR)) + return txsReplyBadSize(pPktHdr, sizeof(TXSPKTHDR)); + + struct + { + TXSPKTHDR Hdr; + char szVer[96]; + char abPadding[TXSPKT_ALIGNMENT]; + } Pkt; + + if (RTStrPrintf2(Pkt.szVer, sizeof(Pkt.szVer), "%s r%s %s.%s (%s %s)", + RTBldCfgVersion(), RTBldCfgRevisionStr(), KBUILD_TARGET, KBUILD_TARGET_ARCH, __DATE__, __TIME__) > 0) + { + return txsReplyInternal(&Pkt.Hdr, "ACK VER ", strlen(Pkt.szVer) + 1); + } + + return txsReplyRC(pPktHdr, VERR_BUFFER_OVERFLOW, "RTStrPrintf2"); +} + +/** + * Verifies and acknowledges a "HOWDY" request. + * + * @returns IPRT status code. + * @param pPktHdr The howdy packet. + */ +static int txsDoHowdy(PCTXSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(TXSPKTHDR)) + return txsReplyBadSize(pPktHdr, sizeof(TXSPKTHDR)); + int rc = txsReplyAck(pPktHdr); + if (RT_SUCCESS(rc)) + { + g_pTransport->pfnNotifyHowdy(); + RTDirRemoveRecursive(g_szScratchPath, RTDIRRMREC_F_CONTENT_ONLY); + } + return rc; +} + +/** + * Replies according to the return code. + * + * @returns rcOperation and pTxsExec->rcReplySend. + * @param pTxsExec The TXSEXEC instance. + * @param rcOperation The status code to report. + * @param pszOperationFmt The operation that failed. Typically giving the + * function call with important arguments. + * @param ... Arguments to the format string. + */ +static int txsExecReplyRC(PTXSEXEC pTxsExec, int rcOperation, const char *pszOperationFmt, ...) +{ + AssertStmt(RT_FAILURE_NP(rcOperation), rcOperation = VERR_IPE_UNEXPECTED_INFO_STATUS); + + char szOperation[128]; + va_list va; + va_start(va, pszOperationFmt); + RTStrPrintfV(szOperation, sizeof(szOperation), pszOperationFmt, va); + va_end(va); + + pTxsExec->rcReplySend = txsReplyFailure(pTxsExec->pPktHdr, "FAILED ", + "%s failed with rc=%Rrc (opcode '%.8s')", + szOperation, rcOperation, pTxsExec->pPktHdr->achOpcode); + return rcOperation; +} + + +/** + * Sends the process exit status reply to the TXS client. + * + * @returns IPRT status code of the send. + * @param pTxsExec The TXSEXEC instance. + * @param fProcessAlive Whether the process is still alive (against our + * will). + * @param fProcessTimedOut Whether the process timed out. + * @param MsProcessKilled When the process was killed, UINT64_MAX if not. + */ +static int txsExecSendExitStatus(PTXSEXEC pTxsExec, bool fProcessAlive, bool fProcessTimedOut, uint64_t MsProcessKilled) +{ + int rc; + if ( fProcessTimedOut && !fProcessAlive && MsProcessKilled != UINT64_MAX) + { + rc = txsReplySimple(pTxsExec->pPktHdr, "PROC TOK"); + if (g_fDisplayOutput) + RTPrintf("txs: Process timed out and was killed\n"); + } + else if (fProcessTimedOut && fProcessAlive && MsProcessKilled != UINT64_MAX) + { + rc = txsReplySimple(pTxsExec->pPktHdr, "PROC TOA"); + if (g_fDisplayOutput) + RTPrintf("txs: Process timed out and was not killed successfully\n"); + } + else if (g_fTerminate && (fProcessAlive || MsProcessKilled != UINT64_MAX)) + rc = txsReplySimple(pTxsExec->pPktHdr, "PROC DWN"); + else if (fProcessAlive) + { + rc = txsReplyFailure(pTxsExec->pPktHdr, "PROC DOO", "Doofus! process is alive when it should not"); + AssertFailed(); + } + else if (MsProcessKilled != UINT64_MAX) + { + rc = txsReplyFailure(pTxsExec->pPktHdr, "PROC DOO", "Doofus! process has been killed when it should not"); + AssertFailed(); + } + else if ( pTxsExec->ProcessStatus.enmReason == RTPROCEXITREASON_NORMAL + && pTxsExec->ProcessStatus.iStatus == 0) + { + rc = txsReplySimple(pTxsExec->pPktHdr, "PROC OK "); + if (g_fDisplayOutput) + RTPrintf("txs: Process exited with status: 0\n"); + } + else if (pTxsExec->ProcessStatus.enmReason == RTPROCEXITREASON_NORMAL) + { + rc = txsReplyFailure(pTxsExec->pPktHdr, "PROC NOK", "%d", pTxsExec->ProcessStatus.iStatus); + if (g_fDisplayOutput) + RTPrintf("txs: Process exited with status: %d\n", pTxsExec->ProcessStatus.iStatus); + } + else if (pTxsExec->ProcessStatus.enmReason == RTPROCEXITREASON_SIGNAL) + { + rc = txsReplyFailure(pTxsExec->pPktHdr, "PROC SIG", "%d", pTxsExec->ProcessStatus.iStatus); + if (g_fDisplayOutput) + RTPrintf("txs: Process exited with status: signal %d\n", pTxsExec->ProcessStatus.iStatus); + } + else if (pTxsExec->ProcessStatus.enmReason == RTPROCEXITREASON_ABEND) + { + rc = txsReplyFailure(pTxsExec->pPktHdr, "PROC ABD", ""); + if (g_fDisplayOutput) + RTPrintf("txs: Process exited with status: abend\n"); + } + else + { + rc = txsReplyFailure(pTxsExec->pPktHdr, "PROC DOO", "enmReason=%d iStatus=%d", + pTxsExec->ProcessStatus.enmReason, pTxsExec->ProcessStatus.iStatus); + AssertMsgFailed(("enmReason=%d iStatus=%d", pTxsExec->ProcessStatus.enmReason, pTxsExec->ProcessStatus.iStatus)); + } + return rc; +} + +/** + * Handle pending output data or error on standard out, standard error or the + * test pipe. + * + * @returns IPRT status code from client send. + * @param hPollSet The polling set. + * @param fPollEvt The event mask returned by RTPollNoResume. + * @param phPipeR The pipe handle. + * @param puCrc32 The current CRC-32 of the stream. (In/Out) + * @param enmHndId The handle ID. + * @param pszOpcode The opcode for the data upload. + * + * @todo Put the last 4 parameters into a struct! + */ +static int txsDoExecHlpHandleOutputEvent(RTPOLLSET hPollSet, uint32_t fPollEvt, PRTPIPE phPipeR, + uint32_t *puCrc32, TXSEXECHNDID enmHndId, const char *pszOpcode) +{ + Log(("txsDoExecHlpHandleOutputEvent: %s fPollEvt=%#x\n", pszOpcode, fPollEvt)); + + /* + * Try drain the pipe before acting on any errors. + */ + int rc = VINF_SUCCESS; + struct + { + TXSPKTHDR Hdr; + uint32_t uCrc32; + char abBuf[_64K]; + char abPadding[TXSPKT_ALIGNMENT]; + } Pkt; + size_t cbRead; + int rc2 = RTPipeRead(*phPipeR, Pkt.abBuf, sizeof(Pkt.abBuf), &cbRead); + if (RT_SUCCESS(rc2) && cbRead) + { + Log(("Crc32=%#x ", *puCrc32)); + *puCrc32 = RTCrc32Process(*puCrc32, Pkt.abBuf, cbRead); + Log(("cbRead=%#x Crc32=%#x \n", cbRead, *puCrc32)); + Pkt.uCrc32 = RTCrc32Finish(*puCrc32); + if (g_fDisplayOutput) + { + if (enmHndId == TXSEXECHNDID_STDOUT) + RTStrmPrintf(g_pStdErr, "%.*s", cbRead, Pkt.abBuf); + else if (enmHndId == TXSEXECHNDID_STDERR) + RTStrmPrintf(g_pStdErr, "%.*s", cbRead, Pkt.abBuf); + } + + rc = txsReplyInternal(&Pkt.Hdr, pszOpcode, cbRead + sizeof(uint32_t)); + + /* Make sure we go another poll round in case there was too much data + for the buffer to hold. */ + fPollEvt &= RTPOLL_EVT_ERROR; + } + else if (RT_FAILURE(rc2)) + { + fPollEvt |= RTPOLL_EVT_ERROR; + AssertMsg(rc2 == VERR_BROKEN_PIPE, ("%Rrc\n", rc)); + } + + /* + * If an error was raised signalled, + */ + if (fPollEvt & RTPOLL_EVT_ERROR) + { + rc2 = RTPollSetRemove(hPollSet, enmHndId); + AssertRC(rc2); + + rc2 = RTPipeClose(*phPipeR); + AssertRC(rc2); + *phPipeR = NIL_RTPIPE; + } + return rc; +} + +/** + * Try write some more data to the standard input of the child. + * + * @returns IPRT status code. + * @param pStdInBuf The standard input buffer. + * @param hStdInW The standard input pipe. + */ +static int txsDoExecHlpWriteStdIn(PTXSEXECSTDINBUF pStdInBuf, RTPIPE hStdInW) +{ + size_t cbToWrite = pStdInBuf->cb - pStdInBuf->off; + size_t cbWritten; + int rc = RTPipeWrite(hStdInW, &pStdInBuf->pch[pStdInBuf->off], cbToWrite, &cbWritten); + if (RT_SUCCESS(rc)) + { + Assert(cbWritten == cbToWrite); + pStdInBuf->off += cbWritten; + } + return rc; +} + +/** + * Handle an error event on standard input. + * + * @param hPollSet The polling set. + * @param fPollEvt The event mask returned by RTPollNoResume. + * @param phStdInW The standard input pipe handle. + * @param pStdInBuf The standard input buffer. + */ +static void txsDoExecHlpHandleStdInErrorEvent(RTPOLLSET hPollSet, uint32_t fPollEvt, PRTPIPE phStdInW, + PTXSEXECSTDINBUF pStdInBuf) +{ + NOREF(fPollEvt); + int rc2; + if (pStdInBuf->off < pStdInBuf->cb) + { + rc2 = RTPollSetRemove(hPollSet, TXSEXECHNDID_STDIN_WRITABLE); + AssertRC(rc2); + } + + rc2 = RTPollSetRemove(hPollSet, TXSEXECHNDID_STDIN); + AssertRC(rc2); + + rc2 = RTPipeClose(*phStdInW); + AssertRC(rc2); + *phStdInW = NIL_RTPIPE; + + RTMemFree(pStdInBuf->pch); + pStdInBuf->pch = NULL; + pStdInBuf->off = 0; + pStdInBuf->cb = 0; + pStdInBuf->cbAllocated = 0; + pStdInBuf->fBitBucket = true; +} + +/** + * Handle an event indicating we can write to the standard input pipe of the + * child process. + * + * @param hPollSet The polling set. + * @param fPollEvt The event mask returned by RTPollNoResume. + * @param phStdInW The standard input pipe. + * @param pStdInBuf The standard input buffer. + */ +static void txsDoExecHlpHandleStdInWritableEvent(RTPOLLSET hPollSet, uint32_t fPollEvt, PRTPIPE phStdInW, + PTXSEXECSTDINBUF pStdInBuf) +{ + int rc; + if (!(fPollEvt & RTPOLL_EVT_ERROR)) + { + rc = txsDoExecHlpWriteStdIn(pStdInBuf, *phStdInW); + if (RT_FAILURE(rc) && rc != VERR_BAD_PIPE) + { + /** @todo do we need to do something about this error condition? */ + AssertRC(rc); + } + + if (pStdInBuf->off < pStdInBuf->cb) + { + rc = RTPollSetRemove(hPollSet, TXSEXECHNDID_STDIN_WRITABLE); + AssertRC(rc); + } + } + else + txsDoExecHlpHandleStdInErrorEvent(hPollSet, fPollEvt, phStdInW, pStdInBuf); +} + +/** + * Handle a transport event or successful pfnPollIn() call. + * + * @returns IPRT status code from client send. + * @retval VINF_EOF indicates ABORT command. + * + * @param hPollSet The polling set. + * @param fPollEvt The event mask returned by RTPollNoResume. + * @param idPollHnd The handle ID. + * @param phStdInW The standard input pipe. + * @param pStdInBuf The standard input buffer. + */ +static int txsDoExecHlpHandleTransportEvent(RTPOLLSET hPollSet, uint32_t fPollEvt, uint32_t idPollHnd, + PRTPIPE phStdInW, PTXSEXECSTDINBUF pStdInBuf) +{ + /* ASSUMES the transport layer will detect or clear any error condition. */ + NOREF(fPollEvt); NOREF(idPollHnd); + Log(("txsDoExecHlpHandleTransportEvent\n")); + /** @todo Use a callback for this case? */ + + /* + * Read client command packet and process it. + */ + /** @todo Sometimes this hangs on windows because there isn't any data pending. + * We probably get woken up at the wrong time or in the wrong way, i.e. RTPoll() + * is busted for sockets. + * + * Temporary workaround: Poll for input before trying to read it. */ + if (!g_pTransport->pfnPollIn()) + { + Log(("Bad transport event\n")); + RTThreadYield(); + return VINF_SUCCESS; + } + PTXSPKTHDR pPktHdr; + int rc = txsRecvPkt(&pPktHdr, false /*fAutoRetryOnFailure*/); + if (RT_FAILURE(rc)) + return rc; + Log(("Bad transport event\n")); + + /* + * The most common thing here would be a STDIN request with data + * for the child process. + */ + if (txsIsSameOpcode(pPktHdr, "STDIN")) + { + if ( !pStdInBuf->fBitBucket + && pPktHdr->cb >= sizeof(TXSPKTHDR) + sizeof(uint32_t)) + { + uint32_t uCrc32 = *(uint32_t *)(pPktHdr + 1); + const char *pch = (const char *)(pPktHdr + 1) + sizeof(uint32_t); + size_t cb = pPktHdr->cb - sizeof(TXSPKTHDR) - sizeof(uint32_t); + + /* Check the CRC */ + pStdInBuf->uCrc32 = RTCrc32Process(pStdInBuf->uCrc32, pch, cb); + if (RTCrc32Finish(pStdInBuf->uCrc32) == uCrc32) + { + + /* Rewind the buffer if it's empty. */ + size_t cbInBuf = pStdInBuf->cb - pStdInBuf->off; + bool const fAddToSet = cbInBuf == 0; + if (fAddToSet) + pStdInBuf->cb = pStdInBuf->off = 0; + + /* Try and see if we can simply append the data. */ + if (cb + pStdInBuf->cb <= pStdInBuf->cbAllocated) + { + memcpy(&pStdInBuf->pch[pStdInBuf->cb], pch, cb); + pStdInBuf->cb += cb; + rc = txsReplyAck(pPktHdr); + } + else + { + /* Try write a bit or two before we move+realloc the buffer. */ + if (cbInBuf > 0) + txsDoExecHlpWriteStdIn(pStdInBuf, *phStdInW); + + /* Move any buffered data to the front. */ + cbInBuf = pStdInBuf->cb - pStdInBuf->off; + if (cbInBuf == 0) + pStdInBuf->cb = pStdInBuf->off = 0; + else + { + memmove(pStdInBuf->pch, &pStdInBuf->pch[pStdInBuf->off], cbInBuf); + pStdInBuf->cb = cbInBuf; + pStdInBuf->off = 0; + } + + /* Do we need to grow the buffer? */ + if (cb + pStdInBuf->cb > pStdInBuf->cbAllocated) + { + size_t cbAlloc = pStdInBuf->cb + cb; + cbAlloc = RT_ALIGN_Z(cbAlloc, _64K); + void *pvNew = RTMemRealloc(pStdInBuf->pch, cbAlloc); + if (pvNew) + { + pStdInBuf->pch = (char *)pvNew; + pStdInBuf->cbAllocated = cbAlloc; + } + } + + /* Finally, copy the data. */ + if (cb + pStdInBuf->cb <= pStdInBuf->cbAllocated) + { + memcpy(&pStdInBuf->pch[pStdInBuf->cb], pch, cb); + pStdInBuf->cb += cb; + rc = txsReplyAck(pPktHdr); + } + else + rc = txsReplySimple(pPktHdr, "STDINMEM"); + } + + /* + * Flush the buffered data and add/remove the standard input + * handle from the set. + */ + txsDoExecHlpWriteStdIn(pStdInBuf, *phStdInW); + if (fAddToSet && pStdInBuf->off < pStdInBuf->cb) + { + int rc2 = RTPollSetAddPipe(hPollSet, *phStdInW, RTPOLL_EVT_WRITE, TXSEXECHNDID_STDIN_WRITABLE); + AssertRC(rc2); + } + else if (!fAddToSet && pStdInBuf->off >= pStdInBuf->cb) + { + int rc2 = RTPollSetRemove(hPollSet, TXSEXECHNDID_STDIN_WRITABLE); + AssertRC(rc2); + } + } + else + rc = txsReplyFailure(pPktHdr, "STDINCRC", "Invalid CRC checksum expected %#x got %#x", + pStdInBuf->uCrc32, uCrc32); + } + else if (pPktHdr->cb < sizeof(TXSPKTHDR) + sizeof(uint32_t)) + rc = txsReplySimple(pPktHdr, "STDINBAD"); + else + rc = txsReplySimple(pPktHdr, "STDINIGN"); + } + /* + * Marks the end of the stream for stdin. + */ + else if (txsIsSameOpcode(pPktHdr, "STDINEOS")) + { + if (RT_LIKELY(pPktHdr->cb == sizeof(TXSPKTHDR))) + { + /* Close the pipe. */ + txsDoExecHlpHandleStdInErrorEvent(hPollSet, fPollEvt, phStdInW, pStdInBuf); + rc = txsReplyAck(pPktHdr); + } + else + rc = txsReplySimple(pPktHdr, "STDINBAD"); + } + /* + * The only other two requests are connection oriented and we return a error + * code so that we unwind the whole EXEC shebang and start afresh. + */ + else if (txsIsSameOpcode(pPktHdr, "BYE")) + { + rc = txsDoBye(pPktHdr); + if (RT_SUCCESS(rc)) + rc = VERR_NET_NOT_CONNECTED; + } + else if (txsIsSameOpcode(pPktHdr, "HOWDY")) + { + rc = txsDoHowdy(pPktHdr); + if (RT_SUCCESS(rc)) + rc = VERR_NET_NOT_CONNECTED; + } + else if (txsIsSameOpcode(pPktHdr, "ABORT")) + { + rc = txsReplyAck(pPktHdr); + if (RT_SUCCESS(rc)) + rc = VINF_EOF; /* this is but ugly! */ + } + else + rc = txsReplyFailure(pPktHdr, "UNKNOWN ", "Opcode '%.8s' is not known or not recognized during EXEC", pPktHdr->achOpcode); + + RTMemFree(pPktHdr); + return rc; +} + +/** + * Handles the output and input of the process, waits for it finish up. + * + * @returns IPRT status code from reply send. + * @param pTxsExec The TXSEXEC instance. + */ +static int txsDoExecHlp2(PTXSEXEC pTxsExec) +{ + int rc; /* client send. */ + int rc2; + TXSEXECSTDINBUF StdInBuf = { 0, 0, NULL, 0, pTxsExec->hStdInW == NIL_RTPIPE, RTCrc32Start() }; + uint32_t uStdOutCrc32 = RTCrc32Start(); + uint32_t uStdErrCrc32 = uStdOutCrc32; + uint32_t uTestPipeCrc32 = uStdOutCrc32; + uint64_t const MsStart = RTTimeMilliTS(); + bool fProcessTimedOut = false; + uint64_t MsProcessKilled = UINT64_MAX; + RTMSINTERVAL const cMsPollBase = g_pTransport->pfnPollSetAdd || pTxsExec->hStdInW == NIL_RTPIPE + ? RT_MS_5SEC : 100; + RTMSINTERVAL cMsPollCur = 0; + + /* + * Before entering the loop, tell the client that we've started the guest + * and that it's now OK to send input to the process. (This is not the + * final ACK, so the packet header is NULL ... kind of bogus.) + */ + rc = txsReplyAck(NULL); + + /* + * Process input, output, the test pipe and client requests. + */ + while ( RT_SUCCESS(rc) + && RT_UNLIKELY(!g_fTerminate)) + { + /* + * Wait/Process all pending events. + */ + uint32_t idPollHnd; + uint32_t fPollEvt; + Log3(("Calling RTPollNoResume(,%u,)...\n", cMsPollCur)); + rc2 = RTPollNoResume(pTxsExec->hPollSet, cMsPollCur, &fPollEvt, &idPollHnd); + Log3(("RTPollNoResume -> fPollEvt=%#x idPollHnd=%u\n", fPollEvt, idPollHnd)); + if (g_fTerminate) + continue; + cMsPollCur = 0; /* no rest until we've checked everything. */ + + if (RT_SUCCESS(rc2)) + { + switch (idPollHnd) + { + case TXSEXECHNDID_STDOUT: + rc = txsDoExecHlpHandleOutputEvent(pTxsExec->hPollSet, fPollEvt, &pTxsExec->hStdOutR, &uStdOutCrc32, + TXSEXECHNDID_STDOUT, "STDOUT "); + break; + + case TXSEXECHNDID_STDERR: + rc = txsDoExecHlpHandleOutputEvent(pTxsExec->hPollSet, fPollEvt, &pTxsExec->hStdErrR, &uStdErrCrc32, + TXSEXECHNDID_STDERR, "STDERR "); + break; + + case TXSEXECHNDID_TESTPIPE: + rc = txsDoExecHlpHandleOutputEvent(pTxsExec->hPollSet, fPollEvt, &pTxsExec->hTestPipeR, &uTestPipeCrc32, + TXSEXECHNDID_TESTPIPE, "TESTPIPE"); + break; + + case TXSEXECHNDID_STDIN: + txsDoExecHlpHandleStdInErrorEvent(pTxsExec->hPollSet, fPollEvt, &pTxsExec->hStdInW, &StdInBuf); + break; + + case TXSEXECHNDID_STDIN_WRITABLE: + txsDoExecHlpHandleStdInWritableEvent(pTxsExec->hPollSet, fPollEvt, &pTxsExec->hStdInW, &StdInBuf); + break; + + case TXSEXECHNDID_THREAD: + rc2 = RTPollSetRemove(pTxsExec->hPollSet, TXSEXECHNDID_THREAD); AssertRC(rc2); + break; + + default: + rc = txsDoExecHlpHandleTransportEvent(pTxsExec->hPollSet, fPollEvt, idPollHnd, &pTxsExec->hStdInW, + &StdInBuf); + break; + } + if (RT_FAILURE(rc) || rc == VINF_EOF) + break; /* abort command, or client dead or something */ + continue; + } + + /* + * Check for incoming data. + */ + if (g_pTransport->pfnPollIn()) + { + rc = txsDoExecHlpHandleTransportEvent(pTxsExec->hPollSet, 0, UINT32_MAX, &pTxsExec->hStdInW, &StdInBuf); + if (RT_FAILURE(rc) || rc == VINF_EOF) + break; /* abort command, or client dead or something */ + continue; + } + + /* + * If the process has terminated, we're should head out. + */ + if (!ASMAtomicReadBool(&pTxsExec->fProcessAlive)) + break; + + /* + * Check for timed out, killing the process. + */ + uint32_t cMilliesLeft = RT_INDEFINITE_WAIT; + if (pTxsExec->cMsTimeout != RT_INDEFINITE_WAIT) + { + uint64_t u64Now = RTTimeMilliTS(); + uint64_t cMsElapsed = u64Now - MsStart; + if (cMsElapsed >= pTxsExec->cMsTimeout) + { + fProcessTimedOut = true; + if ( MsProcessKilled == UINT64_MAX + || u64Now - MsProcessKilled > RT_MS_1SEC) + { + if ( MsProcessKilled != UINT64_MAX + && u64Now - MsProcessKilled > 20*RT_MS_1MIN) + break; /* give up after 20 mins */ + RTCritSectEnter(&pTxsExec->CritSect); + if (pTxsExec->fProcessAlive) + RTProcTerminate(pTxsExec->hProcess); + RTCritSectLeave(&pTxsExec->CritSect); + MsProcessKilled = u64Now; + continue; + } + cMilliesLeft = RT_MS_10SEC; + } + else + cMilliesLeft = pTxsExec->cMsTimeout - (uint32_t)cMsElapsed; + } + + /* Reset the polling interval since we've done all pending work. */ + cMsPollCur = cMilliesLeft >= cMsPollBase ? cMsPollBase : cMilliesLeft; + } + + /* + * At this point we should hopefully only have to wait 0 ms on the thread + * to release the handle... But if for instance the process refuses to die, + * we'll have to try kill it again. Bothersome. + */ + for (size_t i = 0; i < 22; i++) + { + rc2 = RTThreadWait(pTxsExec->hThreadWaiter, RT_MS_1SEC / 2, NULL); + if (RT_SUCCESS(rc)) + { + pTxsExec->hThreadWaiter = NIL_RTTHREAD; + Assert(!pTxsExec->fProcessAlive); + break; + } + if (i == 0 || i == 10 || i == 15 || i == 18 || i > 20) + { + RTCritSectEnter(&pTxsExec->CritSect); + if (pTxsExec->fProcessAlive) + RTProcTerminate(pTxsExec->hProcess); + RTCritSectLeave(&pTxsExec->CritSect); + } + } + + /* + * If we don't have a client problem (RT_FAILURE(rc) we'll reply to the + * clients exec packet now. + */ + if (RT_SUCCESS(rc)) + rc = txsExecSendExitStatus(pTxsExec, pTxsExec->fProcessAlive, fProcessTimedOut, MsProcessKilled); + + RTMemFree(StdInBuf.pch); + return rc; +} + +/** + * Creates a poll set for the pipes and let the transport layer add stuff to it + * as well. + * + * @returns IPRT status code, reply to client made on error. + * @param pTxsExec The TXSEXEC instance. + */ +static int txsExecSetupPollSet(PTXSEXEC pTxsExec) +{ + int rc = RTPollSetCreate(&pTxsExec->hPollSet); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTPollSetCreate"); + + rc = RTPollSetAddPipe(pTxsExec->hPollSet, pTxsExec->hStdInW, RTPOLL_EVT_ERROR, TXSEXECHNDID_STDIN); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTPollSetAddPipe/stdin"); + + rc = RTPollSetAddPipe(pTxsExec->hPollSet, pTxsExec->hStdOutR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, + TXSEXECHNDID_STDOUT); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTPollSetAddPipe/stdout"); + + rc = RTPollSetAddPipe(pTxsExec->hPollSet, pTxsExec->hStdErrR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, + TXSEXECHNDID_STDERR); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTPollSetAddPipe/stderr"); + + rc = RTPollSetAddPipe(pTxsExec->hPollSet, pTxsExec->hTestPipeR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, + TXSEXECHNDID_TESTPIPE); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTPollSetAddPipe/test"); + + rc = RTPollSetAddPipe(pTxsExec->hPollSet, pTxsExec->hWakeUpPipeR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, + TXSEXECHNDID_THREAD); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTPollSetAddPipe/wakeup"); + + if (g_pTransport->pfnPollSetAdd) + { + rc = g_pTransport->pfnPollSetAdd(pTxsExec->hPollSet, TXSEXECHNDID_TRANSPORT); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "%s->pfnPollSetAdd/stdin", g_pTransport->szName); + } + + return VINF_SUCCESS; +} + +/** + * Thread that calls RTProcWait and signals the main thread when it returns. + * + * The thread is created before the process is started and is waiting for a user + * signal from the main thread before it calls RTProcWait. + * + * @returns VINF_SUCCESS (ignored). + * @param hThreadSelf The thread handle. + * @param pvUser The TXEEXEC structure. + */ +static DECLCALLBACK(int) txsExecWaitThreadProc(RTTHREAD hThreadSelf, void *pvUser) +{ + PTXSEXEC pTxsExec = (PTXSEXEC)pvUser; + + /* Wait for the go ahead... */ + int rc = RTThreadUserWait(hThreadSelf, RT_INDEFINITE_WAIT); AssertRC(rc); + + RTCritSectEnter(&pTxsExec->CritSect); + for (;;) + { + RTCritSectLeave(&pTxsExec->CritSect); + rc = RTProcWaitNoResume(pTxsExec->hProcess, RTPROCWAIT_FLAGS_BLOCK, &pTxsExec->ProcessStatus); + RTCritSectEnter(&pTxsExec->CritSect); + + /* If the pipe is NIL, the destructor wants us to get lost ASAP. */ + if (pTxsExec->hWakeUpPipeW == NIL_RTPIPE) + break; + + if (RT_FAILURE(rc)) + { + rc = RTProcWait(pTxsExec->hProcess, RTPROCWAIT_FLAGS_NOBLOCK, &pTxsExec->ProcessStatus); + if (rc == VERR_PROCESS_RUNNING) + continue; + + if (RT_FAILURE(rc)) + { + AssertRC(rc); + pTxsExec->ProcessStatus.iStatus = rc; + pTxsExec->ProcessStatus.enmReason = RTPROCEXITREASON_ABEND; + } + } + + /* The process finished, signal the main thread over the pipe. */ + ASMAtomicWriteBool(&pTxsExec->fProcessAlive, false); + size_t cbIgnored; + RTPipeWrite(pTxsExec->hWakeUpPipeW, "done", 4, &cbIgnored); + RTPipeClose(pTxsExec->hWakeUpPipeW); + pTxsExec->hWakeUpPipeW = NIL_RTPIPE; + break; + } + RTCritSectLeave(&pTxsExec->CritSect); + + return VINF_SUCCESS; +} + +/** + * Sets up the thread that waits for the process to complete. + * + * @returns IPRT status code, reply to client made on error. + * @param pTxsExec The TXSEXEC instance. + */ +static int txsExecSetupThread(PTXSEXEC pTxsExec) +{ + int rc = RTPipeCreate(&pTxsExec->hWakeUpPipeR, &pTxsExec->hWakeUpPipeW, 0 /*fFlags*/); + if (RT_FAILURE(rc)) + { + pTxsExec->hWakeUpPipeR = pTxsExec->hWakeUpPipeW = NIL_RTPIPE; + return txsExecReplyRC(pTxsExec, rc, "RTPipeCreate/wait"); + } + + rc = RTThreadCreate(&pTxsExec->hThreadWaiter, txsExecWaitThreadProc, + pTxsExec, 0 /*cbStack */, RTTHREADTYPE_DEFAULT, + RTTHREADFLAGS_WAITABLE, "TxsProcW"); + if (RT_FAILURE(rc)) + { + pTxsExec->hThreadWaiter = NIL_RTTHREAD; + return txsExecReplyRC(pTxsExec, rc, "RTThreadCreate"); + } + + return VINF_SUCCESS; +} + +/** + * Sets up the test pipe. + * + * @returns IPRT status code, reply to client made on error. + * @param pTxsExec The TXSEXEC instance. + * @param pszTestPipe How to set up the test pipe. + */ +static int txsExecSetupTestPipe(PTXSEXEC pTxsExec, const char *pszTestPipe) +{ + if (strcmp(pszTestPipe, "|")) + return VINF_SUCCESS; + + int rc = RTPipeCreate(&pTxsExec->hTestPipeR, &pTxsExec->hTestPipeW, RTPIPE_C_INHERIT_WRITE); + if (RT_FAILURE(rc)) + { + pTxsExec->hTestPipeR = pTxsExec->hTestPipeW = NIL_RTPIPE; + return txsExecReplyRC(pTxsExec, rc, "RTPipeCreate/test/%s", pszTestPipe); + } + + char szVal[64]; + RTStrPrintf(szVal, sizeof(szVal), "%#llx", (uint64_t)RTPipeToNative(pTxsExec->hTestPipeW)); + rc = RTEnvSetEx(pTxsExec->hEnv, "IPRT_TEST_PIPE", szVal); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTEnvSetEx/test/%s", pszTestPipe); + + return VINF_SUCCESS; +} + +/** + * Sets up the redirection / pipe / nothing for one of the standard handles. + * + * @returns IPRT status code, reply to client made on error. + * @param pTxsExec The TXSEXEC instance. + * @param pszHowTo How to set up this standard handle. + * @param pszStdWhat For what to setup redirection (stdin/stdout/stderr). + * @param fd Which standard handle it is (0 == stdin, 1 == + * stdout, 2 == stderr). + * @param ph The generic handle that @a pph may be set + * pointing to. Always set. + * @param pph Pointer to the RTProcCreateExec argument. + * Always set. + * @param phPipe Where to return the end of the pipe that we + * should service. Always set. + */ +static int txsExecSetupRedir(PTXSEXEC pTxsExec, const char *pszHowTo, const char *pszStdWhat, int fd, PRTHANDLE ph, PRTHANDLE *pph, PRTPIPE phPipe) +{ + ph->enmType = RTHANDLETYPE_PIPE; + ph->u.hPipe = NIL_RTPIPE; + *pph = NULL; + *phPipe = NIL_RTPIPE; + + int rc; + if (!strcmp(pszHowTo, "|")) + { + /* + * Setup a pipe for forwarding to/from the client. + */ + if (fd == 0) + rc = RTPipeCreate(&ph->u.hPipe, phPipe, RTPIPE_C_INHERIT_READ); + else + rc = RTPipeCreate(phPipe, &ph->u.hPipe, RTPIPE_C_INHERIT_WRITE); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTPipeCreate/%s/%s", pszStdWhat, pszHowTo); + ph->enmType = RTHANDLETYPE_PIPE; + *pph = ph; + } + else if (!strcmp(pszHowTo, "/dev/null")) + { + /* + * Redirect to/from /dev/null. + */ + RTFILE hFile; + rc = RTFileOpenBitBucket(&hFile, fd == 0 ? RTFILE_O_READ : RTFILE_O_WRITE); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTFileOpenBitBucket/%s/%s", pszStdWhat, pszHowTo); + + ph->enmType = RTHANDLETYPE_FILE; + ph->u.hFile = hFile; + *pph = ph; + } + else if (*pszHowTo) + { + /* + * Redirect to/from file. + */ + uint32_t fFlags; + if (fd == 0) + fFlags = RTFILE_O_READ | RTFILE_O_DENY_WRITE | RTFILE_O_OPEN; + else + { + if (pszHowTo[0] != '>' || pszHowTo[1] != '>') + fFlags = RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_CREATE_REPLACE; + else + { + /* append */ + pszHowTo += 2; + fFlags = RTFILE_O_WRITE | RTFILE_O_DENY_NONE | RTFILE_O_OPEN_CREATE | RTFILE_O_APPEND; + } + } + + RTFILE hFile; + rc = RTFileOpen(&hFile, pszHowTo, fFlags); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTFileOpen/%s/%s", pszStdWhat, pszHowTo); + + ph->enmType = RTHANDLETYPE_FILE; + ph->u.hFile = hFile; + *pph = ph; + } + else + /* same as parent (us) */ + rc = VINF_SUCCESS; + return rc; +} + +/** + * Create the environment. + * + * @returns IPRT status code, reply to client made on error. + * @param pTxsExec The TXSEXEC instance. + * @param cEnvVars The number of environment variables. + * @param papszEnv The environment variables (var=value). + */ +static int txsExecSetupEnv(PTXSEXEC pTxsExec, uint32_t cEnvVars, const char * const *papszEnv) +{ + /* + * Create the environment. + */ + int rc = RTEnvClone(&pTxsExec->hEnv, RTENV_DEFAULT); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTEnvClone"); + + for (size_t i = 0; i < cEnvVars; i++) + { + rc = RTEnvPutEx(pTxsExec->hEnv, papszEnv[i]); + if (RT_FAILURE(rc)) + return txsExecReplyRC(pTxsExec, rc, "RTEnvPutEx(,'%s')", papszEnv[i]); + } + return VINF_SUCCESS; +} + +/** + * Deletes the TXSEXEC structure and frees the memory backing it. + * + * @param pTxsExec The structure to destroy. + */ +static void txsExecDestroy(PTXSEXEC pTxsExec) +{ + int rc2; + + rc2 = RTEnvDestroy(pTxsExec->hEnv); AssertRC(rc2); + pTxsExec->hEnv = NIL_RTENV; + rc2 = RTPipeClose(pTxsExec->hTestPipeW); AssertRC(rc2); + pTxsExec->hTestPipeW = NIL_RTPIPE; + rc2 = RTHandleClose(pTxsExec->StdErr.phChild); AssertRC(rc2); + pTxsExec->StdErr.phChild = NULL; + rc2 = RTHandleClose(pTxsExec->StdOut.phChild); AssertRC(rc2); + pTxsExec->StdOut.phChild = NULL; + rc2 = RTHandleClose(pTxsExec->StdIn.phChild); AssertRC(rc2); + pTxsExec->StdIn.phChild = NULL; + + rc2 = RTPipeClose(pTxsExec->hTestPipeR); AssertRC(rc2); + pTxsExec->hTestPipeR = NIL_RTPIPE; + rc2 = RTPipeClose(pTxsExec->hStdErrR); AssertRC(rc2); + pTxsExec->hStdErrR = NIL_RTPIPE; + rc2 = RTPipeClose(pTxsExec->hStdOutR); AssertRC(rc2); + pTxsExec->hStdOutR = NIL_RTPIPE; + rc2 = RTPipeClose(pTxsExec->hStdInW); AssertRC(rc2); + pTxsExec->hStdInW = NIL_RTPIPE; + + RTPollSetDestroy(pTxsExec->hPollSet); + pTxsExec->hPollSet = NIL_RTPOLLSET; + + /* + * If the process is still running we're in a bit of a fix... Try kill it, + * although that's potentially racing process termination and reusage of + * the pid. + */ + RTCritSectEnter(&pTxsExec->CritSect); + + RTPipeClose(pTxsExec->hWakeUpPipeW); + pTxsExec->hWakeUpPipeW = NIL_RTPIPE; + RTPipeClose(pTxsExec->hWakeUpPipeR); + pTxsExec->hWakeUpPipeR = NIL_RTPIPE; + + if ( pTxsExec->hProcess != NIL_RTPROCESS + && pTxsExec->fProcessAlive) + RTProcTerminate(pTxsExec->hProcess); + + RTCritSectLeave(&pTxsExec->CritSect); + + int rcThread = VINF_SUCCESS; + if (pTxsExec->hThreadWaiter != NIL_RTTHREAD) + rcThread = RTThreadWait(pTxsExec->hThreadWaiter, 5000, NULL); + if (RT_SUCCESS(rcThread)) + { + pTxsExec->hThreadWaiter = NIL_RTTHREAD; + RTCritSectDelete(&pTxsExec->CritSect); + RTMemFree(pTxsExec); + } + /* else: leak it or RTThreadWait may cause heap corruption later. */ +} + +/** + * Initializes the TXSEXEC structure. + * + * @returns VINF_SUCCESS and non-NULL *ppTxsExec on success, reply send status + * and *ppTxsExec set to NULL on failure. + * @param pPktHdr The exec packet. + * @param cMsTimeout The time parameter. + * @param ppTxsExec Where to return the structure. + */ +static int txsExecCreate(PCTXSPKTHDR pPktHdr, RTMSINTERVAL cMsTimeout, PTXSEXEC *ppTxsExec) +{ + *ppTxsExec = NULL; + + /* + * Allocate the basic resources. + */ + PTXSEXEC pTxsExec = (PTXSEXEC)RTMemAlloc(sizeof(*pTxsExec)); + if (!pTxsExec) + return txsReplyRC(pPktHdr, VERR_NO_MEMORY, "RTMemAlloc(%zu)", sizeof(*pTxsExec)); + int rc = RTCritSectInit(&pTxsExec->CritSect); + if (RT_FAILURE(rc)) + { + RTMemFree(pTxsExec); + return txsReplyRC(pPktHdr, rc, "RTCritSectInit"); + } + + /* + * Initialize the member to NIL values. + */ + pTxsExec->pPktHdr = pPktHdr; + pTxsExec->cMsTimeout = cMsTimeout; + pTxsExec->rcReplySend = VINF_SUCCESS; + + pTxsExec->hPollSet = NIL_RTPOLLSET; + pTxsExec->hStdInW = NIL_RTPIPE; + pTxsExec->hStdOutR = NIL_RTPIPE; + pTxsExec->hStdErrR = NIL_RTPIPE; + pTxsExec->hTestPipeR = NIL_RTPIPE; + pTxsExec->hWakeUpPipeR = NIL_RTPIPE; + pTxsExec->hThreadWaiter = NIL_RTTHREAD; + + pTxsExec->StdIn.phChild = NULL; + pTxsExec->StdOut.phChild = NULL; + pTxsExec->StdErr.phChild = NULL; + pTxsExec->hTestPipeW = NIL_RTPIPE; + pTxsExec->hEnv = NIL_RTENV; + + pTxsExec->hProcess = NIL_RTPROCESS; + pTxsExec->ProcessStatus.iStatus = 254; + pTxsExec->ProcessStatus.enmReason = RTPROCEXITREASON_ABEND; + pTxsExec->fProcessAlive = false; + pTxsExec->hWakeUpPipeW = NIL_RTPIPE; + + *ppTxsExec = pTxsExec; + return VINF_SUCCESS; +} + +/** + * txsDoExec helper that takes over when txsDoExec has expanded the packet. + * + * @returns IPRT status code from send. + * @param pPktHdr The exec packet. + * @param fFlags Flags, reserved for future use. + * @param pszExecName The executable name. + * @param cArgs The argument count. + * @param papszArgs The arguments. + * @param cEnvVars The environment variable count. + * @param papszEnv The environment variables. + * @param pszStdIn How to deal with standard in. + * @param pszStdOut How to deal with standard out. + * @param pszStdErr How to deal with standard err. + * @param pszTestPipe How to deal with the test pipe. + * @param pszUsername The user to run the program as. + * @param cMillies The process time limit in milliseconds. + */ +static int txsDoExecHlp(PCTXSPKTHDR pPktHdr, uint32_t fFlags, const char *pszExecName, + uint32_t cArgs, const char * const *papszArgs, + uint32_t cEnvVars, const char * const *papszEnv, + const char *pszStdIn, const char *pszStdOut, const char *pszStdErr, const char *pszTestPipe, + const char *pszUsername, RTMSINTERVAL cMillies) +{ + int rc2; + RT_NOREF_PV(fFlags); + + /* + * Input validation, filter out things we don't yet support.. + */ + Assert(!fFlags); + if (!*pszExecName) + return txsReplyFailure(pPktHdr, "STR ZERO", "Executable name is empty"); + if (!*pszStdIn) + return txsReplyFailure(pPktHdr, "STR ZERO", "The stdin howto is empty"); + if (!*pszStdOut) + return txsReplyFailure(pPktHdr, "STR ZERO", "The stdout howto is empty"); + if (!*pszStdErr) + return txsReplyFailure(pPktHdr, "STR ZERO", "The stderr howto is empty"); + if (!*pszTestPipe) + return txsReplyFailure(pPktHdr, "STR ZERO", "The testpipe howto is empty"); + if (strcmp(pszTestPipe, "|") && strcmp(pszTestPipe, "/dev/null")) + return txsReplyFailure(pPktHdr, "BAD TSTP", "Only \"|\" and \"/dev/null\" are allowed as testpipe howtos ('%s')", + pszTestPipe); + if (*pszUsername) + return txsReplyFailure(pPktHdr, "NOT IMPL", "Executing as a specific user is not implemented ('%s')", pszUsername); + + /* + * Prepare for process launch. + */ + PTXSEXEC pTxsExec; + int rc = txsExecCreate(pPktHdr, cMillies, &pTxsExec); + if (pTxsExec == NULL) + return rc; + rc = txsExecSetupEnv(pTxsExec, cEnvVars, papszEnv); + if (RT_SUCCESS(rc)) + rc = txsExecSetupRedir(pTxsExec, pszStdIn, "StdIn", 0, &pTxsExec->StdIn.hChild, &pTxsExec->StdIn.phChild, &pTxsExec->hStdInW); + if (RT_SUCCESS(rc)) + rc = txsExecSetupRedir(pTxsExec, pszStdOut, "StdOut", 1, &pTxsExec->StdOut.hChild, &pTxsExec->StdOut.phChild, &pTxsExec->hStdOutR); + if (RT_SUCCESS(rc)) + rc = txsExecSetupRedir(pTxsExec, pszStdErr, "StdErr", 2, &pTxsExec->StdErr.hChild, &pTxsExec->StdErr.phChild, &pTxsExec->hStdErrR); + if (RT_SUCCESS(rc)) + rc = txsExecSetupTestPipe(pTxsExec, pszTestPipe); + if (RT_SUCCESS(rc)) + rc = txsExecSetupThread(pTxsExec); + if (RT_SUCCESS(rc)) + rc = txsExecSetupPollSet(pTxsExec); + if (RT_SUCCESS(rc)) + { + char szPathResolved[RTPATH_MAX + 1]; + rc = RTPathReal(pszExecName, szPathResolved, sizeof(szPathResolved)); + if (RT_SUCCESS(rc)) + { + /* + * Create the process. + */ + if (g_fDisplayOutput) + { + RTPrintf("txs: Executing \"%s\" -> \"%s\": ", pszExecName, szPathResolved); + for (uint32_t i = 0; i < cArgs; i++) + RTPrintf(" \"%s\"", papszArgs[i]); + RTPrintf("\n"); + } + + rc = RTProcCreateEx(szPathResolved, papszArgs, pTxsExec->hEnv, 0 /*fFlags*/, + pTxsExec->StdIn.phChild, pTxsExec->StdOut.phChild, pTxsExec->StdErr.phChild, + *pszUsername ? pszUsername : NULL, NULL, NULL, + &pTxsExec->hProcess); + if (RT_SUCCESS(rc)) + { + ASMAtomicWriteBool(&pTxsExec->fProcessAlive, true); + rc2 = RTThreadUserSignal(pTxsExec->hThreadWaiter); AssertRC(rc2); + + /* + * Close the child ends of any pipes and redirected files. + */ + rc2 = RTHandleClose(pTxsExec->StdIn.phChild); AssertRC(rc2); + pTxsExec->StdIn.phChild = NULL; + rc2 = RTHandleClose(pTxsExec->StdOut.phChild); AssertRC(rc2); + pTxsExec->StdOut.phChild = NULL; + rc2 = RTHandleClose(pTxsExec->StdErr.phChild); AssertRC(rc2); + pTxsExec->StdErr.phChild = NULL; + rc2 = RTPipeClose(pTxsExec->hTestPipeW); AssertRC(rc2); + pTxsExec->hTestPipeW = NIL_RTPIPE; + + /* + * Let another worker function funnel output and input to the + * client as well as the process exit code. + */ + rc = txsDoExecHlp2(pTxsExec); + } + } + + if (RT_FAILURE(rc)) + rc = txsReplyFailure(pPktHdr, "FAILED ", "Executing process \"%s\" failed with %Rrc", + pszExecName, rc); + } + else + rc = pTxsExec->rcReplySend; + txsExecDestroy(pTxsExec); + return rc; +} + +/** + * Execute a program. + * + * @returns IPRT status code from send. + * @param pPktHdr The exec packet. + */ +static int txsDoExec(PCTXSPKTHDR pPktHdr) +{ + /* + * This packet has a lot of parameters, most of which are zero terminated + * strings. The strings used in items 7 thru 10 are either file names, + * "/dev/null" or a pipe char (|). + * + * Packet content: + * 1. Flags reserved for future use (32-bit unsigned). + * 2. The executable name (string). + * 3. The argument count given as a 32-bit unsigned integer. + * 4. The arguments strings. + * 5. The number of environment strings (32-bit unsigned). + * 6. The environment strings (var=val) to apply the environment. + * 7. What to do about standard in (string). + * 8. What to do about standard out (string). + * 9. What to do about standard err (string). + * 10. What to do about the test pipe (string). + * 11. The name of the user to run the program as (string). Empty string + * means running it as the current user. + * 12. Process time limit in milliseconds (32-bit unsigned). Max == no limit. + */ + size_t const cbMin = sizeof(TXSPKTHDR) + + sizeof(uint32_t) /* flags */ + 2 + + sizeof(uint32_t) /* argc */ + 2 /* argv */ + + sizeof(uint32_t) + 0 /* environ */ + + 4 * 1 + + sizeof(uint32_t) /* timeout */; + if (pPktHdr->cb < cbMin) + return txsReplyBadMinSize(pPktHdr, cbMin); + + /* unpack the packet */ + char const *pchEnd = (char const *)pPktHdr + pPktHdr->cb; + char const *pch = (char const *)(pPktHdr + 1); /* cursor */ + + /* 1. flags */ + uint32_t const fFlags = *(uint32_t const *)pch; + pch += sizeof(uint32_t); + if (fFlags != 0) + return txsReplyFailure(pPktHdr, "BAD FLAG", "Invalid EXEC flags %#x, expected 0", fFlags); + + /* 2. exec name */ + int rc; + char *pszExecName = NULL; + if (!txsIsStringValid(pPktHdr, "execname", pch, &pszExecName, &pch, &rc)) + return rc; + + /* 3. argc */ + uint32_t const cArgs = (size_t)(pchEnd - pch) > sizeof(uint32_t) ? *(uint32_t const *)pch : 0xff; + pch += sizeof(uint32_t); + if (cArgs * 1 >= (size_t)(pchEnd - pch)) + rc = txsReplyFailure(pPktHdr, "BAD ARGC", "Bad or missing argument count (%#x)", cArgs); + else if (cArgs > 128) + rc = txsReplyFailure(pPktHdr, "BAD ARGC", "Too many arguments (%#x)", cArgs); + else + { + char **papszArgs = (char **)RTMemTmpAllocZ(sizeof(char *) * (cArgs + 1)); + if (papszArgs) + { + /* 4. argv */ + bool fOk = true; + for (size_t i = 0; i < cArgs && fOk; i++) + { + fOk = txsIsStringValid(pPktHdr, "argvN", pch, &papszArgs[i], &pch, &rc); + if (!fOk) + break; + } + if (fOk) + { + /* 5. cEnvVars */ + uint32_t const cEnvVars = (size_t)(pchEnd - pch) > sizeof(uint32_t) ? *(uint32_t const *)pch : 0xfff; + pch += sizeof(uint32_t); + if (cEnvVars * 1 >= (size_t)(pchEnd - pch)) + rc = txsReplyFailure(pPktHdr, "BAD ENVC", "Bad or missing environment variable count (%#x)", cEnvVars); + else if (cEnvVars > 256) + rc = txsReplyFailure(pPktHdr, "BAD ENVC", "Too many environment variables (%#x)", cEnvVars); + else + { + char **papszEnv = (char **)RTMemTmpAllocZ(sizeof(char *) * (cEnvVars + 1)); + if (papszEnv) + { + /* 6. environ */ + for (size_t i = 0; i < cEnvVars && fOk; i++) + { + fOk = txsIsStringValid(pPktHdr, "envN", pch, &papszEnv[i], &pch, &rc); + if (!fOk) /* Bail out on error. */ + break; + } + if (fOk) + { + /* 7. stdout */ + char *pszStdIn; + if (txsIsStringValid(pPktHdr, "stdin", pch, &pszStdIn, &pch, &rc)) + { + /* 8. stdout */ + char *pszStdOut; + if (txsIsStringValid(pPktHdr, "stdout", pch, &pszStdOut, &pch, &rc)) + { + /* 9. stderr */ + char *pszStdErr; + if (txsIsStringValid(pPktHdr, "stderr", pch, &pszStdErr, &pch, &rc)) + { + /* 10. testpipe */ + char *pszTestPipe; + if (txsIsStringValid(pPktHdr, "testpipe", pch, &pszTestPipe, &pch, &rc)) + { + /* 11. username */ + char *pszUsername; + if (txsIsStringValid(pPktHdr, "username", pch, &pszUsername, &pch, &rc)) + { + /** @todo No password value? */ + + /* 12. time limit */ + uint32_t const cMillies = (size_t)(pchEnd - pch) >= sizeof(uint32_t) + ? *(uint32_t const *)pch + : 0; + if ((size_t)(pchEnd - pch) > sizeof(uint32_t)) + rc = txsReplyFailure(pPktHdr, "BAD END ", "Timeout argument not at end of packet (%#x)", (size_t)(pchEnd - pch)); + else if ((size_t)(pchEnd - pch) < sizeof(uint32_t)) + rc = txsReplyFailure(pPktHdr, "BAD NOTO", "No timeout argument"); + else if (cMillies < 1000) + rc = txsReplyFailure(pPktHdr, "BAD TO ", "Timeout is less than a second (%#x)", cMillies); + else + { + pch += sizeof(uint32_t); + + /* + * Time to employ a helper here before we go way beyond + * the right margin... + */ + rc = txsDoExecHlp(pPktHdr, fFlags, pszExecName, + cArgs, papszArgs, + cEnvVars, papszEnv, + pszStdIn, pszStdOut, pszStdErr, pszTestPipe, + pszUsername, + cMillies == UINT32_MAX ? RT_INDEFINITE_WAIT : cMillies); + } + RTStrFree(pszUsername); + } + RTStrFree(pszTestPipe); + } + RTStrFree(pszStdErr); + } + RTStrFree(pszStdOut); + } + RTStrFree(pszStdIn); + } + } + for (size_t i = 0; i < cEnvVars; i++) + RTStrFree(papszEnv[i]); + RTMemTmpFree(papszEnv); + } + else + rc = txsReplyFailure(pPktHdr, "NO MEM ", "Failed to allocate %zu bytes environ", sizeof(char *) * (cEnvVars + 1)); + } + } + for (size_t i = 0; i < cArgs; i++) + RTStrFree(papszArgs[i]); + RTMemTmpFree(papszArgs); + } + else + rc = txsReplyFailure(pPktHdr, "NO MEM ", "Failed to allocate %zu bytes for argv", sizeof(char *) * (cArgs + 1)); + } + RTStrFree(pszExecName); + + return rc; +} + +/** + * The main loop. + * + * @returns exit code. + */ +static RTEXITCODE txsMainLoop(void) +{ + if (g_cVerbose > 0) + RTMsgInfo("txsMainLoop: start...\n"); + RTEXITCODE enmExitCode = RTEXITCODE_SUCCESS; + while (!g_fTerminate) + { + /* + * Read client command packet and process it. + */ + PTXSPKTHDR pPktHdr; + int rc = txsRecvPkt(&pPktHdr, true /*fAutoRetryOnFailure*/); + if (RT_FAILURE(rc)) + continue; + if (g_cVerbose > 0) + RTMsgInfo("txsMainLoop: CMD: %.8s...", pPktHdr->achOpcode); + + /* + * Do a string switch on the opcode bit. + */ + /* Connection: */ + if ( txsIsSameOpcode(pPktHdr, "HOWDY ")) + rc = txsDoHowdy(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "BYE ")) + rc = txsDoBye(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "VER ")) + rc = txsDoVer(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "UUID ")) + rc = txsDoUuid(pPktHdr); + /* Process: */ + else if (txsIsSameOpcode(pPktHdr, "EXEC ")) + rc = txsDoExec(pPktHdr); + /* Admin: */ + else if (txsIsSameOpcode(pPktHdr, "REBOOT ")) + rc = txsDoReboot(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "SHUTDOWN")) + rc = txsDoShutdown(pPktHdr); + /* CD/DVD control: */ + else if (txsIsSameOpcode(pPktHdr, "CD EJECT")) + rc = txsDoCdEject(pPktHdr); + /* File system: */ + else if (txsIsSameOpcode(pPktHdr, "CLEANUP ")) + rc = txsDoCleanup(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "MKDIR ")) + rc = txsDoMkDir(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "MKDRPATH")) + rc = txsDoMkDrPath(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "MKSYMLNK")) + rc = txsDoMkSymlnk(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "RMDIR ")) + rc = txsDoRmDir(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "RMFILE ")) + rc = txsDoRmFile(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "RMSYMLNK")) + rc = txsDoRmSymlnk(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "RMTREE ")) + rc = txsDoRmTree(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "CHMOD ")) + rc = txsDoChMod(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "CHOWN ")) + rc = txsDoChOwn(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "ISDIR ")) + rc = txsDoIsDir(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "ISFILE ")) + rc = txsDoIsFile(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "ISSYMLNK")) + rc = txsDoIsSymlnk(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "STAT ")) + rc = txsDoStat(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "LSTAT ")) + rc = txsDoLStat(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "LIST ")) + rc = txsDoList(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "CPFILE ")) + rc = txsDoCopyFile(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "PUT FILE")) + rc = txsDoPutFile(pPktHdr, false /*fHasMode*/); + else if (txsIsSameOpcode(pPktHdr, "PUT2FILE")) + rc = txsDoPutFile(pPktHdr, true /*fHasMode*/); + else if (txsIsSameOpcode(pPktHdr, "GET FILE")) + rc = txsDoGetFile(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "PKFILE ")) + rc = txsDoPackFile(pPktHdr); + else if (txsIsSameOpcode(pPktHdr, "UNPKFILE")) + rc = txsDoUnpackFile(pPktHdr); + /* Misc: */ + else if (txsIsSameOpcode(pPktHdr, "EXP STR ")) + rc = txsDoExpandString(pPktHdr); + else + rc = txsReplyUnknown(pPktHdr); + + if (g_cVerbose > 0) + RTMsgInfo("txsMainLoop: CMD: %.8s -> %Rrc", pPktHdr->achOpcode, rc); + RTMemFree(pPktHdr); + } + + if (g_cVerbose > 0) + RTMsgInfo("txsMainLoop: end\n"); + return enmExitCode; +} + + +/** + * Finalizes the scratch directory, making sure it exists. + * + * @returns exit code. + */ +static RTEXITCODE txsFinalizeScratch(void) +{ + RTPathStripTrailingSlash(g_szScratchPath); + char *pszFilename = RTPathFilename(g_szScratchPath); + if (!pszFilename) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "cannot use root for scratch (%s)\n", g_szScratchPath); + + int rc; + if (strchr(pszFilename, 'X')) + { + char ch = *pszFilename; + rc = RTDirCreateFullPath(g_szScratchPath, 0700); + *pszFilename = ch; + if (RT_SUCCESS(rc)) + rc = RTDirCreateTemp(g_szScratchPath, 0700); + } + else + { + if (RTDirExists(g_szScratchPath)) + rc = VINF_SUCCESS; + else + rc = RTDirCreateFullPath(g_szScratchPath, 0700); + } + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "failed to create scratch directory: %Rrc (%s)\n", rc, g_szScratchPath); + return RTEXITCODE_SUCCESS; +} + +/** + * Attempts to complete an upgrade by updating the original and relaunching + * ourselves from there again. + * + * On failure, we'll continue running as the temporary copy. + * + * @returns Exit code. Exit if this is non-zero or @a *pfExit is set. + * @param argc The number of arguments. + * @param argv The argument vector. + * @param pfExit For indicating exit when the exit code is zero. + * @param pszUpgrading The upgraded image path. + */ +static RTEXITCODE txsAutoUpdateStage2(int argc, char **argv, bool *pfExit, const char *pszUpgrading) +{ + if (g_cVerbose > 0) + RTMsgInfo("Auto update stage 2..."); + + /* + * Copy the current executable onto the original. + * Note that we're racing the original program on some platforms, thus the + * 60 sec sleep mess. + */ + char szUpgradePath[RTPATH_MAX]; + if (!RTProcGetExecutablePath(szUpgradePath, sizeof(szUpgradePath))) + { + RTMsgError("RTProcGetExecutablePath failed (step 2)\n"); + return RTEXITCODE_SUCCESS; + } + void *pvUpgrade; + size_t cbUpgrade; + int rc = RTFileReadAll(szUpgradePath, &pvUpgrade, &cbUpgrade); + if (RT_FAILURE(rc)) + { + RTMsgError("RTFileReadAllEx(\"%s\"): %Rrc (step 2)\n", szUpgradePath, rc); + return RTEXITCODE_SUCCESS; + } + + uint64_t StartMilliTS = RTTimeMilliTS(); + RTFILE hFile; + rc = RTFileOpen(&hFile, pszUpgrading, + RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_OPEN_CREATE | RTFILE_O_TRUNCATE + | (0755 << RTFILE_O_CREATE_MODE_SHIFT)); + while ( RT_FAILURE(rc) + && RTTimeMilliTS() - StartMilliTS < 60000) + { + RTThreadSleep(1000); + rc = RTFileOpen(&hFile, pszUpgrading, + RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_OPEN_CREATE | RTFILE_O_TRUNCATE + | (0755 << RTFILE_O_CREATE_MODE_SHIFT)); + } + if (RT_SUCCESS(rc)) + { + rc = RTFileWrite(hFile, pvUpgrade, cbUpgrade, NULL); + RTFileClose(hFile); + if (RT_SUCCESS(rc)) + { + /* + * Relaunch the service with the original name, foricbly barring + * further upgrade cycles in case of bugs (and simplifying the code). + */ + const char **papszArgs = (const char **)RTMemAlloc((argc + 1 + 1) * sizeof(char **)); + if (papszArgs) + { + papszArgs[0] = pszUpgrading; + for (int i = 1; i < argc; i++) + papszArgs[i] = argv[i]; + papszArgs[argc] = "--no-auto-upgrade"; + papszArgs[argc + 1] = NULL; + + RTMsgInfo("Launching upgraded image: \"%s\"\n", pszUpgrading); + RTPROCESS hProc; + rc = RTProcCreate(pszUpgrading, papszArgs, RTENV_DEFAULT, 0 /*fFlags*/, &hProc); + if (RT_SUCCESS(rc)) + *pfExit = true; + else + RTMsgError("RTProcCreate(\"%s\"): %Rrc (upgrade stage 2)\n", pszUpgrading, rc); + RTMemFree(papszArgs); + } + else + RTMsgError("RTMemAlloc failed during upgrade attempt (stage 2)\n"); + } + else + RTMsgError("RTFileWrite(%s,,%zu): %Rrc (step 2) - BAD\n", pszUpgrading, cbUpgrade, rc); + } + else + RTMsgError("RTFileOpen(,%s,): %Rrc\n", pszUpgrading, rc); + RTFileReadAllFree(pvUpgrade, cbUpgrade); + return RTEXITCODE_SUCCESS; +} + +/** + * Checks for an upgrade and respawns if there is. + * + * @returns Exit code. Exit if this is non-zero or @a *pfExit is set. + * @param argc The number of arguments. + * @param argv The argument vector. + * @param cSecsCdWait Number of seconds to wait on the CD. + * @param pfExit For indicating exit when the exit code is zero. + */ +static RTEXITCODE txsAutoUpdateStage1(int argc, char **argv, uint32_t cSecsCdWait, bool *pfExit) +{ + if (g_cVerbose > 1) + RTMsgInfo("Auto update stage 1..."); + + /* + * Figure names of the current service image and the potential upgrade. + */ + char szOrgPath[RTPATH_MAX]; + if (!RTProcGetExecutablePath(szOrgPath, sizeof(szOrgPath))) + { + RTMsgError("RTProcGetExecutablePath failed\n"); + return RTEXITCODE_SUCCESS; + } + + char szUpgradePath[RTPATH_MAX]; + int rc = RTPathJoin(szUpgradePath, sizeof(szUpgradePath), g_szCdRomPath, g_szOsSlashArchShortName); + if (RT_SUCCESS(rc)) + rc = RTPathAppend(szUpgradePath, sizeof(szUpgradePath), RTPathFilename(szOrgPath)); + if (RT_FAILURE(rc)) + { + RTMsgError("Failed to construct path to potential service upgrade: %Rrc\n", rc); + return RTEXITCODE_SUCCESS; + } + + /* + * Query information about the two images and read the entire potential source file. + * Because the CD may take a little time to be mounted when the system boots, we + * need to do some fudging here. + */ + uint64_t nsStart = RTTimeNanoTS(); + RTFSOBJINFO UpgradeInfo; + for (;;) + { + rc = RTPathQueryInfo(szUpgradePath, &UpgradeInfo, RTFSOBJATTRADD_NOTHING); + if (RT_SUCCESS(rc)) + break; + if ( rc != VERR_FILE_NOT_FOUND + && rc != VERR_PATH_NOT_FOUND + && rc != VERR_MEDIA_NOT_PRESENT + && rc != VERR_MEDIA_NOT_RECOGNIZED) + { + RTMsgError("RTPathQueryInfo(\"%s\"): %Rrc (upgrade)\n", szUpgradePath, rc); + return RTEXITCODE_SUCCESS; + } + uint64_t cNsElapsed = RTTimeNanoTS() - nsStart; + if (cNsElapsed >= cSecsCdWait * RT_NS_1SEC_64) + { + if (g_cVerbose > 0) + RTMsgInfo("Auto update: Giving up waiting for media."); + return RTEXITCODE_SUCCESS; + } + RTThreadSleep(500); + } + + RTFSOBJINFO OrgInfo; + rc = RTPathQueryInfo(szOrgPath, &OrgInfo, RTFSOBJATTRADD_NOTHING); + if (RT_FAILURE(rc)) + { + RTMsgError("RTPathQueryInfo(\"%s\"): %Rrc (old)\n", szOrgPath, rc); + return RTEXITCODE_SUCCESS; + } + + void *pvUpgrade; + size_t cbUpgrade; + rc = RTFileReadAllEx(szUpgradePath, 0, UpgradeInfo.cbObject, RTFILE_RDALL_O_DENY_NONE, &pvUpgrade, &cbUpgrade); + if (RT_FAILURE(rc)) + { + RTMsgError("RTPathQueryInfo(\"%s\"): %Rrc (old)\n", szOrgPath, rc); + return RTEXITCODE_SUCCESS; + } + + /* + * Compare and see if we've got a different service image or not. + */ + if (OrgInfo.cbObject == UpgradeInfo.cbObject) + { + /* must compare bytes. */ + void *pvOrg; + size_t cbOrg; + rc = RTFileReadAllEx(szOrgPath, 0, OrgInfo.cbObject, RTFILE_RDALL_O_DENY_NONE, &pvOrg, &cbOrg); + if (RT_FAILURE(rc)) + { + RTMsgError("RTFileReadAllEx(\"%s\"): %Rrc\n", szOrgPath, rc); + RTFileReadAllFree(pvUpgrade, cbUpgrade); + return RTEXITCODE_SUCCESS; + } + bool fSame = !memcmp(pvUpgrade, pvOrg, OrgInfo.cbObject); + RTFileReadAllFree(pvOrg, cbOrg); + if (fSame) + { + RTFileReadAllFree(pvUpgrade, cbUpgrade); + if (g_cVerbose > 0) + RTMsgInfo("Auto update: Not necessary."); + return RTEXITCODE_SUCCESS; + } + } + + /* + * Should upgrade. Start by creating an executable copy of the update + * image in the scratch area. + */ + RTEXITCODE rcExit = txsFinalizeScratch(); + if (rcExit == RTEXITCODE_SUCCESS) + { + char szTmpPath[RTPATH_MAX]; + rc = RTPathJoin(szTmpPath, sizeof(szTmpPath), g_szScratchPath, RTPathFilename(szOrgPath)); + if (RT_SUCCESS(rc)) + { + RTFileDelete(szTmpPath); /* shouldn't hurt. */ + + RTFILE hFile; + rc = RTFileOpen(&hFile, szTmpPath, + RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_CREATE_REPLACE + | (0755 << RTFILE_O_CREATE_MODE_SHIFT)); + if (RT_SUCCESS(rc)) + { + rc = RTFileWrite(hFile, pvUpgrade, UpgradeInfo.cbObject, NULL); + RTFileClose(hFile); + if (RT_SUCCESS(rc)) + { + /* + * Try execute the new image and quit if it works. + */ + const char **papszArgs = (const char **)RTMemAlloc((argc + 2 + 1) * sizeof(char **)); + if (papszArgs) + { + papszArgs[0] = szTmpPath; + for (int i = 1; i < argc; i++) + papszArgs[i] = argv[i]; + papszArgs[argc] = "--upgrading"; + papszArgs[argc + 1] = szOrgPath; + papszArgs[argc + 2] = NULL; + + RTMsgInfo("Launching intermediate automatic upgrade stage: \"%s\"\n", szTmpPath); + RTPROCESS hProc; + rc = RTProcCreate(szTmpPath, papszArgs, RTENV_DEFAULT, 0 /*fFlags*/, &hProc); + if (RT_SUCCESS(rc)) + *pfExit = true; + else + RTMsgError("RTProcCreate(\"%s\"): %Rrc (upgrade stage 1)\n", szTmpPath, rc); + RTMemFree(papszArgs); + } + else + RTMsgError("RTMemAlloc failed during upgrade attempt (stage)\n"); + } + else + RTMsgError("RTFileWrite(%s,,%zu): %Rrc\n", szTmpPath, UpgradeInfo.cbObject, rc); + } + else + RTMsgError("RTFileOpen(,%s,): %Rrc\n", szTmpPath, rc); + } + else + RTMsgError("Failed to construct path to temporary upgrade image: %Rrc\n", rc); + } + + RTFileReadAllFree(pvUpgrade, cbUpgrade); + return rcExit; +} + +/** + * Determines the default configuration. + */ +static void txsSetDefaults(void) +{ + /* + * OS and ARCH. + */ + AssertCompile(sizeof(KBUILD_TARGET) <= sizeof(g_szOsShortName)); + strcpy(g_szOsShortName, KBUILD_TARGET); + + AssertCompile(sizeof(KBUILD_TARGET_ARCH) <= sizeof(g_szArchShortName)); + strcpy(g_szArchShortName, KBUILD_TARGET_ARCH); + + AssertCompile(sizeof(KBUILD_TARGET) + sizeof(KBUILD_TARGET_ARCH) <= sizeof(g_szOsDotArchShortName)); + strcpy(g_szOsDotArchShortName, KBUILD_TARGET); + g_szOsDotArchShortName[sizeof(KBUILD_TARGET) - 1] = '.'; + strcpy(&g_szOsDotArchShortName[sizeof(KBUILD_TARGET)], KBUILD_TARGET_ARCH); + + AssertCompile(sizeof(KBUILD_TARGET) + sizeof(KBUILD_TARGET_ARCH) <= sizeof(g_szOsSlashArchShortName)); + strcpy(g_szOsSlashArchShortName, KBUILD_TARGET); + g_szOsSlashArchShortName[sizeof(KBUILD_TARGET) - 1] = '/'; + strcpy(&g_szOsSlashArchShortName[sizeof(KBUILD_TARGET)], KBUILD_TARGET_ARCH); + +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + strcpy(g_szExeSuff, ".exe"); + strcpy(g_szScriptSuff, ".cmd"); +#else + strcpy(g_szExeSuff, ""); + strcpy(g_szScriptSuff, ".sh"); +#endif + + int rc = RTPathGetCurrent(g_szCwd, sizeof(g_szCwd)); + if (RT_FAILURE(rc)) + RTMsgError("RTPathGetCurrent failed: %Rrc\n", rc); + g_szCwd[sizeof(g_szCwd) - 1] = '\0'; + + if (!RTProcGetExecutablePath(g_szTxsDir, sizeof(g_szTxsDir))) + RTMsgError("RTProcGetExecutablePath failed!\n"); + g_szTxsDir[sizeof(g_szTxsDir) - 1] = '\0'; + RTPathStripFilename(g_szTxsDir); + RTPathStripTrailingSlash(g_szTxsDir); + + /* + * The CD/DVD-ROM location. + */ + /** @todo do a better job here :-) */ +#ifdef RT_OS_WINDOWS + strcpy(g_szDefCdRomPath, "D:/"); +#elif defined(RT_OS_OS2) + strcpy(g_szDefCdRomPath, "D:/"); +#else + if (RTDirExists("/media")) + strcpy(g_szDefCdRomPath, "/media/cdrom"); + else + strcpy(g_szDefCdRomPath, "/mnt/cdrom"); +#endif + strcpy(g_szCdRomPath, g_szDefCdRomPath); + + /* + * Temporary directory. + */ + rc = RTPathTemp(g_szDefScratchPath, sizeof(g_szDefScratchPath)); + if (RT_SUCCESS(rc)) +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) || defined(RT_OS_DOS) + rc = RTPathAppend(g_szDefScratchPath, sizeof(g_szDefScratchPath), "txs-XXXX.tmp"); +#else + rc = RTPathAppend(g_szDefScratchPath, sizeof(g_szDefScratchPath), "txs-XXXXXXXXX.tmp"); +#endif + if (RT_FAILURE(rc)) + { + RTMsgError("RTPathTemp/Append failed when constructing scratch path: %Rrc\n", rc); + strcpy(g_szDefScratchPath, "/tmp/txs-XXXX.tmp"); + } + strcpy(g_szScratchPath, g_szDefScratchPath); + + /* + * The default transporter is the first one. + */ + g_pTransport = g_apTransports[0]; +} + +/** + * Prints the usage. + * + * @param pStrm Where to print it. + * @param pszArgv0 The program name (argv[0]). + */ +static void txsUsage(PRTSTREAM pStrm, const char *pszArgv0) +{ + RTStrmPrintf(pStrm, + "Usage: %Rbn [options]\n" + "\n" + "Options:\n" + " --cdrom <path>\n" + " Where the CD/DVD-ROM will be mounted.\n" + " Default: %s\n" + " --scratch <path>\n" + " Where to put scratch files.\n" + " Default: %s \n" + , + pszArgv0, + g_szDefCdRomPath, + g_szDefScratchPath); + RTStrmPrintf(pStrm, + " --transport <name>\n" + " Use the specified transport layer, one of the following:\n"); + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + RTStrmPrintf(pStrm, " %s - %s\n", g_apTransports[i]->szName, g_apTransports[i]->pszDesc); + RTStrmPrintf(pStrm, " Default: %s\n", g_pTransport->szName); + RTStrmPrintf(pStrm, + " --auto-upgrade, --no-auto-upgrade\n" + " To enable or disable the automatic upgrade mechanism where any different\n" + " version found on the CD-ROM on startup will replace the initial copy.\n" + " Default: --auto-upgrade\n" + " --wait-cdrom <secs>\n" + " Number of seconds to wait for the CD-ROM to be mounted before giving up\n" + " on automatic upgrading.\n" + " Default: --wait-cdrom 1; solaris: --wait-cdrom 8\n" + " --upgrading <org-path>\n" + " Internal use only.\n"); + RTStrmPrintf(pStrm, + " --display-output, --no-display-output\n" + " Display the output and the result of all child processes.\n"); + RTStrmPrintf(pStrm, + " --foreground\n" + " Don't daemonize, run in the foreground.\n"); + RTStrmPrintf(pStrm, + " --verbose, -v\n" + " Increases the verbosity level. Can be specified multiple times.\n"); + RTStrmPrintf(pStrm, + " --quiet, -q\n" + " Mutes any logging output.\n"); + RTStrmPrintf(pStrm, + " --help, -h, -?\n" + " Display this message and exit.\n" + " --version, -V\n" + " Display the version and exit.\n"); + + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + if (g_apTransports[i]->cOpts) + { + RTStrmPrintf(pStrm, + "\n" + "Options for %s:\n", g_apTransports[i]->szName); + g_apTransports[i]->pfnUsage(g_pStdOut); + } +} + +/** + * Parses the arguments. + * + * @returns Exit code. Exit if this is non-zero or @a *pfExit is set. + * @param argc The number of arguments. + * @param argv The argument vector. + * @param pfExit For indicating exit when the exit code is zero. + */ +static RTEXITCODE txsParseArgv(int argc, char **argv, bool *pfExit) +{ + *pfExit = false; + + /* + * Storage for locally handled options. + */ + bool fAutoUpgrade = true; + bool fDaemonize = true; + bool fDaemonized = false; + const char *pszUpgrading = NULL; +#ifdef RT_OS_SOLARIS + uint32_t cSecsCdWait = 8; +#else + uint32_t cSecsCdWait = 1; +#endif + + /* + * Combine the base and transport layer option arrays. + */ + static const RTGETOPTDEF s_aBaseOptions[] = + { + { "--transport", 't', RTGETOPT_REQ_STRING }, + { "--cdrom", 'c', RTGETOPT_REQ_STRING }, + { "--wait-cdrom", 'w', RTGETOPT_REQ_UINT32 }, + { "--scratch", 's', RTGETOPT_REQ_STRING }, + { "--auto-upgrade", 'a', RTGETOPT_REQ_NOTHING }, + { "--no-auto-upgrade", 'A', RTGETOPT_REQ_NOTHING }, + { "--upgrading", 'U', RTGETOPT_REQ_STRING }, + { "--display-output", 'd', RTGETOPT_REQ_NOTHING }, + { "--no-display-output",'D', RTGETOPT_REQ_NOTHING }, + { "--foreground", 'f', RTGETOPT_REQ_NOTHING }, + { "--daemonized", 'Z', RTGETOPT_REQ_NOTHING }, + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + }; + + size_t cOptions = RT_ELEMENTS(s_aBaseOptions); + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + cOptions += g_apTransports[i]->cOpts; + + PRTGETOPTDEF paOptions = (PRTGETOPTDEF)alloca(cOptions * sizeof(RTGETOPTDEF)); + if (!paOptions) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "alloca failed\n"); + + memcpy(paOptions, s_aBaseOptions, sizeof(s_aBaseOptions)); + cOptions = RT_ELEMENTS(s_aBaseOptions); + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + { + memcpy(&paOptions[cOptions], g_apTransports[i]->paOpts, g_apTransports[i]->cOpts * sizeof(RTGETOPTDEF)); + cOptions += g_apTransports[i]->cOpts; + } + + /* + * Parse the arguments. + */ + RTGETOPTSTATE GetState; + int rc = RTGetOptInit(&GetState, argc, argv, paOptions, cOptions, 1, 0 /* fFlags */); + AssertRC(rc); + + int ch; + RTGETOPTUNION Val; + while ((ch = RTGetOpt(&GetState, &Val))) + { + switch (ch) + { + case 'a': + fAutoUpgrade = true; + break; + + case 'A': + fAutoUpgrade = false; + break; + + case 'c': + rc = RTStrCopy(g_szCdRomPath, sizeof(g_szCdRomPath), Val.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "CD/DVD-ROM is path too long (%Rrc)\n", rc); + break; + + case 'd': + g_fDisplayOutput = true; + break; + + case 'D': + g_fDisplayOutput = false; + break; + + case 'f': + fDaemonize = false; + break; + + case 'h': + txsUsage(g_pStdOut, argv[0]); + *pfExit = true; + return RTEXITCODE_SUCCESS; + + case 's': + rc = RTStrCopy(g_szScratchPath, sizeof(g_szScratchPath), Val.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "scratch path is too long (%Rrc)\n", rc); + break; + + case 't': + { + PCTXSTRANSPORT pTransport = NULL; + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + if (!strcmp(g_apTransports[i]->szName, Val.psz)) + { + pTransport = g_apTransports[i]; + break; + } + if (!pTransport) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unknown transport layer name '%s'\n", Val.psz); + g_pTransport = pTransport; + break; + } + + case 'U': + pszUpgrading = Val.psz; + break; + + case 'w': + cSecsCdWait = Val.u32; + break; + + case 'q': + g_cVerbose = 0; + break; + + case 'v': + g_cVerbose++; + break; + + case 'V': + RTPrintf("$Revision: 155244 $\n"); + *pfExit = true; + return RTEXITCODE_SUCCESS; + + case 'Z': + fDaemonized = true; + fDaemonize = false; + break; + + default: + { + rc = VERR_TRY_AGAIN; + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + if (g_apTransports[i]->cOpts) + { + rc = g_apTransports[i]->pfnOption(ch, &Val); + if (RT_SUCCESS(rc)) + break; + if (rc != VERR_TRY_AGAIN) + { + *pfExit = true; + return RTEXITCODE_SYNTAX; + } + } + if (rc == VERR_TRY_AGAIN) + { + *pfExit = true; + return RTGetOptPrintError(ch, &Val); + } + break; + } + } + } + + /* + * Handle automatic upgrading of the service. + */ + if (fAutoUpgrade && !*pfExit) + { + RTEXITCODE rcExit; + if (pszUpgrading) + rcExit = txsAutoUpdateStage2(argc, argv, pfExit, pszUpgrading); + else + rcExit = txsAutoUpdateStage1(argc, argv, cSecsCdWait, pfExit); + if ( *pfExit + || rcExit != RTEXITCODE_SUCCESS) + return rcExit; + } + + /* + * Daemonize ourselves if asked to. + */ + if (fDaemonize && !*pfExit) + { + if (g_cVerbose > 0) + RTMsgInfo("Daemonizing..."); + rc = RTProcDaemonize(argv, "--daemonized"); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcDaemonize: %Rrc\n", rc); + *pfExit = true; + } + + return RTEXITCODE_SUCCESS; +} + +/** + * @callback_method_impl{FNRTLOGPHASE, Release logger callback} + */ +static DECLCALLBACK(void) logHeaderFooter(PRTLOGGER pLoggerRelease, RTLOGPHASE enmPhase, PFNRTLOGPHASEMSG pfnLog) +{ + /* Some introductory information. */ + static RTTIMESPEC s_TimeSpec; + char szTmp[256]; + if (enmPhase == RTLOGPHASE_BEGIN) + RTTimeNow(&s_TimeSpec); + RTTimeSpecToString(&s_TimeSpec, szTmp, sizeof(szTmp)); + + switch (enmPhase) + { + case RTLOGPHASE_BEGIN: + { + pfnLog(pLoggerRelease, + "TestExecService (Validation Kit TxS) %s r%s (verbosity: %u) %s %s (%s %s) release log\n" + "Copyright (C) " VBOX_C_YEAR " " VBOX_VENDOR "\n\n" + "Log opened %s\n", + RTBldCfgVersion(), RTBldCfgRevisionStr(), g_cVerbose, + KBUILD_TARGET, KBUILD_TARGET_ARCH, + __DATE__, __TIME__, szTmp); + + int vrc = RTSystemQueryOSInfo(RTSYSOSINFO_PRODUCT, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Product: %s\n", szTmp); + vrc = RTSystemQueryOSInfo(RTSYSOSINFO_RELEASE, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Release: %s\n", szTmp); + vrc = RTSystemQueryOSInfo(RTSYSOSINFO_VERSION, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Version: %s\n", szTmp); + vrc = RTSystemQueryOSInfo(RTSYSOSINFO_SERVICE_PACK, szTmp, sizeof(szTmp)); + if (RT_SUCCESS(vrc) || vrc == VERR_BUFFER_OVERFLOW) + pfnLog(pLoggerRelease, "OS Service Pack: %s\n", szTmp); + + /* the package type is interesting for Linux distributions */ + char szExecName[RTPATH_MAX]; + char *pszExecName = RTProcGetExecutablePath(szExecName, sizeof(szExecName)); + pfnLog(pLoggerRelease, + "Executable: %s\n" + "Process ID: %u\n" + "Package type: %s" +#ifdef VBOX_OSE + " (OSE)" +#endif + "\n", + pszExecName ? pszExecName : "unknown", + RTProcSelf(), + VBOX_PACKAGE_STRING); + break; + } + + case RTLOGPHASE_PREROTATE: + pfnLog(pLoggerRelease, "Log rotated - Log started %s\n", szTmp); + break; + + case RTLOGPHASE_POSTROTATE: + pfnLog(pLoggerRelease, "Log continuation - Log started %s\n", szTmp); + break; + + case RTLOGPHASE_END: + pfnLog(pLoggerRelease, "End of log file - Log started %s\n", szTmp); + break; + + default: + /* nothing */ + break; + } +} + +int main(int argc, char **argv) +{ + /* + * Initialize the runtime. + */ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Determine defaults and parse the arguments. + */ + txsSetDefaults(); + bool fExit; + RTEXITCODE rcExit = txsParseArgv(argc, argv, &fExit); + if (rcExit != RTEXITCODE_SUCCESS || fExit) + return rcExit; + + /* + * Enable (release) TxS logging to stdout + file. This is independent from the actual test cases being run. + * + * Keep the log file path + naming predictable (the OS' temp dir) so that we later can retrieve it + * from the host side without guessing much. + * + * If enabling logging fails for some reason, just tell but don't bail out to not make tests fail. + */ + char szLogFile[RTPATH_MAX]; + rc = RTPathTemp(szLogFile, sizeof(szLogFile)); + if (RT_SUCCESS(rc)) + { + rc = RTPathAppend(szLogFile, sizeof(szLogFile), "vbox-txs-release.log"); + if (RT_FAILURE(rc)) + RTMsgError("RTPathAppend failed when constructing log file path: %Rrc\n", rc); + } + else + RTMsgError("RTPathTemp failed when constructing log file path: %Rrc\n", rc); + + if (RT_SUCCESS(rc)) + { + RTUINT fFlags = RTLOGFLAGS_PREFIX_THREAD | RTLOGFLAGS_PREFIX_TIME_PROG; +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + fFlags |= RTLOGFLAGS_USECRLF; +#endif + static const char * const s_apszLogGroups[] = VBOX_LOGGROUP_NAMES; + rc = RTLogCreateEx(&g_pRelLogger, "VBOX_TXS_RELEASE_LOG", fFlags, "all", + RT_ELEMENTS(s_apszLogGroups), s_apszLogGroups, UINT32_MAX /* cMaxEntriesPerGroup */, + 0 /*cBufDescs*/, NULL /* paBufDescs */, RTLOGDEST_STDOUT | RTLOGDEST_FILE, + logHeaderFooter /* pfnPhase */ , + 10 /* cHistory */, 100 * _1M /* cbHistoryFileMax */, RT_SEC_1DAY /* cSecsHistoryTimeSlot */, + NULL /*pOutputIf*/, NULL /*pvOutputIfUser*/, + NULL /* pErrInfo */, "%s", szLogFile); + if (RT_SUCCESS(rc)) + { + RTLogRelSetDefaultInstance(g_pRelLogger); + if (g_cVerbose) + { + RTMsgInfo("Setting verbosity logging to level %u\n", g_cVerbose); + switch (g_cVerbose) /* Not very elegant, but has to do it for now. */ + { + case 1: + rc = RTLogGroupSettings(g_pRelLogger, "all.e.l.l2"); + break; + + case 2: + rc = RTLogGroupSettings(g_pRelLogger, "all.e.l.l2.l3"); + break; + + case 3: + rc = RTLogGroupSettings(g_pRelLogger, "all.e.l.l2.l3.l4"); + break; + + case 4: + RT_FALL_THROUGH(); + default: + rc = RTLogGroupSettings(g_pRelLogger, "all.e.l.l2.l3.l4.f"); + break; + } + if (RT_FAILURE(rc)) + RTMsgError("Setting logging groups failed, rc=%Rrc\n", rc); + } + } + else + RTMsgError("Failed to create release logger: %Rrc", rc); + + if (RT_SUCCESS(rc)) + RTMsgInfo("Log file written to '%s'\n", szLogFile); + } + + /* + * Generate a UUID for this TXS instance. + */ + rc = RTUuidCreate(&g_InstanceUuid); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTUuidCreate failed: %Rrc", rc); + if (g_cVerbose > 0) + RTMsgInfo("Instance UUID: %RTuuid", &g_InstanceUuid); + + /* + * Finalize the scratch directory and initialize the transport layer. + */ + rcExit = txsFinalizeScratch(); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + rc = g_pTransport->pfnInit(); + if (RT_FAILURE(rc)) + return RTEXITCODE_FAILURE; + + /* + * Ok, start working + */ + rcExit = txsMainLoop(); + + /* + * Cleanup. + */ + g_pTransport->pfnTerm(); + + return rcExit; +} diff --git a/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceInternal.h b/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceInternal.h new file mode 100644 index 00000000..3aaf4578 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceInternal.h @@ -0,0 +1,232 @@ +/* $Id: TestExecServiceInternal.h $ */ +/** @file + * TestExecServ - Basic Remote Execution Service, Internal Header. + */ + +/* + * 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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_TestExecServ_TestExecServiceInternal_h +#define VBOX_INCLUDED_SRC_TestExecServ_TestExecServiceInternal_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/getopt.h> +#include <iprt/stream.h> + +RT_C_DECLS_BEGIN + +/** + * Packet header. + */ +typedef struct TXSPKTHDR +{ + /** The unpadded packet length. This include this header. */ + uint32_t cb; + /** The CRC-32 for the packet starting from the opcode field. 0 if the packet + * hasn't been CRCed. */ + uint32_t uCrc32; + /** Packet opcode, an unterminated ASCII string. */ + uint8_t achOpcode[8]; +} TXSPKTHDR; +AssertCompileSize(TXSPKTHDR, 16); +/** Pointer to a packet header. */ +typedef TXSPKTHDR *PTXSPKTHDR; +/** Pointer to a packet header. */ +typedef TXSPKTHDR const *PCTXSPKTHDR; +/** Pointer to a packet header pointer. */ +typedef PTXSPKTHDR *PPTXSPKTHDR; + +/** Packet alignment. */ +#define TXSPKT_ALIGNMENT 16 +/** Max packet size. */ +#define TXSPKT_MAX_SIZE _256K + + +/** + * Transport layer descriptor. + */ +typedef struct TXSTRANSPORT +{ + /** The name. */ + char szName[16]; + /** The description. */ + const char *pszDesc; + /** Pointer to an array of options. */ + PCRTGETOPTDEF paOpts; + /** The number of options in the array. */ + size_t cOpts; + + /** + * Print the usage information for this transport layer. + * + * @param pStream The stream to print the usage info to. + * + * @remarks This is only required if TXSTRANSPORT::cOpts is greater than 0. + */ + DECLR3CALLBACKMEMBER(void, pfnUsage,(PRTSTREAM pStream)); + + /** + * Handle an option. + * + * When encountering an options that is not part of the base options, we'll call + * this method for each transport layer until one handles it. + * + * @retval VINF_SUCCESS if handled. + * @retval VERR_TRY_AGAIN if not handled. + * @retval VERR_INVALID_PARAMETER if we should exit with a non-zero status. + * + * @param ch The short option value. + * @param pVal Pointer to the value union. + * + * @remarks This is only required if TXSTRANSPORT::cOpts is greater than 0. + */ + DECLR3CALLBACKMEMBER(int, pfnOption,(int ch, PCRTGETOPTUNION pVal)); + + /** + * Initializes the transport layer. + * + * @returns IPRT status code. On errors, the transport layer shall call + * RTMsgError to display the error details to the user. + */ + DECLR3CALLBACKMEMBER(int, pfnInit,(void)); + + /** + * Terminate the transport layer, closing and freeing resources. + * + * On errors, the transport layer shall call RTMsgError to display the error + * details to the user. + */ + DECLR3CALLBACKMEMBER(void, pfnTerm,(void)); + + /** + * Polls for incoming packets. + * + * @returns true if there are pending packets, false if there isn't. + */ + DECLR3CALLBACKMEMBER(bool, pfnPollIn,(void)); + + /** + * Adds any pollable handles to the poll set. + * + * This is optional and layers that doesn't have anything that can be polled + * shall set this method pointer to NULL to indicate that pfnPollIn must be used + * instead. + * + * @returns IPRT status code. + * @param hPollSet The poll set to add them to. + * @param idStart The handle ID to start at. + */ + DECLR3CALLBACKMEMBER(int, pfnPollSetAdd,(RTPOLLSET hPollSet, uint32_t idStart)); + + /** + * Receives an incoming packet. + * + * This will block until the data becomes available or we're interrupted by a + * signal or something. + * + * @returns IPRT status code. On error conditions other than VERR_INTERRUPTED, + * the current operation will be aborted when applicable. When + * interrupted, the transport layer will store the data until the next + * receive call. + * + * @param ppPktHdr Where to return the pointer to the packet we've + * read. This is allocated from the heap using + * RTMemAlloc (w/ TXSPKT_ALIGNMENT) and must be + * free by calling RTMemFree. + */ + DECLR3CALLBACKMEMBER(int, pfnRecvPkt,(PPTXSPKTHDR ppPktHdr)); + + /** + * Sends an outgoing packet. + * + * This will block until the data has been written. + * + * @returns IPRT status code. + * @retval VERR_INTERRUPTED if interrupted before anything was sent. + * + * @param pPktHdr The packet to send. The size is given by + * aligning the size in the header by + * TXSPKT_ALIGNMENT. + */ + DECLR3CALLBACKMEMBER(int, pfnSendPkt,(PCTXSPKTHDR pPktHdr)); + + /** + * Sends a babble packet and disconnects the client (if applicable). + * + * @param pPktHdr The packet to send. The size is given by + * aligning the size in the header by + * TXSPKT_ALIGNMENT. + * @param cMsSendTimeout The send timeout measured in milliseconds. + */ + DECLR3CALLBACKMEMBER(void, pfnBabble,(PCTXSPKTHDR pPktHdr, RTMSINTERVAL cMsSendTimeout)); + + /** + * Notification about a client HOWDY. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyHowdy,(void)); + + /** + * Notification about a client BYE. + * + * For connection oriented transport layers, it would be good to disconnect the + * client at this point. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyBye,(void)); + + /** + * Notification about a REBOOT or SHUTDOWN. + * + * For connection oriented transport layers, stop listening for and + * accepting at this point. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyReboot,(void)); + + /** Non-zero end marker. */ + uint32_t u32EndMarker; +} TXSTRANSPORT; +/** Pointer to a const transport layer descriptor. */ +typedef const struct TXSTRANSPORT *PCTXSTRANSPORT; + + +extern TXSTRANSPORT const g_TcpTransport; +extern TXSTRANSPORT const g_SerialTransport; +extern TXSTRANSPORT const g_FileSysTransport; +extern TXSTRANSPORT const g_GuestPropTransport; +extern TXSTRANSPORT const g_TestDevTransport; + +extern uint32_t g_cVerbose; + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_TestExecServ_TestExecServiceInternal_h */ + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceSerial.cpp b/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceSerial.cpp new file mode 100644 index 00000000..5bfa1059 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceSerial.cpp @@ -0,0 +1,417 @@ +/* $Id: TestExecServiceSerial.cpp $ */ +/** @file + * TestExecServ - Basic Remote Execution Service, Serial port Transport Layer. + */ + +/* + * Copyright (C) 2018-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_DEFAULT +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/err.h> +#include <iprt/log.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/string.h> +#include <iprt/serialport.h> +#include <iprt/thread.h> +#include <iprt/time.h> + +#include "TestExecServiceInternal.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** The default baud rate port. */ +#define TXS_SERIAL_DEF_BAUDRATE 115200 +/** The default serial device to use. */ +#if defined(RT_OS_LINUX) +# define TXS_SERIAL_DEF_DEVICE "/dev/ttyS0" +#elif defined(RT_OS_WINDOWS) +# define TXS_SERIAL_DEF_DEVICE "COM1" +#elif defined(RT_OS_SOLARIS) +# define TXS_SERIAL_DEF_DEVICE "<todo>" +#elif defined(RT_OS_FREEBSD) +# define TXS_SERIAL_DEF_DEVICE "<todo>" +#elif defined(RT_OS_DARWIN) +# define TXS_SERIAL_DEF_DEVICE "<todo>" +#else +# error "Port me" +#endif + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** @name Serial Parameters + * @{ */ +/** The addresses to bind to. Empty string means any. */ +static uint32_t g_uSerialBaudRate = TXS_SERIAL_DEF_BAUDRATE; +/** The serial port device to use. */ +static char g_szSerialDevice[256] = TXS_SERIAL_DEF_DEVICE; +/** @} */ + +/** The serial port handle. */ +static RTSERIALPORT g_hSerialPort = NIL_RTSERIALPORT; +/** The size of the stashed data. */ +static size_t g_cbSerialStashed = 0; +/** The size of the stashed data allocation. */ +static size_t g_cbSerialStashedAlloced = 0; +/** The stashed data. */ +static uint8_t *g_pbSerialStashed = NULL; + + + +/** + * @interface_method_impl{TXSTRANSPORT,pfnNotifyReboot} + */ +static DECLCALLBACK(void) txsSerialNotifyReboot(void) +{ + /* nothing to do here */ +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnNotifyBye} + */ +static DECLCALLBACK(void) txsSerialNotifyBye(void) +{ + /* nothing to do here */ +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnNotifyHowdy} + */ +static DECLCALLBACK(void) txsSerialNotifyHowdy(void) +{ + /* nothing to do here */ +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnBabble} + */ +static DECLCALLBACK(void) txsSerialBabble(PCTXSPKTHDR pPktHdr, RTMSINTERVAL cMsSendTimeout) +{ + Assert(g_hSerialPort != NIL_RTSERIALPORT); + + /* + * Try send the babble reply. + */ + NOREF(cMsSendTimeout); /** @todo implement the timeout here; non-blocking write + select-on-write. */ + int rc; + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, TXSPKT_ALIGNMENT); + do rc = RTSerialPortWrite(g_hSerialPort, pPktHdr, cbToSend, NULL); + while (rc == VERR_INTERRUPTED); + + /* + * Disconnect the client. + */ + Log(("txsSerialBabble: RTSerialPortWrite rc=%Rrc\n", rc)); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnSendPkt} + */ +static DECLCALLBACK(int) txsSerialSendPkt(PCTXSPKTHDR pPktHdr) +{ + Assert(g_hSerialPort != NIL_RTSERIALPORT); + Assert(pPktHdr->cb >= sizeof(TXSPKTHDR)); + + /* + * Write it. + */ + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, TXSPKT_ALIGNMENT); + int rc = RTSerialPortWrite(g_hSerialPort, pPktHdr, cbToSend, NULL); + if ( RT_FAILURE(rc) + && rc != VERR_INTERRUPTED) + { + /* assume fatal connection error. */ + Log(("RTSerialPortWrite -> %Rrc\n", rc)); + } + + return rc; +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnRecvPkt} + */ +static DECLCALLBACK(int) txsSerialRecvPkt(PPTXSPKTHDR ppPktHdr) +{ + Assert(g_hSerialPort != NIL_RTSERIALPORT); + + int rc = VINF_SUCCESS; + *ppPktHdr = NULL; + + /* + * Read state. + */ + size_t offData = 0; + size_t cbData = 0; + size_t cbDataAlloced; + uint8_t *pbData = NULL; + + /* + * Any stashed data? + */ + if (g_cbSerialStashedAlloced) + { + offData = g_cbSerialStashed; + cbDataAlloced = g_cbSerialStashedAlloced; + pbData = g_pbSerialStashed; + + g_cbSerialStashed = 0; + g_cbSerialStashedAlloced = 0; + g_pbSerialStashed = NULL; + } + else + { + cbDataAlloced = RT_ALIGN_Z(64, TXSPKT_ALIGNMENT); + pbData = (uint8_t *)RTMemAlloc(cbDataAlloced); + if (!pbData) + return VERR_NO_MEMORY; + } + + /* + * Read and valid the length. + */ + while (offData < sizeof(uint32_t)) + { + size_t cbRead = sizeof(uint32_t) - offData; + rc = RTSerialPortRead(g_hSerialPort, pbData + offData, cbRead, NULL); + if (RT_FAILURE(rc)) + break; + offData += cbRead; + } + if (RT_SUCCESS(rc)) + { + ASMCompilerBarrier(); /* paranoia^3 */ + cbData = *(uint32_t volatile *)pbData; + if (cbData >= sizeof(TXSPKTHDR) && cbData <= TXSPKT_MAX_SIZE) + { + /* + * Align the length and reallocate the return packet it necessary. + */ + cbData = RT_ALIGN_Z(cbData, TXSPKT_ALIGNMENT); + if (cbData > cbDataAlloced) + { + void *pvNew = RTMemRealloc(pbData, cbData); + if (pvNew) + { + pbData = (uint8_t *)pvNew; + cbDataAlloced = cbData; + } + else + rc = VERR_NO_MEMORY; + } + if (RT_SUCCESS(rc)) + { + /* + * Read the remainder of the data. + */ + while (offData < cbData) + { + size_t cbRead = cbData - offData; + rc = RTSerialPortRead(g_hSerialPort, pbData + offData, cbRead, NULL); + if (RT_FAILURE(rc)) + break; + offData += cbRead; + } + } + } + else + rc = VERR_NET_PROTOCOL_ERROR; + } + if (RT_SUCCESS(rc)) + *ppPktHdr = (PTXSPKTHDR)pbData; + else + { + /* + * Deal with errors. + */ + if (rc == VERR_INTERRUPTED) + { + /* stash it away for the next call. */ + g_cbSerialStashed = cbData; + g_cbSerialStashedAlloced = cbDataAlloced; + g_pbSerialStashed = pbData; + } + else + { + RTMemFree(pbData); + + /* assume fatal connection error. */ + Log(("txsSerialRecvPkt: RTSerialPortRead -> %Rrc\n", rc)); + } + } + + return rc; +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnPollIn} + */ +static DECLCALLBACK(bool) txsSerialPollIn(void) +{ + Assert(g_hSerialPort != NIL_RTSERIALPORT); + + uint32_t fEvtsRecv = 0; + int rc = RTSerialPortEvtPoll(g_hSerialPort, RTSERIALPORT_EVT_F_DATA_RX, + &fEvtsRecv, 0/*cMillies*/); + return RT_SUCCESS(rc); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnTerm} + */ +static DECLCALLBACK(void) txsSerialTerm(void) +{ + if (g_hSerialPort != NIL_RTSERIALPORT) + RTSerialPortClose(g_hSerialPort); + + /* Clean up stashing. */ + if (g_pbSerialStashed) + RTMemFree(g_pbSerialStashed); + g_pbSerialStashed = NULL; + g_cbSerialStashed = 0; + g_cbSerialStashedAlloced = 0; + + Log(("txsSerialTerm: done\n")); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnInit} + */ +static DECLCALLBACK(int) txsSerialInit(void) +{ + uint32_t fOpenFlags = RTSERIALPORT_OPEN_F_READ | RTSERIALPORT_OPEN_F_WRITE; + int rc = RTSerialPortOpen(&g_hSerialPort, &g_szSerialDevice[0], fOpenFlags); + if (RT_SUCCESS(rc)) + { + RTSERIALPORTCFG SerPortCfg; + + SerPortCfg.uBaudRate = g_uSerialBaudRate; + SerPortCfg.enmParity = RTSERIALPORTPARITY_NONE; + SerPortCfg.enmDataBitCount = RTSERIALPORTDATABITS_8BITS; + SerPortCfg.enmStopBitCount = RTSERIALPORTSTOPBITS_ONE; + rc = RTSerialPortCfgSet(g_hSerialPort, &SerPortCfg, NULL); + if (RT_FAILURE(rc)) + { + RTMsgError("RTSerialPortCfgSet() failed: %Rrc\n", rc); + RTSerialPortClose(g_hSerialPort); + g_hSerialPort = NIL_RTSERIALPORT; + } + } + else + RTMsgError("RTSerialPortOpen(, %s, %#x) failed: %Rrc\n", + g_szSerialDevice, fOpenFlags, rc); + + return rc; +} + +/** Options */ +enum TXSSERIALOPT +{ + TXSSERIALOPT_BAUDRATE = 1000, + TXSSERIALOPT_DEVICE +}; + +/** + * @interface_method_impl{TXSTRANSPORT,pfnOption} + */ +static DECLCALLBACK(int) txsSerialOption(int ch, PCRTGETOPTUNION pVal) +{ + int rc; + + switch (ch) + { + case TXSSERIALOPT_DEVICE: + rc = RTStrCopy(g_szSerialDevice, sizeof(g_szSerialDevice), pVal->psz); + if (RT_FAILURE(rc)) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "Serial port device path is too long (%Rrc)", rc); + if (!g_szSerialDevice[0]) + strcpy(g_szSerialDevice, TXS_SERIAL_DEF_DEVICE); + return VINF_SUCCESS; + case TXSSERIALOPT_BAUDRATE: + g_uSerialBaudRate = pVal->u32 == 0 ? TXS_SERIAL_DEF_BAUDRATE : pVal->u32; + return VINF_SUCCESS; + } + return VERR_TRY_AGAIN; +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnUsage} + */ +DECLCALLBACK(void) txsSerialUsage(PRTSTREAM pStream) +{ + RTStrmPrintf(pStream, + " --serial-device <device>\n" + " Selects the serial port to use.\n" + " Default: %s\n" + " --serial-baudrate <baudrate>\n" + " Selects the baudrate to set the serial port to.\n" + " Default: %u\n" + , TXS_SERIAL_DEF_DEVICE, TXS_SERIAL_DEF_BAUDRATE); +} + +/** Command line options for the serial transport layer. */ +static const RTGETOPTDEF g_SerialOpts[] = +{ + { "--serial-device", TXSSERIALOPT_DEVICE, RTGETOPT_REQ_STRING }, + { "--serial-baudrate", TXSSERIALOPT_BAUDRATE, RTGETOPT_REQ_UINT32 } +}; + +/** Serial port transport layer. */ +const TXSTRANSPORT g_SerialTransport = +{ + /* .szName = */ "serial", + /* .pszDesc = */ "Serial", + /* .cOpts = */ &g_SerialOpts[0], + /* .paOpts = */ RT_ELEMENTS(g_SerialOpts), + /* .pfnUsage = */ txsSerialUsage, + /* .pfnOption = */ txsSerialOption, + /* .pfnInit = */ txsSerialInit, + /* .pfnTerm = */ txsSerialTerm, + /* .pfnPollIn = */ txsSerialPollIn, + /* .pfnPollSetAdd = */ NULL, + /* .pfnRecvPkt = */ txsSerialRecvPkt, + /* .pfnSendPkt = */ txsSerialSendPkt, + /* .pfnBabble = */ txsSerialBabble, + /* .pfnNotifyHowdy = */ txsSerialNotifyHowdy, + /* .pfnNotifyBye = */ txsSerialNotifyBye, + /* .pfnNotifyReboot = */ txsSerialNotifyReboot, + /* .u32EndMarker = */ UINT32_C(0x12345678) +}; + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceTcp.cpp b/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceTcp.cpp new file mode 100644 index 00000000..2c78477e --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/TestExecServiceTcp.cpp @@ -0,0 +1,842 @@ +/* $Id: TestExecServiceTcp.cpp $ */ +/** @file + * TestExecServ - Basic Remote Execution Service, TCP/IP Transport Layer. + */ + +/* + * 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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_DEFAULT +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/critsect.h> +#include <iprt/err.h> +#include <iprt/log.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/poll.h> +#include <iprt/string.h> +#include <iprt/tcp.h> +#include <iprt/thread.h> +#include <iprt/time.h> + +#include "TestExecServiceInternal.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** The default server port. */ +#define TXS_TCP_DEF_BIND_PORT 5042 +/** The default client port. */ +#define TXS_TCP_DEF_CONNECT_PORT 5048 + +/** The default server bind address. */ +#define TXS_TCP_DEF_BIND_ADDRESS "" +/** The default client connect address (i.e. of the host server). */ +#define TXS_TCP_DEF_CONNECT_ADDRESS "10.0.2.2" + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** @name TCP Parameters + * @{ */ +static enum { TXSTCPMODE_BOTH, TXSTCPMODE_CLIENT, TXSTCPMODE_SERVER } + g_enmTcpMode = TXSTCPMODE_BOTH; + +/** The addresses to bind to. Empty string means any. */ +static char g_szTcpBindAddr[256] = TXS_TCP_DEF_BIND_ADDRESS; +/** The TCP port to listen to. */ +static uint32_t g_uTcpBindPort = TXS_TCP_DEF_BIND_PORT; +/** The addresses to connect to if fRevesedSetupMode is @c true. */ +static char g_szTcpConnectAddr[256] = TXS_TCP_DEF_CONNECT_ADDRESS; +/** The TCP port to listen to. */ +static uint32_t g_uTcpConnectPort = TXS_TCP_DEF_CONNECT_PORT; +/** @} */ + +/** Critical section for serializing access to the next few variables. */ +static RTCRITSECT g_TcpCritSect; +/** Pointer to the TCP server instance. */ +static PRTTCPSERVER g_pTcpServer = NULL; +/** Thread calling RTTcpServerListen2. */ +static RTTHREAD g_hThreadTcpServer = NIL_RTTHREAD; +/** Thread calling RTTcpClientConnect. */ +static RTTHREAD g_hThreadTcpConnect = NIL_RTTHREAD; +/** The main thread handle (for signalling). */ +static RTTHREAD g_hThreadMain = NIL_RTTHREAD; +/** Stop connecting attempts when set. */ +static bool g_fTcpStopConnecting = false; +/** Connect cancel cookie. */ +static PRTTCPCLIENTCONNECTCANCEL volatile g_pTcpConnectCancelCookie = NULL; + +/** Socket of the current client. */ +static RTSOCKET g_hTcpClient = NIL_RTSOCKET; +/** Indicates whether g_hTcpClient comes from the server or from a client + * connect (relevant when closing it). */ +static bool g_fTcpClientFromServer = false; +/** The size of the stashed data. */ +static size_t g_cbTcpStashed = 0; +/** The size of the stashed data allocation. */ +static size_t g_cbTcpStashedAlloced = 0; +/** The stashed data. */ +static uint8_t *g_pbTcpStashed = NULL; + + + +/** + * Disconnects the current client. + */ +static void txsTcpDisconnectClient(void) +{ + int rc; + if (g_fTcpClientFromServer) + rc = RTTcpServerDisconnectClient2(g_hTcpClient); + else + rc = RTTcpClientClose(g_hTcpClient); + AssertRCSuccess(rc); + g_hTcpClient = NIL_RTSOCKET; +} + +/** + * Sets the current client socket in a safe manner. + * + * @returns NIL_RTSOCKET if consumed, other wise hTcpClient. + * @param hTcpClient The client socket. + */ +static RTSOCKET txsTcpSetClient(RTSOCKET hTcpClient) +{ + RTCritSectEnter(&g_TcpCritSect); + if ( g_hTcpClient == NIL_RTSOCKET + && !g_fTcpStopConnecting + && g_hThreadMain != NIL_RTTHREAD + ) + { + g_fTcpClientFromServer = true; + g_hTcpClient = hTcpClient; + int rc = RTThreadUserSignal(g_hThreadMain); AssertRC(rc); + hTcpClient = NIL_RTSOCKET; + } + RTCritSectLeave(&g_TcpCritSect); + return hTcpClient; +} + +/** + * Server mode connection thread. + * + * @returns iprt status code. + * @param hSelf Thread handle. Ignored. + * @param pvUser Ignored. + */ +static DECLCALLBACK(int) txsTcpServerConnectThread(RTTHREAD hSelf, void *pvUser) +{ + RTSOCKET hTcpClient; + int rc = RTTcpServerListen2(g_pTcpServer, &hTcpClient); + Log(("txsTcpConnectServerThread: RTTcpServerListen2 -> %Rrc\n", rc)); + if (RT_SUCCESS(rc)) + { + hTcpClient = txsTcpSetClient(hTcpClient); + RTTcpServerDisconnectClient2(hTcpClient); + } + + RT_NOREF2(hSelf, pvUser); + return rc; +} + +/** + * Checks if it's a fatal RTTcpClientConnect return code. + * + * @returns true / false. + * @param rc The IPRT status code. + */ +static bool txsTcpIsFatalClientConnectStatus(int rc) +{ + return rc != VERR_NET_UNREACHABLE + && rc != VERR_NET_HOST_DOWN + && rc != VERR_NET_HOST_UNREACHABLE + && rc != VERR_NET_CONNECTION_REFUSED + && rc != VERR_TIMEOUT + && rc != VERR_NET_CONNECTION_TIMED_OUT; +} + +/** + * Client mode connection thread. + * + * @returns iprt status code. + * @param hSelf Thread handle. Use to sleep on. The main thread will + * signal it to speed up thread shutdown. + * @param pvUser Ignored. + */ +static DECLCALLBACK(int) txsTcpClientConnectThread(RTTHREAD hSelf, void *pvUser) +{ + RT_NOREF1(pvUser); + + for (;;) + { + /* Stop? */ + RTCritSectEnter(&g_TcpCritSect); + bool fStop = g_fTcpStopConnecting; + RTCritSectLeave(&g_TcpCritSect); + if (fStop) + return VINF_SUCCESS; + + /* Try connect. */ /** @todo make cancelable! */ + RTSOCKET hTcpClient; + Log2(("Calling RTTcpClientConnect(%s, %u,)...\n", g_szTcpConnectAddr, g_uTcpConnectPort)); + int rc = RTTcpClientConnectEx(g_szTcpConnectAddr, g_uTcpConnectPort, &hTcpClient, + RT_SOCKETCONNECT_DEFAULT_WAIT, &g_pTcpConnectCancelCookie); + Log(("txsTcpRecvPkt: RTTcpClientConnect -> %Rrc\n", rc)); + if (RT_SUCCESS(rc)) + { + hTcpClient = txsTcpSetClient(hTcpClient); + RTTcpClientCloseEx(hTcpClient, true /* fGracefulShutdown*/); + break; + } + + if (txsTcpIsFatalClientConnectStatus(rc)) + return rc; + + /* Delay a wee bit before retrying. */ + RTThreadUserWait(hSelf, 1536); + } + return VINF_SUCCESS; +} + +/** + * Wait on the threads to complete. + * + * @returns Thread status (if collected), otherwise VINF_SUCCESS. + * @param cMillies The period to wait on each thread. + */ +static int txsTcpConnectWaitOnThreads(RTMSINTERVAL cMillies) +{ + int rcRet = VINF_SUCCESS; + + if (g_hThreadTcpConnect != NIL_RTTHREAD) + { + int rcThread; + int rc2 = RTThreadWait(g_hThreadTcpConnect, cMillies, &rcThread); + if (RT_SUCCESS(rc2)) + { + g_hThreadTcpConnect = NIL_RTTHREAD; + rcRet = rcThread; + } + } + + if (g_hThreadTcpServer != NIL_RTTHREAD) + { + int rcThread; + int rc2 = RTThreadWait(g_hThreadTcpServer, cMillies, &rcThread); + if (RT_SUCCESS(rc2)) + { + g_hThreadTcpServer = NIL_RTTHREAD; + if (RT_SUCCESS(rc2)) + rcRet = rcThread; + } + } + return rcRet; +} + +/** + * Connects to the peer. + * + * @returns VBox status code. Updates g_hTcpClient and g_fTcpClientFromServer on + * success + */ +static int txsTcpConnect(void) +{ + int rc; + if (g_enmTcpMode == TXSTCPMODE_SERVER) + { + g_fTcpClientFromServer = true; + rc = RTTcpServerListen2(g_pTcpServer, &g_hTcpClient); + Log(("txsTcpRecvPkt: RTTcpServerListen2 -> %Rrc\n", rc)); + } + else if (g_enmTcpMode == TXSTCPMODE_CLIENT) + { + g_fTcpClientFromServer = false; + for (;;) + { + Log2(("Calling RTTcpClientConnect(%s, %u,)...\n", g_szTcpConnectAddr, g_uTcpConnectPort)); + rc = RTTcpClientConnect(g_szTcpConnectAddr, g_uTcpConnectPort, &g_hTcpClient); + Log(("txsTcpRecvPkt: RTTcpClientConnect -> %Rrc\n", rc)); + if (RT_SUCCESS(rc) || txsTcpIsFatalClientConnectStatus(rc)) + break; + + /* Delay a wee bit before retrying. */ + RTThreadSleep(1536); + } + } + else + { + Assert(g_enmTcpMode == TXSTCPMODE_BOTH); + RTTHREAD hSelf = RTThreadSelf(); + + /* + * Create client threads. + */ + RTCritSectEnter(&g_TcpCritSect); + RTThreadUserReset(hSelf); + g_hThreadMain = hSelf; + g_fTcpStopConnecting = false; + RTCritSectLeave(&g_TcpCritSect); + + txsTcpConnectWaitOnThreads(32); + + rc = VINF_SUCCESS; + if (g_hThreadTcpConnect == NIL_RTTHREAD) + { + g_pTcpConnectCancelCookie = NULL; + rc = RTThreadCreate(&g_hThreadTcpConnect, txsTcpClientConnectThread, NULL, 0, RTTHREADTYPE_DEFAULT, + RTTHREADFLAGS_WAITABLE, "tcpconn"); + } + if (g_hThreadTcpServer == NIL_RTTHREAD && RT_SUCCESS(rc)) + rc = RTThreadCreate(&g_hThreadTcpServer, txsTcpServerConnectThread, NULL, 0, RTTHREADTYPE_DEFAULT, + RTTHREADFLAGS_WAITABLE, "tcpserv"); + + RTCritSectEnter(&g_TcpCritSect); + + /* + * Wait for connection to be established. + */ + while ( RT_SUCCESS(rc) + && g_hTcpClient == NIL_RTSOCKET) + { + RTCritSectLeave(&g_TcpCritSect); + RTThreadUserWait(hSelf, 1536); + rc = txsTcpConnectWaitOnThreads(0); + RTCritSectEnter(&g_TcpCritSect); + } + + /* + * Cancel the threads. + */ + g_hThreadMain = NIL_RTTHREAD; + g_fTcpStopConnecting = true; + + RTCritSectLeave(&g_TcpCritSect); + RTTcpClientCancelConnect(&g_pTcpConnectCancelCookie); + } + + AssertMsg(RT_SUCCESS(rc) ? g_hTcpClient != NIL_RTSOCKET : g_hTcpClient == NIL_RTSOCKET, ("%Rrc %p\n", rc, g_hTcpClient)); + g_cbTcpStashed = 0; + return rc; +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnNotifyReboot} + */ +static DECLCALLBACK(void) txsTcpNotifyReboot(void) +{ + Log(("txsTcpNotifyReboot: RTTcpServerDestroy(%p)\n", g_pTcpServer)); + if (g_pTcpServer) + { + int rc = RTTcpServerDestroy(g_pTcpServer); + if (RT_FAILURE(rc)) + RTMsgInfo("RTTcpServerDestroy failed in txsTcpNotifyReboot: %Rrc", rc); + g_pTcpServer = NULL; + } +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnNotifyBye} + */ +static DECLCALLBACK(void) txsTcpNotifyBye(void) +{ + Log(("txsTcpNotifyBye: txsTcpDisconnectClient %RTsock\n", g_hTcpClient)); + txsTcpDisconnectClient(); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnNotifyHowdy} + */ +static DECLCALLBACK(void) txsTcpNotifyHowdy(void) +{ + /* nothing to do here */ +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnBabble} + */ +static DECLCALLBACK(void) txsTcpBabble(PCTXSPKTHDR pPktHdr, RTMSINTERVAL cMsSendTimeout) +{ + /* + * Quietly ignore already disconnected client. + */ + RTSOCKET hTcpClient = g_hTcpClient; + if (hTcpClient == NIL_RTSOCKET) + return; + + /* + * Try send the babble reply. + */ + NOREF(cMsSendTimeout); /** @todo implement the timeout here; non-blocking write + select-on-write. */ + int rc; + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, TXSPKT_ALIGNMENT); + do rc = RTTcpWrite(hTcpClient, pPktHdr, cbToSend); + while (rc == VERR_INTERRUPTED); + + /* + * Disconnect the client. + */ + Log(("txsTcpBabble: txsTcpDisconnectClient(%RTsock) (RTTcpWrite rc=%Rrc)\n", g_hTcpClient, rc)); + txsTcpDisconnectClient(); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnSendPkt} + */ +static DECLCALLBACK(int) txsTcpSendPkt(PCTXSPKTHDR pPktHdr) +{ + Assert(pPktHdr->cb >= sizeof(TXSPKTHDR)); + + /* + * Fail if no client connection. + */ + RTSOCKET hTcpClient = g_hTcpClient; + if (hTcpClient == NIL_RTSOCKET) + return VERR_NET_NOT_CONNECTED; + + /* + * Write it. + */ + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, TXSPKT_ALIGNMENT); + int rc = RTTcpWrite(hTcpClient, pPktHdr, cbToSend); + if ( RT_FAILURE(rc) + && rc != VERR_INTERRUPTED) + { + /* assume fatal connection error. */ + Log(("RTTcpWrite -> %Rrc -> txsTcpDisconnectClient(%RTsock)\n", rc, g_hTcpClient)); + txsTcpDisconnectClient(); + } + + return rc; +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnRecvPkt} + */ +static DECLCALLBACK(int) txsTcpRecvPkt(PPTXSPKTHDR ppPktHdr) +{ + int rc = VINF_SUCCESS; + *ppPktHdr = NULL; + + /* + * Do we have to wait for a client to connect? + */ + RTSOCKET hTcpClient = g_hTcpClient; + if (hTcpClient == NIL_RTSOCKET) + { + rc = txsTcpConnect(); + if (RT_FAILURE(rc)) + return rc; + hTcpClient = g_hTcpClient; Assert(hTcpClient != NIL_RTSOCKET); + } + + /* + * Read state. + */ + size_t offData = 0; + size_t cbData = 0; + size_t cbDataAlloced; + uint8_t *pbData = NULL; + + /* + * Any stashed data? + */ + if (g_cbTcpStashedAlloced) + { + offData = g_cbTcpStashed; + cbDataAlloced = g_cbTcpStashedAlloced; + pbData = g_pbTcpStashed; + + g_cbTcpStashed = 0; + g_cbTcpStashedAlloced = 0; + g_pbTcpStashed = NULL; + } + else + { + cbDataAlloced = RT_ALIGN_Z(64, TXSPKT_ALIGNMENT); + pbData = (uint8_t *)RTMemAlloc(cbDataAlloced); + if (!pbData) + return VERR_NO_MEMORY; + } + + /* + * Read and valid the length. + */ + while (offData < sizeof(uint32_t)) + { + size_t cbRead; + rc = RTTcpRead(hTcpClient, pbData + offData, sizeof(uint32_t) - offData, &cbRead); + if (RT_FAILURE(rc)) + break; + if (cbRead == 0) + { + Log(("txsTcpRecvPkt: RTTcpRead -> %Rrc / cbRead=0 -> VERR_NET_NOT_CONNECTED (#1)\n", rc)); + rc = VERR_NET_NOT_CONNECTED; + break; + } + offData += cbRead; + } + if (RT_SUCCESS(rc)) + { + ASMCompilerBarrier(); /* paranoia^3 */ + cbData = *(uint32_t volatile *)pbData; + if (cbData >= sizeof(TXSPKTHDR) && cbData <= TXSPKT_MAX_SIZE) + { + /* + * Align the length and reallocate the return packet it necessary. + */ + cbData = RT_ALIGN_Z(cbData, TXSPKT_ALIGNMENT); + if (cbData > cbDataAlloced) + { + void *pvNew = RTMemRealloc(pbData, cbData); + if (pvNew) + { + pbData = (uint8_t *)pvNew; + cbDataAlloced = cbData; + } + else + rc = VERR_NO_MEMORY; + } + if (RT_SUCCESS(rc)) + { + /* + * Read the remainder of the data. + */ + while (offData < cbData) + { + size_t cbRead; + rc = RTTcpRead(hTcpClient, pbData + offData, cbData - offData, &cbRead); + if (RT_FAILURE(rc)) + break; + if (cbRead == 0) + { + Log(("txsTcpRecvPkt: RTTcpRead -> %Rrc / cbRead=0 -> VERR_NET_NOT_CONNECTED (#2)\n", rc)); + rc = VERR_NET_NOT_CONNECTED; + break; + } + offData += cbRead; + } + } + } + else + rc = VERR_NET_PROTOCOL_ERROR; + } + if (RT_SUCCESS(rc)) + *ppPktHdr = (PTXSPKTHDR)pbData; + else + { + /* + * Deal with errors. + */ + if (rc == VERR_INTERRUPTED) + { + /* stash it away for the next call. */ + g_cbTcpStashed = cbData; + g_cbTcpStashedAlloced = cbDataAlloced; + g_pbTcpStashed = pbData; + } + else + { + RTMemFree(pbData); + + /* assume fatal connection error. */ + Log(("txsTcpRecvPkt: RTTcpRead -> %Rrc -> txsTcpDisconnectClient(%RTsock)\n", rc, g_hTcpClient)); + txsTcpDisconnectClient(); + } + } + + return rc; +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnPollSetAdd} + */ +static DECLCALLBACK(int) txsTcpPollSetAdd(RTPOLLSET hPollSet, uint32_t idStart) +{ + return RTPollSetAddSocket(hPollSet, g_hTcpClient, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, idStart); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnPollIn} + */ +static DECLCALLBACK(bool) txsTcpPollIn(void) +{ + RTSOCKET hTcpClient = g_hTcpClient; + if (hTcpClient == NIL_RTSOCKET) + return false; + int rc = RTTcpSelectOne(hTcpClient, 0/*cMillies*/); + return RT_SUCCESS(rc); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnTerm} + */ +static DECLCALLBACK(void) txsTcpTerm(void) +{ + /* Signal thread */ + if (RTCritSectIsInitialized(&g_TcpCritSect)) + { + RTCritSectEnter(&g_TcpCritSect); + g_fTcpStopConnecting = true; + RTCritSectLeave(&g_TcpCritSect); + } + + if (g_hThreadTcpConnect != NIL_RTTHREAD) + { + RTThreadUserSignal(g_hThreadTcpConnect); + RTTcpClientCancelConnect(&g_pTcpConnectCancelCookie); + } + + /* Shut down the server (will wake up thread). */ + if (g_pTcpServer) + { + Log(("txsTcpTerm: Destroying server...\n")); + int rc = RTTcpServerDestroy(g_pTcpServer); + if (RT_FAILURE(rc)) + RTMsgInfo("RTTcpServerDestroy failed in txsTcpTerm: %Rrc", rc); + g_pTcpServer = NULL; + } + + /* Shut down client */ + if (g_hTcpClient != NIL_RTSOCKET) + { + if (g_fTcpClientFromServer) + { + Log(("txsTcpTerm: Disconnecting client...\n")); + int rc = RTTcpServerDisconnectClient2(g_hTcpClient); + if (RT_FAILURE(rc)) + RTMsgInfo("RTTcpServerDisconnectClient2(%RTsock) failed in txsTcpTerm: %Rrc", g_hTcpClient, rc); + } + else + { + int rc = RTTcpClientClose(g_hTcpClient); + if (RT_FAILURE(rc)) + RTMsgInfo("RTTcpClientClose(%RTsock) failed in txsTcpTerm: %Rrc", g_hTcpClient, rc); + } + g_hTcpClient = NIL_RTSOCKET; + } + + /* Clean up stashing. */ + RTMemFree(g_pbTcpStashed); + g_pbTcpStashed = NULL; + g_cbTcpStashed = 0; + g_cbTcpStashedAlloced = 0; + + /* Wait for the thread (they should've had some time to quit by now). */ + txsTcpConnectWaitOnThreads(15000); + + /* Finally, clean up the critical section. */ + if (RTCritSectIsInitialized(&g_TcpCritSect)) + RTCritSectDelete(&g_TcpCritSect); + + Log(("txsTcpTerm: done\n")); +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnInit} + */ +static DECLCALLBACK(int) txsTcpInit(void) +{ + int rc = RTCritSectInit(&g_TcpCritSect); + if (RT_SUCCESS(rc) && g_enmTcpMode != TXSTCPMODE_CLIENT) + { + rc = RTTcpServerCreateEx(g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, &g_pTcpServer); + if (RT_FAILURE(rc)) + { + if (rc == VERR_NET_DOWN) + { + RTMsgInfo("RTTcpServerCreateEx(%s, %u,) failed: %Rrc, retrying for 20 seconds...\n", + g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, rc); + uint64_t StartMs = RTTimeMilliTS(); + do + { + RTThreadSleep(1000); + rc = RTTcpServerCreateEx(g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, &g_pTcpServer); + } while ( rc == VERR_NET_DOWN + && RTTimeMilliTS() - StartMs < 20000); + if (RT_SUCCESS(rc)) + RTMsgInfo("RTTcpServerCreateEx succceeded.\n"); + } + if (RT_FAILURE(rc)) + { + g_pTcpServer = NULL; + RTCritSectDelete(&g_TcpCritSect); + RTMsgError("RTTcpServerCreateEx(%s, %u,) failed: %Rrc\n", + g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, rc); + } + } + } + + return rc; +} + +/** Options */ +enum TXSTCPOPT +{ + TXSTCPOPT_MODE = 1000, + TXSTCPOPT_BIND_ADDRESS, + TXSTCPOPT_BIND_PORT, + TXSTCPOPT_CONNECT_ADDRESS, + TXSTCPOPT_CONNECT_PORT, + + /* legacy: */ + TXSTCPOPT_LEGACY_PORT, + TXSTCPOPT_LEGACY_CONNECT +}; + +/** + * @interface_method_impl{TXSTRANSPORT,pfnOption} + */ +static DECLCALLBACK(int) txsTcpOption(int ch, PCRTGETOPTUNION pVal) +{ + int rc; + + switch (ch) + { + case TXSTCPOPT_MODE: + if (!strcmp(pVal->psz, "both")) + g_enmTcpMode = TXSTCPMODE_BOTH; + else if (!strcmp(pVal->psz, "client")) + g_enmTcpMode = TXSTCPMODE_CLIENT; + else if (!strcmp(pVal->psz, "server")) + g_enmTcpMode = TXSTCPMODE_SERVER; + else + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "Invalid TCP mode: '%s'\n", pVal->psz); + return VINF_SUCCESS; + + case TXSTCPOPT_BIND_ADDRESS: + rc = RTStrCopy(g_szTcpBindAddr, sizeof(g_szTcpBindAddr), pVal->psz); + if (RT_FAILURE(rc)) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "TCP bind address is too long (%Rrc)", rc); + return VINF_SUCCESS; + + case TXSTCPOPT_BIND_PORT: + g_uTcpBindPort = pVal->u16 == 0 ? TXS_TCP_DEF_BIND_PORT : pVal->u16; + return VINF_SUCCESS; + + case TXSTCPOPT_LEGACY_CONNECT: + g_enmTcpMode = TXSTCPMODE_CLIENT; + RT_FALL_THRU(); + case TXSTCPOPT_CONNECT_ADDRESS: + rc = RTStrCopy(g_szTcpConnectAddr, sizeof(g_szTcpConnectAddr), pVal->psz); + if (RT_FAILURE(rc)) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "TCP connect address is too long (%Rrc)", rc); + if (!g_szTcpConnectAddr[0]) + strcpy(g_szTcpConnectAddr, TXS_TCP_DEF_CONNECT_ADDRESS); + return VINF_SUCCESS; + + case TXSTCPOPT_CONNECT_PORT: + g_uTcpConnectPort = pVal->u16 == 0 ? TXS_TCP_DEF_CONNECT_PORT : pVal->u16; + return VINF_SUCCESS; + + case TXSTCPOPT_LEGACY_PORT: + if (pVal->u16 == 0) + { + g_uTcpBindPort = TXS_TCP_DEF_BIND_PORT; + g_uTcpConnectPort = TXS_TCP_DEF_CONNECT_PORT; + } + else + { + g_uTcpBindPort = pVal->u16; + g_uTcpConnectPort = pVal->u16; + } + return VINF_SUCCESS; + } + return VERR_TRY_AGAIN; +} + +/** + * @interface_method_impl{TXSTRANSPORT,pfnUsage} + */ +DECLCALLBACK(void) txsTcpUsage(PRTSTREAM pStream) +{ + RTStrmPrintf(pStream, + " --tcp-mode <both|client|server>\n" + " Selects the mode of operation.\n" + " Default: both\n" + " --tcp-bind-address <address>\n" + " The address(es) to listen to TCP connection on. Empty string\n" + " means any address, this is the default.\n" + " --tcp-bind-port <port>\n" + " The port to listen to TCP connections on.\n" + " Default: %u\n" + " --tcp-connect-address <address>\n" + " The address of the server to try connect to in client mode.\n" + " Default: " TXS_TCP_DEF_CONNECT_ADDRESS "\n" + " --tcp-connect-port <port>\n" + " The port on the server to connect to in client mode.\n" + " Default: %u\n" + , TXS_TCP_DEF_BIND_PORT, TXS_TCP_DEF_CONNECT_PORT); +} + +/** Command line options for the TCP/IP transport layer. */ +static const RTGETOPTDEF g_TcpOpts[] = +{ + { "--tcp-mode", TXSTCPOPT_MODE, RTGETOPT_REQ_STRING }, + { "--tcp-bind-address", TXSTCPOPT_BIND_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-bind-port", TXSTCPOPT_BIND_PORT, RTGETOPT_REQ_UINT16 }, + { "--tcp-connect-address", TXSTCPOPT_CONNECT_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-connect-port", TXSTCPOPT_CONNECT_PORT, RTGETOPT_REQ_UINT16 }, + + /* legacy */ + { "--tcp-port", TXSTCPOPT_LEGACY_PORT, RTGETOPT_REQ_UINT16 }, + { "--tcp-connect", TXSTCPOPT_LEGACY_CONNECT, RTGETOPT_REQ_STRING }, +}; + +/** TCP/IP transport layer. */ +const TXSTRANSPORT g_TcpTransport = +{ + /* .szName = */ "tcp", + /* .pszDesc = */ "TCP/IP", + /* .cOpts = */ &g_TcpOpts[0], + /* .paOpts = */ RT_ELEMENTS(g_TcpOpts), + /* .pfnUsage = */ txsTcpUsage, + /* .pfnOption = */ txsTcpOption, + /* .pfnInit = */ txsTcpInit, + /* .pfnTerm = */ txsTcpTerm, + /* .pfnPollIn = */ txsTcpPollIn, + /* .pfnPollSetAdd = */ txsTcpPollSetAdd, + /* .pfnRecvPkt = */ txsTcpRecvPkt, + /* .pfnSendPkt = */ txsTcpSendPkt, + /* .pfnBabble = */ txsTcpBabble, + /* .pfnNotifyHowdy = */ txsTcpNotifyHowdy, + /* .pfnNotifyBye = */ txsTcpNotifyBye, + /* .pfnNotifyReboot = */ txsTcpNotifyReboot, + /* .u32EndMarker = */ UINT32_C(0x12345678) +}; + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-nat.sh b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-nat.sh new file mode 100755 index 00000000..c666e480 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-nat.sh @@ -0,0 +1,174 @@ +#!/bin/sh +## @file +# VirtualBox Test Execution Service Init Script for NATted setups. +# + +# +# Copyright (C) 2006-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +# chkconfig: 35 35 65 +# description: VirtualBox Test Execution Service +# +### BEGIN INIT INFO +# Provides: vboxtxs +# Required-Start: $network +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: VirtualBox Test Execution Service +### END INIT INFO + +PATH=$PATH:/bin:/sbin:/usr/sbin +SCRIPTNAME=vboxtxs-nat.sh + +CDROM_PATH=/media/cdrom +SCRATCH_PATH=/tmp/vboxtxs-scratch + +PIDFILE="/var/run/vboxtxs" + +# Preamble for Gentoo +if [ "`which $0`" = "/sbin/rc" ]; then + shift +fi + +begin_msg() +{ + test -n "${2}" && echo "${SCRIPTNAME}: ${1}." + logger -t "${SCRIPTNAME}" "${1}." +} + +succ_msg() +{ + logger -t "${SCRIPTNAME}" "${1}." +} + +fail_msg() +{ + echo "${SCRIPTNAME}: failed: ${1}." >&2 + logger -t "${SCRIPTNAME}" "failed: ${1}." +} + +killproc() { + kp_binary="${1##*/}" + pkill "${kp_binary}" || return 0 + sleep 1 + pkill "${kp_binary}" || return 0 + sleep 1 + pkill -9 "${kp_binary}" + return 0 +} + +case "`uname -m`" in + AMD64|amd64|X86_64|x86_64) + binary=/opt/validationkit/linux/amd64/TestExecService + ;; + + i386|x86|i486|i586|i686|i787) + binary=/opt/validationkit/linux/x86/TestExecService + ;; + + *) + binary=/opt/validationkit/linux/x86/TestExecService + ;; +esac + +fixAndTestBinary() { + chmod a+x "$binary" 2> /dev/null > /dev/null + test -x "$binary" || { + echo "Cannot run $binary" + exit 1 + } +} + +start() { + if ! test -f $PIDFILE; then + begin_msg "Starting VirtualBox Test Execution Service" console + fixAndTestBinary + mount /dev/cdrom "${CDROM_PATH}" 2> /dev/null > /dev/null + $binary --auto-upgrade --scratch="${SCRATCH_PATH}" --cdrom="${CDROM_PATH}" \ + --no-display-output --tcp-connect 10.0.2.2 > /dev/null + RETVAL=$? + test $RETVAL -eq 0 && sleep 2 && echo `pidof TestExecService` > $PIDFILE + if ! test -s "${PIDFILE}"; then + RETVAL=5 + fi + if test $RETVAL -eq 0; then + succ_msg "VirtualBox Test Execution service started" + else + fail_msg "VirtualBox Test Execution service failed to start" + fi + fi + return $RETVAL +} + +stop() { + if test -f $PIDFILE; then + begin_msg "Stopping VirtualBox Test Execution Service" console + killproc $binary + fi +} + +restart() { + stop && start +} + +status() { + echo -n "Checking for vboxtxs" + if [ -f $PIDFILE ]; then + echo " ...running" + else + echo " ...not running" + fi +} + +case "$1" in +start) + start + ;; +stop) + stop + ;; +restart) + restart + ;; +status) + status + ;; +setup) + ;; +cleanup) + ;; +*) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 +esac + +exit $RETVAL diff --git a/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-runvm.desktop b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-runvm.desktop new file mode 100644 index 00000000..95e7ddcb --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-runvm.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +Encoding=UTF-8 +Name=vboxtxs +Exec=sudo sh -c "/opt/validationkit/linux/vboxtxs-runvm start" + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-runvm.sh b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-runvm.sh new file mode 100755 index 00000000..e44b1546 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs-runvm.sh @@ -0,0 +1,208 @@ +#!/bin/sh +## @file +# VirtualBox Test Execution Service Init Script. +# + +# +# Copyright (C) 2018-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +# chkconfig: 35 35 65 +# description: VirtualBox Test Execution Service +# +### BEGIN INIT INFO +# Provides: vboxtxs-runvm +# Required-Start: $ALL +# Required-Stop: +# Default-Start: 5 +# Default-Stop: 0 1 6 +# Description: VirtualBox Test Execution Service +### END INIT INFO + +PATH=$PATH:/bin:/sbin:/usr/sbin +SCRIPTNAME=vboxtxs-runvm.sh + +CDROM_PATH=/media/cdrom +SCRATCH_PATH=/tmp/vboxtxs-scratch +SMOKEOUTPUT_PATH=/tmp/vboxtxs-smoketestoutput +DEVKMSG_PATH=/dev/kmsg + +PIDFILE="/var/run/vboxtxs" + +export TESTBOX_PATH_RESOURCES="/home/vbox/testrsrc" +SMOKETEST_SCRIPT="/opt/validationkit/tests/smoketests/tdSmokeTest1.py" +PYTHON_BINARY="python" + +# Preamble for Gentoo +if [ "`which $0`" = "/sbin/rc" ]; then + shift +fi + +kernlog_msg() { + test -n "$2" && echo "${SCRIPTNAME}: ${1}" + echo "${SCRIPTNAME}: ${1}" > $DEVKMSG_PATH +} + +dumpfile_to_kernlog() { + if test -f "$1"; then + kernlog_msg "---------------------- DUMP BEGIN ----------------------" + cat "$1" | while read LINE + do + kernlog_msg "${LINE}" + done + kernlog_msg "---------------------- DUMP END ------------------------" + rm -f "$1" + else + kernlog_msg "${1}: file not found" console + fi +} + +killproc() +{ + kp_binary="${1##*/}" + pkill "${kp_binary}" || return 0 + sleep 1 + pkill "${kp_binary}" || return 0 + sleep 1 + pkill -9 "${kp_binary}" + return 0 +} + +case "`uname -m`" in + AMD64|amd64|X86_64|x86_64) + binary=/opt/validationkit/linux/amd64/TestExecService + ;; + + i386|x86|i486|i586|i686|i787) + binary=/opt/validationkit/linux/x86/TestExecService + ;; + + *) + binary=/opt/validationkit/linux/x86/TestExecService + ;; +esac + +fixAndTestBinary() { + chmod a+x "$binary" 2> /dev/null > /dev/null + test -x "$binary" || { + echo "Cannot run $binary" + exit 1 + } +} + +testRsrcPath() { + test -d "$TESTBOX_PATH_RESOURCES" || { + echo "TESTBOX_PATH_RESOURCES directory not found" + exit 1 + } +} + +start() { + if ! test -f $PIDFILE; then + kernlog_msg "Starting Nested Smoke Test" console + fixAndTestBinary + testRsrcPath + $PYTHON_BINARY $SMOKETEST_SCRIPT -v -v -d --vbox-session-type gui --nic-attachment nat --quick all 1> "${SMOKEOUTPUT_PATH}" 2>&1 + RETVAL=$? + dumpfile_to_kernlog "${SMOKEOUTPUT_PATH}" + sync + sleep 15 + if test $RETVAL -eq 0; then + kernlog_msg "Nested Smoke Test done; Starting Test Execution service" console + mkdir -p "${CDROM_PATH}" + mount -o ro /dev/cdrom "${CDROM_PATH}" 2> /dev/null > /dev/null + $binary --auto-upgrade --scratch="${SCRATCH_PATH}" --cdrom="${CDROM_PATH}" --no-display-output > /dev/null + RETVAL=$? + test $RETVAL -eq 0 && sleep 3 && echo `pidof TestExecService` > $PIDFILE + if ! test -s "${PIDFILE}"; then + RETVAL=5 + fi + if test $RETVAL -eq 0; then + kernlog_msg "Test Execution service started" console + else + kernlog_msg "Test Execution service failed to start" console + RETVAL=6 + fi + else + kernlog_msg "Smoke Test failed! error code ${RETVAL}" console + RETVAL=7 + fi + else + kernlog_msg "Starting Nested Smoke Test failed! Pidfile ${PIDFILE} exists" console + RETVAL=9 + fi + return $RETVAL +} + +stop() { + if test -f $PIDFILE; then + kernlog_msg "Stopping Test Execution Service" + killproc $binary + rm -f $PIDFILE + fi +} + +restart() { + stop && start +} + +status() { + echo -n "Checking for vboxtxs" + if [ -f $PIDFILE ]; then + echo " ...running" + else + echo " ...not running" + fi +} + +case "$1" in +start) + start + ;; +stop) + stop + ;; +restart) + restart + ;; +status) + status + ;; +setup) + ;; +cleanup) + ;; +*) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 +esac + +exit $RETVAL + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs.service b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs.service new file mode 100644 index 00000000..b61426e9 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs.service @@ -0,0 +1,18 @@ +[Unit] +Description=VirtualBox Test Execution Service +SourcePath=/opt/validationkit/linux/vboxtxs + +[Service] +Type=forking +Restart=no +TimeoutSec=5min +IgnoreSIGPIPE=no +KillMode=process +GuessMainPID=no +RemainAfterExit=yes +ExecStart=/opt/validationkit/linux/vboxtxs start +ExecStop=/opt/validationkit/linux/vboxtxs stop + +[Install] +WantedBy=multi-user.target + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs.sh b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs.sh new file mode 100755 index 00000000..887ac2d2 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/linux/vboxtxs.sh @@ -0,0 +1,173 @@ +#!/bin/sh +## @file +# VirtualBox Test Execution Service Init Script. +# + +# +# Copyright (C) 2006-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +# chkconfig: 35 35 65 +# description: VirtualBox Test Execution Service +# +### BEGIN INIT INFO +# Provides: vboxtxs +# Required-Start: $network +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: VirtualBox Test Execution Service +### END INIT INFO + +PATH=$PATH:/bin:/sbin:/usr/sbin +SCRIPTNAME=vboxtxs.sh + +CDROM_PATH=/media/cdrom +SCRATCH_PATH=/tmp/vboxtxs-scratch + +PIDFILE="/var/run/vboxtxs" + +# Preamble for Gentoo +if [ "`which $0`" = "/sbin/rc" ]; then + shift +fi + +begin_msg() +{ + test -n "${2}" && echo "${SCRIPTNAME}: ${1}." + logger -t "${SCRIPTNAME}" "${1}." +} + +succ_msg() +{ + logger -t "${SCRIPTNAME}" "${1}." +} + +fail_msg() +{ + echo "${SCRIPTNAME}: failed: ${1}." >&2 + logger -t "${SCRIPTNAME}" "failed: ${1}." +} + +killproc() { + kp_binary="${1##*/}" + pkill "${kp_binary}" || return 0 + sleep 1 + pkill "${kp_binary}" || return 0 + sleep 1 + pkill -9 "${kp_binary}" + return 0 +} + +case "`uname -m`" in + AMD64|amd64|X86_64|x86_64) + binary=/opt/validationkit/linux/amd64/TestExecService + ;; + + i386|x86|i486|i586|i686|i787) + binary=/opt/validationkit/linux/x86/TestExecService + ;; + + *) + binary=/opt/validationkit/linux/x86/TestExecService + ;; +esac + +fixAndTestBinary() { + chmod a+x "$binary" 2> /dev/null > /dev/null + test -x "$binary" || { + echo "Cannot run $binary" + exit 1 + } +} + +start() { + if ! test -f $PIDFILE; then + begin_msg "Starting VirtualBox Test Execution Service" console + fixAndTestBinary + mount /dev/cdrom "${CDROM_PATH}" 2> /dev/null > /dev/null + $binary --auto-upgrade --scratch="${SCRATCH_PATH}" --cdrom="${CDROM_PATH}" --no-display-output > /dev/null + RETVAL=$? + test $RETVAL -eq 0 && sleep 2 && echo `pidof TestExecService` > $PIDFILE + if ! test -s "${PIDFILE}"; then + RETVAL=5 + fi + if test $RETVAL -eq 0; then + succ_msg "VirtualBox Test Execution service started" + else + fail_msg "VirtualBox Test Execution service failed to start" + fi + fi + return $RETVAL +} + +stop() { + if test -f $PIDFILE; then + begin_msg "Stopping VirtualBox Test Execution Service" console + killproc $binary + fi +} + +restart() { + stop && start +} + +status() { + echo -n "Checking for vboxtxs" + if [ -f $PIDFILE ]; then + echo " ...running" + else + echo " ...not running" + fi +} + +case "$1" in +start) + start + ;; +stop) + stop + ;; +restart) + restart + ;; +status) + status + ;; +setup) + ;; +cleanup) + ;; +*) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 +esac + +exit $RETVAL diff --git a/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs-sol10.xml b/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs-sol10.xml new file mode 100644 index 00000000..a93901f6 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs-sol10.xml @@ -0,0 +1,84 @@ +<?xml version='1.0'?> +<!-- + Solaris SMF service manifest for the VirtualBox Test eXecution Service. + $Id: vboxtxs-sol10.xml $ +--> +<!-- + 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 <https://www.gnu.org/licenses>. + + The contents of this file may alternatively be used under the terms + of the Common Development and Distribution License Version 1.0 + (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + in the VirtualBox distribution, in which case the provisions of the + CDDL are applicable instead of those of the GPL. + + You may elect to license modified versions of this file under the + terms and conditions of either the GPL or the CDDL or both. + + SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +--> +<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1"> + +<service_bundle type='manifest' name='export'> +<service name='system/virtualbox/vboxtxs' type='service' version='1'> + + <create_default_instance enabled='false' /> + <single_instance/> + + <!-- Wait for the network to start up --> + <dependency name='milestone-network' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/network:default' /> + </dependency> + + <!-- Wait for devices to be initialized as we depend on vboxguest (PCI) --> + <dependency name='milestone-devices' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/devices:default' /> + </dependency> + + <!-- We wish to be started as late as possible... so go crazy with deps. --> + <dependency name='multi-user' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/multi-user:default' /> + </dependency> + <dependency name='multi-user-server' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/multi-user-server:default' /> + </dependency> + <dependency name='filesystem-local' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/system/filesystem/local:default' /> + </dependency> + <dependency name='filesystem-autofs' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/system/filesystem/autofs:default' /> + </dependency> + <dependency name='filesystem-volfs' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/system/filesystem/volfs:default' /> + </dependency> + + <!-- start + stop methods --> + <exec_method type='method' name='start' exec='/opt/VBoxTest/testsuite/solaris/vboxtxs.sh' timeout_seconds='30' /> + <exec_method type='method' name='stop' exec=':kill' timeout_seconds='60' /> + + <!-- Description --> + <template> + <common_name> + <loctext xml:lang='C'>VirtualBox Test eXecution Service</loctext> + </common_name> + </template> +</service> + +</service_bundle> + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs.sh b/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs.sh new file mode 100755 index 00000000..f9f5b9ad --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs.sh @@ -0,0 +1,64 @@ +#!/bin/sh +## @file +# VirtualBox Test Execution Service Architecture Wrapper for Solaris. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +# 1. Change directory to the script directory (usually /opt/VBoxTest/). +set -x +MY_DIR=`dirname "$0"` +cd "${MY_DIR}" + +# 2. Determine the architecture. +MY_ARCH=`isainfo -k` +case "${MY_ARCH}" in + amd64) + MY_ARCH=amd64 + ;; + i386) + MY_ARCH=x86 + ;; + *) + echo "vboxtxs.sh: Unsupported architecture '${MY_ARCH}' returned by isainfo -k." >&2 + exit 2; + ;; +esac + +# 3. Exec the service. +exec "./${MY_ARCH}/TestExecService" \ + --cdrom="/cdrom/cdrom0/" \ + --scratch="/var/tmp/VBoxTest/" \ + --no-display-output \ + $* +exit 3; + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs.xml b/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs.xml new file mode 100644 index 00000000..6bbc614e --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/solaris/vboxtxs.xml @@ -0,0 +1,84 @@ +<?xml version='1.0'?> +<!-- + Solaris SMF service manifest for the VirtualBox Test eXecution Service. + $Id: vboxtxs.xml $ +--> +<!-- + 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 <https://www.gnu.org/licenses>. + + The contents of this file may alternatively be used under the terms + of the Common Development and Distribution License Version 1.0 + (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + in the VirtualBox distribution, in which case the provisions of the + CDDL are applicable instead of those of the GPL. + + You may elect to license modified versions of this file under the + terms and conditions of either the GPL or the CDDL or both. + + SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +--> +<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1"> + +<service_bundle type='manifest' name='export'> +<service name='system/virtualbox/vboxtxs' type='service' version='1'> + + <create_default_instance enabled='false' /> + <single_instance/> + + <!-- Wait for the network to start up --> + <dependency name='milestone-network' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/network:default' /> + </dependency> + + <!-- Wait for devices to be initialized as we depend on vboxguest (PCI) --> + <dependency name='milestone-devices' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/devices:default' /> + </dependency> + + <!-- We wish to be started as late as possible... so go crazy with deps. --> + <dependency name='multi-user' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/multi-user:default' /> + </dependency> + <dependency name='multi-user-server' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/milestone/multi-user-server:default' /> + </dependency> + <dependency name='filesystem-local' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/system/filesystem/local:default' /> + </dependency> + <dependency name='filesystem-autofs' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/system/filesystem/autofs:default' /> + </dependency> + <dependency name='filesystem-rmvolmgr' grouping='require_all' restart_on='none' type='service'> + <service_fmri value='svc:/system/filesystem/rmvolmgr:default' /> + </dependency> + + <!-- start + stop methods --> + <exec_method type='method' name='start' exec='/opt/VBoxTest/testsuite/solaris/vboxtxs.sh' timeout_seconds='30' /> + <exec_method type='method' name='stop' exec=':kill' timeout_seconds='60' /> + + <!-- Description --> + <template> + <common_name> + <loctext xml:lang='C'>VirtualBox Test eXecution Service</loctext> + </common_name> + </template> +</service> + +</service_bundle> + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/vboxtxs-readme.txt b/src/VBox/ValidationKit/utils/TestExecServ/vboxtxs-readme.txt new file mode 100644 index 00000000..70de141f --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/vboxtxs-readme.txt @@ -0,0 +1,145 @@ +$Id: vboxtxs-readme.txt $ + + +VirtualBox Test eXecution Service +================================= + +This readme briefly describes how to install the Test eXecution Service (TXS) +on the various systems. + +There are currently two transport options for the TXS: + + - The default is to use it in TCP server mode, i.e. the test script needs + to know the guest's IP and therefore requires guest additions to be + installed as well. (Please use the latest stable additions compatible with + the VBox host versions you intend to test.) + + - The alternative is for NATted setups where TXS will act like a TCP client + and try connect to the test script on the host. Since this require that + TXS knows which IP to connect to, it's only really possible in a NATted + setup where we know the host IP is 10.0.2.2. + +Since r85596 TXS operates in both modes by default so the nat version of +the init scripts is not required anymore. Instead the other type can be installed +for both cases. + +Linux Installation +------------------ + +1. mkdir -p /opt/validationkit +2. scp/download VBoxValidationKit*.zip there. +3. unzip VBoxValidationKit*.zip +4. chmod -R u+w,a+x /opt/validationkit/ && chown -R root.root /opt/ +5. cd /etc/init.d/ + +6 a) For init.rc distros: + Link up the right init script (see connection type above): + nat) ln -s ../../opt/validationkit/linux/vboxtxs-nat ./vboxtxs + other) ln -s ../../opt/validationkit/linux/vboxtxs ./vboxtxs +6 b) Add vboxtxs to runlevels 2, 3, 5 and any other that makes sense + on the distro. There is usually some command for doing this, e.g. + ```update-rc.d vboxtxs defaults && update-rc.d vboxtxs enable``` (Debian-based) + or + ```chkconfig --add vboxtxs``` (OL/RHEL) + + ... or ... + +7 a) For systemd distros: Link/copy up the vboxtxs.system to [/usr]/lib/systemd/, e.g. + cp /opt/validationkit/linux/vboxtxs.service /etc/systemd/system + b) Enable the vboxtxs service via: + systemctl enable vboxtxs + + For all distros again: + +8a. Check the CD-ROM location (--cdrom <path>) in vboxtxs and fix it so it's correct, make sure + to update in svn as well. +8b. Optional: If no suitable CD-ROM location is available on the guest yet, do a: + mkdir -p /media/cdrom; vi /etc/fstab + and enter this in /etc/fstab: + /dev/sr0<tab>/media/cdrom<tab>udf,iso9660<tab>user,noauto,exec,utf8<tab>0<tab>0 +8c. Optional: If SELinux denies execution of TXS, make sure to allow this, based on + how the distribution handles SELinux exceptions. Often there even is a GUI for that + (e.g. Oracle Linux 8+). +9. Make sure that the package sources are still valid and up to date (apt / yum / ++) +10. reboot / done. +11. Do test. + + +OS/2 Installation +-------------------- + +1. Start an "OS/2 Window" ("OS/2 System" -> "Command Prompts") +2. md C:\Apps +3. cd C:\Apps +4. Mount the validationkit iso. +5. copy D:\os2\x86\* C:\Apps +5. copy D:\os2\x86\libc*.dll C:\OS2\DLL\ +6. Open C:\startup.cmd in an editor (tedit.exe for instance or e.exe). +7. Add the line "start /C C:\Apps\TestExecService.exe --foreground" at the top of the file. +8. reboot / done +9. Do test. + + +Solaris Installation +-------------------- + +1. Start the guest and open a root console. +2. mkdir -p /opt/VBoxTest +3. cd /opt/VBoxTest +4. scp/download VBoxValidationKit*.zip there. +5. unzip VBoxValidationKit*.zip +6. chmod -R u+w,a+x /opt/VBoxTest/ +7. Import the right service setup depending on the Solaris version: + <= 10u9) /usr/sbin/svccfg import /opt/VBoxTest/validationkit/solaris/vboxtxs-sol10.xml + >= 11.0) /usr/sbin/svccfg import /opt/VBoxTest/validationkit/solaris/vboxtxs.xml +8. /usr/sbin/svcadm enable svc:/system/virtualbox/vboxtxs +9. reboot / done. + +To remove the service before repeating steps 7 & 8: +1. /usr/sbin/svcadm disable -s svc:/system/virtualbox/vboxtxs:default +2. /usr/sbin/svccfg delete svc:/system/virtualbox/vboxtxs:default + +Note. To configure dhcp for more a new interface the files +/etc/hostname.<if#X> and /etc/dhcp.<ifnm#> have to exist. If you want the VM +to work with any network card you throw at it, create /etc/*.pcn[01] and +/etc/*.e1000g[012] as Solaris will remember it has seen the other variants +before and use a different instance number (or something to that effect). + + +Windows Installation +-------------------- + +1. Log on as Administrator. +2. Make sure you have set a secure password, which you'll need in step 9. +3. Start CMD.EXE or equivalent. +4. md C:\Apps +5. cd C:\Apps +6. Mount the validationkit iso. +7. copy D:\win\* C:\Apps +8. copy D:\win\<x86 or amd64>\* C:\Apps +9. Put the password from step 2 into the right service setup (see connection + type above) and import it into the registry: + nat) start C:\Apps\vboxtxs-nat.reg + other) start C:\Apps\vboxtxs.reg +10. Make sure that the CD-ROM location is assigned to D: (via "Disk Management"). +11. reboot / done +12. Do test. + +NT 3.1 and 3.x tricks: +- Make sure the file system is NTFS. Observed issues converting 2GB partitions, + more success with smaller. +- For NT3.1 PCNET drivers can be found on the net. No DHCP, so NAT only with + IP 10.0.2.15, 10.0.2.2 as gateway, and 10.0.2.3 as DNS with --natdnsproxy1 on. +- On NT3.1 you need to add SystemDrive=C: to the environment. +- Need to perform registry edits manually. +- Use startup folder instead of non-exising Windows/Run key. + + +Testing the setup +----------------- + +1. Make sure the validationkit.iso is inserted. +2. Boot / reboot the guest. +3. Depending on the TXS transport options: + nat) python testdriver/tst-txsclient.py --reversed-setup + other) python testdriver/tst-txsclient.py --hostname <guest-ip> diff --git a/src/VBox/ValidationKit/utils/TestExecServ/vboxtxs-runvm-readme.txt b/src/VBox/ValidationKit/utils/TestExecServ/vboxtxs-runvm-readme.txt new file mode 100644 index 00000000..f8745391 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/vboxtxs-runvm-readme.txt @@ -0,0 +1,54 @@ +$Id: vboxtxs-runvm-readme.txt $ + + +VirtualBox Test eXecution Service +================================= + +This readme briefly describes how to install the Test eXecution Service (TXS) +for nested hardware-virtualization smoke testing on the various systems. + +The basic idea is to execute one smoke test within the VM and then launch +the regular TXS service in the VM to report success or failure to the host. + +Linux Installation +------------------ + +1. scp/download latest release build of VirtualBox and install it in the VM. +2. scp/download the required smoke test VDI from remote test-resource to + /home/vbox/testrsrc/3.0/tcp/win2k3ent-acpi.vdi +3. cd /root +3. scp/download VBoxValidationKit*.zip there. +5. unzip VBoxValidationKit*.zip +6. chmod -R u+w,a+x /opt/validationkit/ +7a. Gnome: Copy /opt/validationkit/linux/vboxtxs-runvm.desktop to /etc/xdg/autostart +7b. KDE/Others: TODO: Document other desktop managers +8. Add the vbox user to sudo group using: + sudo usermod -a -G sudo vbox +9. Ensure no password is required for vbox when using sudo: + sudo echo "vbox ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/username +10. Check the cdrom location and /dev/kmsg equivalent of your linux distro + in /opt/validationkit/linux/vboxtxs-runvm and fix it so it's correct +11. Reboot / done. + +TODO: Document other OSes as we add them. + +Note: vboxtxs-runvm uses a GUI session to launch the nested-VM for better +visibility when troubleshooting the nested smoke test. + +If this causes problems try troubleshooting the XAUTHORITY and DISPLAY +environment variables in vboxtxs-runvm.service. It might differ depending +on the display manager of the particular linux distro. + + + +Testing the setup +----------------- + +1. Make sure the validationkit.iso is inserted. +2. Boot / reboot the guest. +3. To test the connection - Depending on the TXS transport options: + nat) python testdriver/tst-txsclient.py --reversed-setup + other) python testdriver/tst-txsclient.py --hostname <guest-ip> +4. To test the smoke test: + python tests/smoketests/tdSmokeTest1.py -v -v -d --vbox-session-type gui --test-vms <guest-name> + diff --git a/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs-nat.cmd b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs-nat.cmd new file mode 100644 index 00000000..5d0ffa0c --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs-nat.cmd @@ -0,0 +1,41 @@ +@REM REM @file
+@REM VirtualBox Test Execution Service Init Script for NATted VMs.
+@REM
+
+@REM
+REM
+REM Copyright (C) 2006-2023 Oracle and/or its affiliates.
+REM
+REM This file is part of VirtualBox base platform packages, as
+REM available from https://www.virtualbox.org.
+REM
+REM This program is free software; you can redistribute it and/or
+REM modify it under the terms of the GNU General Public License
+REM as published by the Free Software Foundation, in version 3 of the
+REM License.
+REM
+REM This program is distributed in the hope that it will be useful, but
+REM WITHOUT ANY WARRANTY; without even the implied warranty of
+REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+REM General Public License for more details.
+REM
+REM You should have received a copy of the GNU General Public License
+REM along with this program; if not, see <https://www.gnu.org/licenses>.
+REM
+REM The contents of this file may alternatively be used under the terms
+REM of the Common Development and Distribution License Version 1.0
+REM (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+REM in the VirtualBox distribution, in which case the provisions of the
+REM CDDL are applicable instead of those of the GPL.
+REM
+REM You may elect to license modified versions of this file under the
+REM terms and conditions of either the GPL or the CDDL or both.
+REM
+REM SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+REM
+
+%SystemDrive%\Apps\TestExecService.exe --foreground --display-output ^
+--cdrom D:\ --scratch C:\Temp\vboxtest --auto-upgrade ^
+--tcp-connect 10.0.2.2
+pause
+
diff --git a/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs-nat.reg b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs-nat.reg new file mode 100644 index 00000000..20ab995b --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs-nat.reg @@ -0,0 +1,13 @@ +REGEDIT4
+
+[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]
+"PowerdownAfterShutdown"="1"
+"AutoAdminLogon"="1"
+"ForceAutoLogon"="1"
+"DefaultUserName"="Administrator"
+; Placeholder password for test VM, see TestExecServ/vboxtxs-readme.txt
+"DefaultPassword"="Please_put_your_secure_password_here_see_TestExecServ/vboxtxs-readme.txt"
+
+[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run]
+"NTConfiguration"="c:\\Apps\\vboxtxs-nat.cmd"
+
diff --git a/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.cmd b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.cmd new file mode 100644 index 00000000..6b41153e --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.cmd @@ -0,0 +1,40 @@ +@REM REM @file
+@REM VirtualBox Test Execution Service Init Script.
+@REM
+
+@REM
+REM
+REM Copyright (C) 2006-2023 Oracle and/or its affiliates.
+REM
+REM This file is part of VirtualBox base platform packages, as
+REM available from https://www.virtualbox.org.
+REM
+REM This program is free software; you can redistribute it and/or
+REM modify it under the terms of the GNU General Public License
+REM as published by the Free Software Foundation, in version 3 of the
+REM License.
+REM
+REM This program is distributed in the hope that it will be useful, but
+REM WITHOUT ANY WARRANTY; without even the implied warranty of
+REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+REM General Public License for more details.
+REM
+REM You should have received a copy of the GNU General Public License
+REM along with this program; if not, see <https://www.gnu.org/licenses>.
+REM
+REM The contents of this file may alternatively be used under the terms
+REM of the Common Development and Distribution License Version 1.0
+REM (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+REM in the VirtualBox distribution, in which case the provisions of the
+REM CDDL are applicable instead of those of the GPL.
+REM
+REM You may elect to license modified versions of this file under the
+REM terms and conditions of either the GPL or the CDDL or both.
+REM
+REM SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+REM
+
+%SystemDrive%\Apps\TestExecService.exe --foreground --display-output ^
+--cdrom D:\ --scratch C:\Temp\vboxtest --auto-upgrade
+pause
+
diff --git a/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.reg b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.reg new file mode 100644 index 00000000..037ac425 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.reg @@ -0,0 +1,13 @@ +REGEDIT4
+
+[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]
+"PowerdownAfterShutdown"="1"
+"AutoAdminLogon"="1"
+"ForceAutoLogon"="1"
+"DefaultUserName"="Administrator"
+; Sample password for test VM, see TestExecServ/vboxtxs-readme.txt
+"DefaultPassword"="Please_put_your_secure_password_here_see_TestExecServ/vboxtxs-readme.txt"
+
+[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run]
+"NTConfiguration"="c:\\Apps\\vboxtxs.cmd"
+
diff --git a/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.xml b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.xml new file mode 100644 index 00000000..4dde1f83 --- /dev/null +++ b/src/VBox/ValidationKit/utils/TestExecServ/win/vboxtxs.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-16"?> +<!-- + Copyright (C) 2019-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 <https://www.gnu.org/licenses>. + + The contents of this file may alternatively be used under the terms + of the Common Development and Distribution License Version 1.0 + (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + in the VirtualBox distribution, in which case the provisions of the + CDDL are applicable instead of those of the GPL. + + You may elect to license modified versions of this file under the + terms and conditions of either the GPL or the CDDL or both. + + SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +--> +<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> + <RegistrationInfo> + <Date>2019-05-27T18:41:34.3048528</Date> + <Author>Administrator</Author> + </RegistrationInfo> + <Triggers> + <LogonTrigger> + <Enabled>true</Enabled> + <UserId>Administrator</UserId> + </LogonTrigger> + </Triggers> + <Principals> + <Principal id="Author"> + <UserId>Administrator</UserId> + <LogonType>InteractiveToken</LogonType> + <RunLevel>HighestAvailable</RunLevel> + </Principal> + </Principals> + <Settings> + <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> + <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> + <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> + <AllowHardTerminate>true</AllowHardTerminate> + <StartWhenAvailable>false</StartWhenAvailable> + <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> + <IdleSettings> + <StopOnIdleEnd>false</StopOnIdleEnd> + <RestartOnIdle>false</RestartOnIdle> + </IdleSettings> + <AllowStartOnDemand>true</AllowStartOnDemand> + <Enabled>true</Enabled> + <Hidden>false</Hidden> + <RunOnlyIfIdle>false</RunOnlyIfIdle> + <WakeToRun>false</WakeToRun> + <ExecutionTimeLimit>PT0S</ExecutionTimeLimit> + <Priority>7</Priority> + </Settings> + <Actions Context="Author"> + <Exec> + <Command>C:\Apps\vboxtxs.cmd</Command> + </Exec> + </Actions> +</Task> + diff --git a/src/VBox/ValidationKit/utils/audio/Makefile.kmk b/src/VBox/ValidationKit/utils/audio/Makefile.kmk new file mode 100644 index 00000000..f249189f --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/Makefile.kmk @@ -0,0 +1,220 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Audio Utilities. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Make sure the ValKit config file is included when the additions build +# is including just this makefile. +# +ifndef VBOX_VALIDATIONKIT_CONFIG_KMK_INCLUDED + include $(PATH_ROOT)/src/VBox/ValidationKit/Config.kmk +endif + + +# +# Globals. +# +VBOX_PATH_SRC_DEVICES = $(PATH_ROOT)/src/VBox/Devices +VKAT_PATH_AUDIO = $(VBOX_PATH_SRC_DEVICES)/Audio + + +# +# Append what we build here to PROGRAMS (at the top because it's a bit messy). +# +ifn1of ($(KBUILD_TARGET), os2 freebsd netbsd openbsd) + if defined(VBOX_ONLY_VALIDATIONKIT) || !defined(VBOX_ONLY_BUILD) + PROGRAMS += vkat + if defined(VBOX_WITH_HOST_SHIPPING_AUDIO_TEST) && !defined(VBOX_ONLY_BUILD) + PROGRAMS += vkathost + endif + endif + if defined(VBOX_WITH_ADDITIONS_SHIPPING_AUDIO_TEST) \ + && defined(VBOX_WITH_ADDITIONS) \ + && !defined(VBOX_WITH_ADDITIONS_FROM_BUILD_SERVER) \ + && (defined(VBOX_ONLY_ADDITIONS) || !defined(VBOX_ONLY_BUILD)) + PROGRAMS += vkatadd + endif +endif + + +# +# Utility to play sine wave to Default Audio Device. +# +if 0 # Disabled for now; does not work without WinMM.dll import validator files. + PROGRAMS.win += ntPlayToneWaveX + ntPlayToneWaveX_TEMPLATE = VBoxValidationKitR3 + ntPlayToneWaveX_SOURCES = ntPlayToneWaveX.cpp + ntPlayToneWaveX_LIBS += \ + WinMM.Lib +endif + + +# +# The Validation Kit Audio Test (VKAT) utility. +# +vkat_TEMPLATE = VBoxValidationKitR3 +vkat_VBOX_IMPORT_CHECKER.win.x86 = nt4 +vkat_DEFS = VBOX_AUDIO_VKAT IN_VMM_R3 IN_VMM_STATIC +vkat_INCS = \ + $(PATH_ROOT)/src/VBox/Devices/build \ + $(PATH_ROOT)/src/VBox/Devices \ + $(PATH_ROOT)/src/VBox/Devices/Audio +vkat_SOURCES = \ + vkat.cpp \ + vkatCommon.cpp \ + vkatCmdGeneric.cpp \ + vkatDriverStack.cpp \ + $(VKAT_PATH_AUDIO)/AudioTest.cpp \ + $(VKAT_PATH_AUDIO)/DrvAudio.cpp \ + $(VKAT_PATH_AUDIO)/DrvHostAudioNull.cpp \ + $(VKAT_PATH_AUDIO)/AudioMixer.cpp \ + $(VKAT_PATH_AUDIO)/AudioMixBuffer.cpp \ + $(VKAT_PATH_AUDIO)/AudioHlp.cpp + +# Debug stuff. +ifdef VBOX_WITH_AUDIO_DEBUG + vkat_DEFS += VBOX_WITH_AUDIO_DEBUG + vkat_SOURCES += \ + $(VKAT_PATH_AUDIO)/DrvHostAudioDebug.cpp +endif + +# Self-test stuff. +vkat_DEFS += VBOX_WITH_AUDIO_VALIDATIONKIT +vkat_SOURCES += \ + vkatCmdSelfTest.cpp \ + $(VKAT_PATH_AUDIO)/DrvHostAudioValidationKit.cpp \ + $(VKAT_PATH_AUDIO)/AudioTestService.cpp \ + $(VKAT_PATH_AUDIO)/AudioTestServiceClient.cpp \ + $(VKAT_PATH_AUDIO)/AudioTestServiceProtocol.cpp \ + $(VKAT_PATH_AUDIO)/AudioTestServiceTcp.cpp + +ifdef VBOX_WITH_AUDIO_PULSE + vkat_DEFS += VBOX_WITH_AUDIO_PULSE + vkat_SOURCES += \ + $(VKAT_PATH_AUDIO)/DrvHostAudioPulseAudioStubs.cpp \ + $(VKAT_PATH_AUDIO)/DrvHostAudioPulseAudio.cpp +endif + +ifdef VBOX_WITH_AUDIO_ALSA + vkat_DEFS += VBOX_WITH_AUDIO_ALSA + vkat_SOURCES += \ + $(VKAT_PATH_AUDIO)/DrvHostAudioAlsa.cpp \ + $(VKAT_PATH_AUDIO)/DrvHostAudioAlsaStubs.cpp +endif + +ifdef VBOX_WITH_AUDIO_OSS + vkat_DEFS += VBOX_WITH_AUDIO_OSS + vkat_SOURCES += \ + $(VKAT_PATH_AUDIO)/DrvHostAudioOss.cpp +endif + +vkat_SOURCES.win += \ + $(VKAT_PATH_AUDIO)/DrvHostAudioDSound.cpp \ + $(VKAT_PATH_AUDIO)/DrvHostAudioWasApi.cpp +ifdef VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT + vkat_DEFS.win += VBOX_WITH_AUDIO_MMNOTIFICATION_CLIENT + vkat_SOURCES.win += \ + $(VKAT_PATH_AUDIO)/DrvHostAudioDSoundMMNotifClient.cpp +endif + +vkat_SOURCES.darwin = \ + $(VKAT_PATH_AUDIO)/DrvHostAudioCoreAudio.cpp \ + $(VKAT_PATH_AUDIO)/DrvHostAudioCoreAudioAuth.mm +vkat_LDFLAGS.darwin = \ + -framework CoreAudio \ + -framework AudioUnit \ + -framework AudioToolbox \ + -framework Foundation +ifn1of ($(VBOX_DEF_MACOSX_VERSION_MIN), 10.4 10.5 10.6) + vkat_LDFLAGS.darwin += \ + -framework AVFoundation +endif + + +# +# The additions variant of the audio test utility. +# +# We name it VBoxAudioTest though, to not clutter up Guest Additions +# installations with cryptic binaries not sporting 'VBox' as prefix. +# +vkatadd_TEMPLATE = VBoxGuestR3Exe +vkatadd_EXTENDS = vkat +vkatadd_EXTENDS_BY = appending +vkatadd_NAME = VBoxAudioTest +vkatadd_SDKS = VBoxZlibStatic +vkatadd_LDFLAGS.darwin = -framework IOKit +vkatadd_LIBS.solaris = m + + +# +# Build the valkit vkat to bin as VBoxAudioTest, so that it can be shipped with +# the host installer too. +# +# Note: We also need to have this as a signed binary, so don't just copy the +# vkat binary to bin/ directory but built this as an own binary. +# +vkathost_TEMPLATE := VBoxR3Exe +vkathost_EXTENDS := vkat +vkathost_INST := $(INST_BIN) +vkathost_NAME := VBoxAudioTest +vkathost_SOURCES = \ + $(vkat_SOURCES) \ + $(VBOX_PATH_SRC_DEVICES)/build/VBoxDD.d +vkathost_LIBS = \ + $(LIB_RUNTIME) + + +if defined(VBOX_WITH_TESTCASES) && !defined(VBOX_ONLY_ADDITIONS) && !defined(VBOX_ONLY_SDK) \ + && 0 ## @todo r=bird: Disabled because nobody really wants or needs to run this during build other than Andy. + ## And more importantly, it breaks the build (os2, bsd*). + + PROGRAMS += tstVkatHostSelftest + tstVkatHostSelftest_EXTENDS = vkat + tstVkatHostSelftest_EXTENDS_BY = appending + tstVkatHostSelftest_INST = $(INST_TESTCASE) + tstVkatHostSelftest_DEFS.debug = VBOX_WITH_EF_WRAPS + + TESTING += $(tstVkatHostSelftest_0_OUTDIR)/tstVkatHostSelftest.run + $$(tstVkatHostSelftest_0_OUTDIR)/tstVkatHostSelftest.run: $$(tstVkatHostSelftest_1_TARGET) + export VKAT_RELEASE_LOG=-all; $(tstVkatHostSelftest_1_TARGET) selftest + $(QUIET)$(APPEND) -t "$@" "done" + +endif + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp b/src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp new file mode 100644 index 00000000..e07165dc --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/ntPlayToneWaveX.cpp @@ -0,0 +1,226 @@ +/* $Id: ntPlayToneWaveX.cpp $ */ +/** @file + * ???? + */ + +/* + * Copyright (C) 2012-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/win/windows.h> + +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/message.h> +#include <iprt/stream.h> +#include <iprt/errcore.h> + +#define _USE_MATH_DEFINES +#include <math.h> + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +uint32_t g_cSamplesPerSec = 44100; +uint32_t g_cSamplesPerPeriod = 100; // 441.0Hz for 44.1kHz +uint32_t g_cSamplesInBuffer = 4096; +double g_rdSecDuration = 5.0; + +uint32_t g_cbSample; // assuming 16-bit stereo (for now) + +HWAVEOUT g_hWaveOut; +HANDLE g_hWavEvent; + + +int main(int argc, char **argv) +{ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + static const RTGETOPTDEF s_aOptions[] = + { + { "--samples-per-sec", 's', RTGETOPT_REQ_UINT32 }, + { "--period-in-samples", 'p', RTGETOPT_REQ_UINT32 }, + { "--bufsize-in-samples", 'b', RTGETOPT_REQ_UINT32 }, + { "--total-duration-in-secs", 'd', RTGETOPT_REQ_UINT32 } + }; + + RTGETOPTSTATE State; + RTGetOptInit(&State, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0); + RTGETOPTUNION ValueUnion; + int chOpt; + while ((chOpt = RTGetOpt(&State, &ValueUnion)) != 0) + { + switch (chOpt) + { + case 's': g_cSamplesPerSec = ValueUnion.u32; break; + case 'p': g_cSamplesPerPeriod = ValueUnion.u32; break; + case 'b': g_cSamplesInBuffer = ValueUnion.u32; break; + case 'd': g_rdSecDuration = ValueUnion.u32; break; + case 'h': + RTPrintf("usage: ntPlayToneWaveX.exe\n" + "[-s|--samples-per-sec]\n" + "[-p|--period-in-samples]\n" + "[-b|--bufsize-in-samples]\n" + "[-d|--total-duration-in-secs]\n" + "\n" + "Plays sine tone using ancient waveX API\n"); + return 0; + + default: + return RTGetOptPrintError(chOpt, &ValueUnion); + } + } + + + WAVEFORMATEX waveFormatEx = { 0 }; + MMRESULT mmresult; + + waveFormatEx.wFormatTag = WAVE_FORMAT_PCM; + waveFormatEx.nChannels = 2; + waveFormatEx.nSamplesPerSec = g_cSamplesPerSec; + waveFormatEx.wBitsPerSample = 16; + waveFormatEx.nBlockAlign = g_cbSample = waveFormatEx.nChannels * waveFormatEx.wBitsPerSample / 8; + waveFormatEx.nAvgBytesPerSec = waveFormatEx.nBlockAlign * waveFormatEx.nSamplesPerSec; + waveFormatEx.cbSize = 0; + + g_hWavEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + mmresult = waveOutOpen(&g_hWaveOut, WAVE_MAPPER, &waveFormatEx, (DWORD_PTR)g_hWavEvent, NULL, CALLBACK_EVENT); + + if (mmresult != MMSYSERR_NOERROR) + { + RTMsgError("waveOutOpen failed with 0x%X\n", mmresult); + return -1; + } + + + uint32_t ui32SamplesToPlayTotal = (uint32_t)(g_rdSecDuration * g_cSamplesPerSec); + uint32_t ui32SamplesToPlay = ui32SamplesToPlayTotal; + uint32_t ui32SamplesPlayed = 0; + uint32_t ui32SamplesForWavBuf; + + WAVEHDR waveHdr1 = {0}, waveHdr2 = {0}, *pWaveHdr, *pWaveHdrPlaying, *pWaveHdrWaiting; + uint32_t i, k; + DWORD res; + + int16_t *i16Samples1 = (int16_t *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, g_cSamplesInBuffer * g_cbSample); + int16_t *i16Samples2 = (int16_t *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, g_cSamplesInBuffer * g_cbSample); + + k = 0; // This is discrete time really!!! + + for (i = 0; i < g_cSamplesInBuffer; i++, k++) + { + i16Samples1[2 * i] = (uint16_t)(10000.0 * sin(2.0 * M_PI * k / g_cSamplesPerPeriod)); + i16Samples1[2 * i + 1] = i16Samples1[2 * i]; + } + + ui32SamplesForWavBuf = min(ui32SamplesToPlay, g_cSamplesInBuffer); + + waveHdr1.lpData = (LPSTR)i16Samples1; + waveHdr1.dwBufferLength = ui32SamplesForWavBuf * g_cbSample; + waveHdr1.dwFlags = 0; + waveHdr1.dwLoops = 0; + + ui32SamplesToPlay -= ui32SamplesForWavBuf; + ui32SamplesPlayed += ui32SamplesForWavBuf; + + pWaveHdrPlaying = &waveHdr1; + + mmresult = waveOutPrepareHeader(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR)); + mmresult = waveOutWrite(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR)); + //RTMsgInfo("waveOutWrite completes with %d\n", mmresult); + + res = WaitForSingleObject(g_hWavEvent, INFINITE); + //RTMsgInfo("WaitForSingleObject completes with %d\n\n", res); + + waveHdr2.lpData = (LPSTR)i16Samples2; + waveHdr2.dwBufferLength = 0; + waveHdr2.dwFlags = 0; + waveHdr2.dwLoops = 0; + + pWaveHdrWaiting = &waveHdr2; + + while (ui32SamplesToPlay > 0) + { + int16_t *i16Samples = (int16_t *)pWaveHdrWaiting->lpData; + + for (i = 0; i < g_cSamplesInBuffer; i++, k++) + { + i16Samples[2 * i] = (uint16_t)(10000.0 * sin(2.0 * M_PI * k / g_cSamplesPerPeriod)); + i16Samples[2 * i + 1] = i16Samples[2 * i]; + } + + ui32SamplesForWavBuf = min(ui32SamplesToPlay, g_cSamplesInBuffer); + + pWaveHdrWaiting->dwBufferLength = ui32SamplesForWavBuf * g_cbSample; + pWaveHdrWaiting->dwFlags = 0; + pWaveHdrWaiting->dwLoops = 0; + + + ui32SamplesToPlay -= ui32SamplesForWavBuf; + ui32SamplesPlayed += ui32SamplesForWavBuf; + + mmresult = waveOutPrepareHeader(g_hWaveOut, pWaveHdrWaiting, sizeof(WAVEHDR)); + mmresult = waveOutWrite(g_hWaveOut, pWaveHdrWaiting, sizeof(WAVEHDR)); + //RTMsgInfo("waveOutWrite completes with %d\n", mmresult); + + res = WaitForSingleObject(g_hWavEvent, INFINITE); + //RTMsgInfo("WaitForSingleObject completes with %d\n\n", res); + + mmresult = waveOutUnprepareHeader(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR)); + //RTMsgInfo("waveOutUnprepareHeader completes with %d\n", mmresult); + + pWaveHdr = pWaveHdrWaiting; + pWaveHdrWaiting = pWaveHdrPlaying; + pWaveHdrPlaying = pWaveHdr; + } + + while (mmresult = waveOutUnprepareHeader(g_hWaveOut, pWaveHdrPlaying, sizeof(WAVEHDR))) + { + //Expecting WAVERR_STILLPLAYING + //RTMsgInfo("waveOutUnprepareHeader failed with 0x%X\n", mmresult); + Sleep(100); + } + + if (mmresult == MMSYSERR_NOERROR) + { + waveOutClose(g_hWaveOut); + } + + HeapFree(GetProcessHeap(), 0, i16Samples1); + HeapFree(GetProcessHeap(), 0, i16Samples2); +} + diff --git a/src/VBox/ValidationKit/utils/audio/readme.txt b/src/VBox/ValidationKit/utils/audio/readme.txt new file mode 100644 index 00000000..ba894b5a --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/readme.txt @@ -0,0 +1,2 @@ + +See docs/VBoxAudioValidationKitReadMe.txt or docs/VBoxAudioValidationKitReadMe.html. diff --git a/src/VBox/ValidationKit/utils/audio/vkat.cpp b/src/VBox/ValidationKit/utils/audio/vkat.cpp new file mode 100644 index 00000000..0ff17e4c --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/vkat.cpp @@ -0,0 +1,1649 @@ +/* $Id: vkat.cpp $ */ +/** @file + * Validation Kit Audio Test (VKAT) utility for testing and validating the audio stack. + */ + +/* + * Copyright (C) 2021-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_TEST + +#include <iprt/buildconfig.h> +#include <iprt/ctype.h> +#include <iprt/dir.h> +#include <iprt/errcore.h> +#include <iprt/file.h> +#include <iprt/initterm.h> +#include <iprt/getopt.h> +#include <iprt/message.h> +#include <iprt/path.h> +#include <iprt/process.h> +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#include <package-generated.h> +#include "product-generated.h" + +#include <VBox/version.h> +#include <VBox/log.h> + +#ifdef RT_OS_WINDOWS +# include <iprt/win/windows.h> /* for CoInitializeEx and SetConsoleCtrlHandler */ +#else +# include <signal.h> +#endif + +#include "vkatInternal.h" + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static int audioVerifyOne(const char *pszPathSetA, const char *pszPathSetB, PAUDIOTESTVERIFYOPTS pOpts); + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** + * Backends description table. + * + * @note The first backend in the array is the default one for the platform. + */ +AUDIOTESTBACKENDDESC const g_aBackends[] = +{ +#ifdef VBOX_WITH_AUDIO_PULSE + { &g_DrvHostPulseAudio, "pulseaudio" }, + { &g_DrvHostPulseAudio, "pulse" }, + { &g_DrvHostPulseAudio, "pa" }, +#endif +/* + * Note: ALSA has to come second so that PulseAudio above always is the default on Linux-y OSes + * -- most distros are using an ALSA plugin for PulseAudio nowadays. + * However, some of these configurations do not seem to work by default (can't create audio streams). + * + * If PulseAudio is not available, the (optional) probing ("--probe-backends") will choose the "pure" ALSA stack instead then. + */ +#if defined(VBOX_WITH_AUDIO_ALSA) && defined(RT_OS_LINUX) + { &g_DrvHostALSAAudio, "alsa" }, +#endif +#ifdef VBOX_WITH_AUDIO_OSS + { &g_DrvHostOSSAudio, "oss" }, +#endif +#if defined(RT_OS_DARWIN) + { &g_DrvHostCoreAudio, "coreaudio" }, + { &g_DrvHostCoreAudio, "core" }, + { &g_DrvHostCoreAudio, "ca" }, +#endif +#if defined(RT_OS_WINDOWS) + { &g_DrvHostAudioWas, "wasapi" }, + { &g_DrvHostAudioWas, "was" }, + { &g_DrvHostDSound, "directsound" }, + { &g_DrvHostDSound, "dsound" }, + { &g_DrvHostDSound, "ds" }, +#endif +#ifdef VBOX_WITH_AUDIO_DEBUG + { &g_DrvHostDebugAudio, "debug" }, +#endif + { &g_DrvHostValidationKitAudio, "valkit" } +}; +AssertCompile(sizeof(g_aBackends) > 0 /* port me */); +/** Number of backends defined. */ +unsigned g_cBackends = RT_ELEMENTS(g_aBackends); + +/** + * Long option values for the 'test' command. + */ +enum +{ + VKAT_TEST_OPT_COUNT = 900, + VKAT_TEST_OPT_DEV, + VKAT_TEST_OPT_GUEST_ATS_ADDR, + VKAT_TEST_OPT_GUEST_ATS_PORT, + VKAT_TEST_OPT_HOST_ATS_ADDR, + VKAT_TEST_OPT_HOST_ATS_PORT, + VKAT_TEST_OPT_MODE, + VKAT_TEST_OPT_NO_AUDIO_OK, + VKAT_TEST_OPT_NO_VERIFY, + VKAT_TEST_OPT_OUTDIR, + VKAT_TEST_OPT_PAUSE, + VKAT_TEST_OPT_PCM_HZ, + VKAT_TEST_OPT_PCM_BIT, + VKAT_TEST_OPT_PCM_CHAN, + VKAT_TEST_OPT_PCM_SIGNED, + VKAT_TEST_OPT_PROBE_BACKENDS, + VKAT_TEST_OPT_TAG, + VKAT_TEST_OPT_TEMPDIR, + VKAT_TEST_OPT_VOL, + VKAT_TEST_OPT_TCP_BIND_ADDRESS, + VKAT_TEST_OPT_TCP_BIND_PORT, + VKAT_TEST_OPT_TCP_CONNECT_ADDRESS, + VKAT_TEST_OPT_TCP_CONNECT_PORT, + VKAT_TEST_OPT_TONE_DURATION_MS, + VKAT_TEST_OPT_TONE_VOL_PERCENT +}; + +/** + * Long option values for the 'verify' command. + */ +enum +{ + VKAT_VERIFY_OPT_MAX_DIFF_COUNT = 900, + VKAT_VERIFY_OPT_MAX_DIFF_PERCENT, + VKAT_VERIFY_OPT_MAX_SIZE_PERCENT, + VKAT_VERIFY_OPT_NORMALIZE +}; + +/** + * Common command line parameters. + */ +static const RTGETOPTDEF g_aCmdCommonOptions[] = +{ + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + { "--daemonize", AUDIO_TEST_OPT_CMN_DAEMONIZE, RTGETOPT_REQ_NOTHING }, + { "--daemonized", AUDIO_TEST_OPT_CMN_DAEMONIZED, RTGETOPT_REQ_NOTHING }, + { "--debug-audio", AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_ENABLE, RTGETOPT_REQ_NOTHING }, + { "--debug-audio-path", AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_PATH, RTGETOPT_REQ_STRING }, +}; + +/** + * Command line parameters for test mode. + */ +static const RTGETOPTDEF g_aCmdTestOptions[] = +{ + { "--backend", 'b', RTGETOPT_REQ_STRING }, + { "--drvaudio", 'd', RTGETOPT_REQ_NOTHING }, + { "--exclude", 'e', RTGETOPT_REQ_UINT32 }, + { "--exclude-all", 'a', RTGETOPT_REQ_NOTHING }, + { "--guest-ats-addr", VKAT_TEST_OPT_GUEST_ATS_ADDR, RTGETOPT_REQ_STRING }, + { "--guest-ats-port", VKAT_TEST_OPT_GUEST_ATS_PORT, RTGETOPT_REQ_UINT32 }, + { "--host-ats-address", VKAT_TEST_OPT_HOST_ATS_ADDR, RTGETOPT_REQ_STRING }, + { "--host-ats-port", VKAT_TEST_OPT_HOST_ATS_PORT, RTGETOPT_REQ_UINT32 }, + { "--include", 'i', RTGETOPT_REQ_UINT32 }, + { "--outdir", VKAT_TEST_OPT_OUTDIR, RTGETOPT_REQ_STRING }, + { "--count", VKAT_TEST_OPT_COUNT, RTGETOPT_REQ_UINT32 }, + { "--device", VKAT_TEST_OPT_DEV, RTGETOPT_REQ_STRING }, + { "--pause", VKAT_TEST_OPT_PAUSE, RTGETOPT_REQ_UINT32 }, + { "--pcm-bit", VKAT_TEST_OPT_PCM_BIT, RTGETOPT_REQ_UINT8 }, + { "--pcm-chan", VKAT_TEST_OPT_PCM_CHAN, RTGETOPT_REQ_UINT8 }, + { "--pcm-hz", VKAT_TEST_OPT_PCM_HZ, RTGETOPT_REQ_UINT16 }, + { "--pcm-signed", VKAT_TEST_OPT_PCM_SIGNED, RTGETOPT_REQ_BOOL }, + { "--probe-backends", VKAT_TEST_OPT_PROBE_BACKENDS, RTGETOPT_REQ_NOTHING }, + { "--mode", VKAT_TEST_OPT_MODE, RTGETOPT_REQ_STRING }, + { "--no-audio-ok", VKAT_TEST_OPT_NO_AUDIO_OK, RTGETOPT_REQ_NOTHING }, + { "--no-verify", VKAT_TEST_OPT_NO_VERIFY, RTGETOPT_REQ_NOTHING }, + { "--tag", VKAT_TEST_OPT_TAG, RTGETOPT_REQ_STRING }, + { "--tempdir", VKAT_TEST_OPT_TEMPDIR, RTGETOPT_REQ_STRING }, + { "--vol", VKAT_TEST_OPT_VOL, RTGETOPT_REQ_UINT8 }, + { "--tcp-bind-addr", VKAT_TEST_OPT_TCP_BIND_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-bind-port", VKAT_TEST_OPT_TCP_BIND_PORT, RTGETOPT_REQ_UINT16 }, + { "--tcp-connect-addr", VKAT_TEST_OPT_TCP_CONNECT_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-connect-port", VKAT_TEST_OPT_TCP_CONNECT_PORT, RTGETOPT_REQ_UINT16 }, + { "--tone-duration", VKAT_TEST_OPT_TONE_DURATION_MS, RTGETOPT_REQ_UINT32 }, + { "--tone-vol", VKAT_TEST_OPT_TONE_VOL_PERCENT, RTGETOPT_REQ_UINT8 } +}; + +/** + * Command line parameters for verification mode. + */ +static const RTGETOPTDEF g_aCmdVerifyOptions[] = +{ + { "--max-diff-count", VKAT_VERIFY_OPT_MAX_DIFF_COUNT, RTGETOPT_REQ_UINT32 }, + { "--max-diff-percent", VKAT_VERIFY_OPT_MAX_DIFF_PERCENT, RTGETOPT_REQ_UINT8 }, + { "--max-size-percent", VKAT_VERIFY_OPT_MAX_SIZE_PERCENT, RTGETOPT_REQ_UINT8 }, + { "--normalize", VKAT_VERIFY_OPT_NORMALIZE, RTGETOPT_REQ_BOOL } +}; + +/** Terminate ASAP if set. Set on Ctrl-C. */ +bool volatile g_fTerminate = false; +/** The release logger. */ +PRTLOGGER g_pRelLogger = NULL; +/** The test handle. */ +RTTEST g_hTest; +/** The current verbosity level. */ +unsigned g_uVerbosity = 0; +/** DrvAudio: Enable debug (or not). */ +bool g_fDrvAudioDebug = false; +/** DrvAudio: The debug output path. */ +const char *g_pszDrvAudioDebug = NULL; + + +/** + * Get default backend. + */ +PCPDMDRVREG AudioTestGetDefaultBackend(void) +{ + return g_aBackends[0].pDrvReg; +} + + +/** + * Helper for handling --backend options. + * + * @returns Pointer to the specified backend, NULL if not found (error + * displayed). + * @param pszBackend The backend option value. + */ +PCPDMDRVREG AudioTestFindBackendOpt(const char *pszBackend) +{ + for (uintptr_t i = 0; i < RT_ELEMENTS(g_aBackends); i++) + if ( strcmp(pszBackend, g_aBackends[i].pszName) == 0 + || strcmp(pszBackend, g_aBackends[i].pDrvReg->szName) == 0) + return g_aBackends[i].pDrvReg; + RTMsgError("Unknown backend: '%s'\n\n", pszBackend); + RTPrintf("Supported backend values are: "); + for (uintptr_t i = 0; i < RT_ELEMENTS(g_aBackends); i++) + { + if (i > 0) + RTPrintf(", "); + RTPrintf(g_aBackends[i].pszName); + } + RTPrintf("\n"); + return NULL; +} + + +/********************************************************************************************************************************* +* Test callbacks * +*********************************************************************************************************************************/ + +/** + * @copydoc FNAUDIOTESTSETUP + */ +static DECLCALLBACK(int) audioTestPlayToneSetup(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx) +{ + RT_NOREF(pTstDesc, ppvCtx); + + int rc = VINF_SUCCESS; + + if (strlen(pTstEnv->szDev)) + { + rc = audioTestDriverStackSetDevice(pTstEnv->pDrvStack, PDMAUDIODIR_OUT, pTstEnv->szDev); + if (RT_FAILURE(rc)) + return rc; + } + + pTstParmsAcq->enmType = AUDIOTESTTYPE_TESTTONE_PLAY; + pTstParmsAcq->enmDir = PDMAUDIODIR_OUT; + + pTstParmsAcq->TestTone = pTstEnv->ToneParms; + + pTstParmsAcq->TestTone.Hdr.idxTest = pTstEnv->idxTest; /* Assign unique test ID. */ + + return rc; +} + +/** + * @copydoc FNAUDIOTESTEXEC + */ +static DECLCALLBACK(int) audioTestPlayToneExec(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms) +{ + RT_NOREF(pvCtx); + + int rc = VINF_SUCCESS; + + PAUDIOTESTTONEPARMS const pToneParms = &pTstParms->TestTone; + + uint32_t const idxTest = pToneParms->Hdr.idxTest; + + RTTIMESPEC NowTimeSpec; + RTTimeExplode(&pToneParms->Hdr.tsCreated, RTTimeNow(&NowTimeSpec)); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing test tone (%RU16Hz, %RU32ms)\n", + idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration); + + /* + * 1. Arm the (host) ValKit ATS with the recording parameters. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Telling ValKit audio driver on host to record new tone ...\n", idxTest); + + rc = AudioTestSvcClientToneRecord(&pTstEnv->u.Host.AtsClValKit, pToneParms); + if (RT_SUCCESS(rc)) + { + /* Give the Validaiton Kit audio driver on the host a bit of time to register / arming the new test. */ + RTThreadSleep(5000); /* Fudge factor. */ + + /* + * 2. Tell VKAT on guest to start playback. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Telling VKAT on guest to play tone ...\n", idxTest); + + rc = AudioTestSvcClientTonePlay(&pTstEnv->u.Host.AtsClGuest, pToneParms); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientTonePlay() failed with %Rrc\n", idxTest, rc); + } + else + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientToneRecord() failed with %Rrc\n", idxTest, rc); + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing tone done\n", idxTest); + + /* Give the audio stack a random amount of time for draining data before the next iteration. */ + if (pTstEnv->cIterations > 1) + RTThreadSleep(RTRandU32Ex(2000, 5000)); /** @todo Implement some dedicated ATS command for this? */ + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: Playing test tone failed with %Rrc\n", idxTest, rc); + + return rc; +} + +/** + * @copydoc FNAUDIOTESTDESTROY + */ +static DECLCALLBACK(int) audioTestPlayToneDestroy(PAUDIOTESTENV pTstEnv, void *pvCtx) +{ + RT_NOREF(pTstEnv, pvCtx); + + return VINF_SUCCESS; +} + +/** + * @copydoc FNAUDIOTESTSETUP + */ +static DECLCALLBACK(int) audioTestRecordToneSetup(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx) +{ + RT_NOREF(pTstDesc, ppvCtx); + + int rc = VINF_SUCCESS; + + if (strlen(pTstEnv->szDev)) + { + rc = audioTestDriverStackSetDevice(pTstEnv->pDrvStack, PDMAUDIODIR_IN, pTstEnv->szDev); + if (RT_FAILURE(rc)) + return rc; + } + + pTstParmsAcq->enmType = AUDIOTESTTYPE_TESTTONE_RECORD; + pTstParmsAcq->enmDir = PDMAUDIODIR_IN; + + pTstParmsAcq->TestTone = pTstEnv->ToneParms; + + pTstParmsAcq->TestTone.Hdr.idxTest = pTstEnv->idxTest; /* Assign unique test ID. */ + + return rc; +} + +/** + * @copydoc FNAUDIOTESTEXEC + */ +static DECLCALLBACK(int) audioTestRecordToneExec(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms) +{ + RT_NOREF(pvCtx); + + int rc = VINF_SUCCESS; + + PAUDIOTESTTONEPARMS const pToneParms = &pTstParms->TestTone; + + uint32_t const idxTest = pToneParms->Hdr.idxTest; + + RTTIMESPEC NowTimeSpec; + RTTimeExplode(&pToneParms->Hdr.tsCreated, RTTimeNow(&NowTimeSpec)); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording test tone (%RU16Hz, %RU32ms)\n", + idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration); + + /* + * 1. Arm the (host) ValKit ATS with the playback parameters. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Telling ValKit audio driver on host to inject recording data ...\n", idxTest); + + rc = AudioTestSvcClientTonePlay(&pTstEnv->u.Host.AtsClValKit, &pTstParms->TestTone); + if (RT_SUCCESS(rc)) + { + /* + * 2. Tell the guest ATS to start recording. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Telling VKAT on guest to record audio ...\n", idxTest); + + rc = AudioTestSvcClientToneRecord(&pTstEnv->u.Host.AtsClGuest, &pTstParms->TestTone); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientToneRecord() failed with %Rrc\n", idxTest, rc); + } + else + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientTonePlay() failed with %Rrc\n", idxTest, rc); + + if (RT_SUCCESS(rc)) + { + /* Wait a bit to let the left over audio bits being processed. */ + if (pTstEnv->cIterations > 1) + RTThreadSleep(RTRandU32Ex(2000, 5000)); /** @todo Implement some dedicated ATS command for this? */ + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: Recording test tone failed with %Rrc\n", idxTest, rc); + + return rc; +} + +/** + * @copydoc FNAUDIOTESTDESTROY + */ +static DECLCALLBACK(int) audioTestRecordToneDestroy(PAUDIOTESTENV pTstEnv, void *pvCtx) +{ + RT_NOREF(pTstEnv, pvCtx); + + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Test execution * +*********************************************************************************************************************************/ + +/** Test definition table. */ +AUDIOTESTDESC g_aTests[] = +{ + /* pszTest fExcluded pfnSetup */ + { "PlayTone", false, audioTestPlayToneSetup, audioTestPlayToneExec, audioTestPlayToneDestroy }, + { "RecordTone", false, audioTestRecordToneSetup, audioTestRecordToneExec, audioTestRecordToneDestroy } +}; +/** Number of tests defined. */ +unsigned g_cTests = RT_ELEMENTS(g_aTests); + +/** + * Runs one specific audio test. + * + * @returns VBox status code. + * @param pTstEnv Test environment to use for running the test. + * @param pTstDesc Test to run. + */ +static int audioTestOne(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc) +{ + int rc = VINF_SUCCESS; + + AUDIOTESTPARMS TstParms; + audioTestParmsInit(&TstParms); + + RTTestSub(g_hTest, pTstDesc->pszName); + + if (pTstDesc->fExcluded) + { + RTTestSkipped(g_hTest, "Test #%RU32 is excluded from list, skipping", pTstEnv->idxTest); + return VINF_SUCCESS; + } + + pTstEnv->cIterations = pTstEnv->cIterations == 0 ? RTRandU32Ex(1, 10) : pTstEnv->cIterations; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32 (%RU32 iterations total)\n", pTstEnv->idxTest, pTstEnv->cIterations); + + void *pvCtx = NULL; /* Test-specific opaque context. Optional and can be NULL. */ + + AssertPtr(pTstDesc->pfnExec); + for (uint32_t i = 0; i < pTstEnv->cIterations; i++) + { + int rc2; + + if (pTstDesc->pfnSetup) + { + rc2 = pTstDesc->pfnSetup(pTstEnv, pTstDesc, &TstParms, &pvCtx); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Test #%RU32 setup failed with %Rrc\n", pTstEnv->idxTest, rc2); + } + else + rc2 = VINF_SUCCESS; + + if (RT_SUCCESS(rc2)) + { + AssertPtrBreakStmt(pTstDesc->pfnExec, VERR_INVALID_POINTER); + rc2 = pTstDesc->pfnExec(pTstEnv, pvCtx, &TstParms); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Test #%RU32 execution failed with %Rrc\n", pTstEnv->idxTest, rc2); + } + + if (pTstDesc->pfnDestroy) + { + rc2 = pTstDesc->pfnDestroy(pTstEnv, pvCtx); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Test #%RU32 destruction failed with %Rrc\n", pTstEnv->idxTest, rc2); + } + + if (RT_SUCCESS(rc)) + rc = rc2; + + /* Keep going. */ + pTstEnv->idxTest++; + } + + RTTestSubDone(g_hTest); + + audioTestParmsDestroy(&TstParms); + + return rc; +} + +/** + * Runs all specified tests in a row. + * + * @returns VBox status code. + * @param pTstEnv Test environment to use for running all tests. + */ +int audioTestWorker(PAUDIOTESTENV pTstEnv) +{ + int rc = VINF_SUCCESS; + + if (pTstEnv->enmMode == AUDIOTESTMODE_GUEST) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Guest ATS running\n"); + + while (!g_fTerminate) + RTThreadSleep(100); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Shutting down guest ATS ...\n"); + + int rc2 = AudioTestSvcStop(pTstEnv->pSrv); + if (RT_SUCCESS(rc)) + rc = rc2; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Guest ATS shutdown complete\n"); + } + else if (pTstEnv->enmMode == AUDIOTESTMODE_HOST) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using tag '%s'\n", pTstEnv->szTag); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Telling ValKit audio driver on host to begin a new test set ...\n"); + rc = AudioTestSvcClientTestSetBegin(&pTstEnv->u.Host.AtsClValKit, pTstEnv->szTag); + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Telling VKAT on guest to begin a new test set ...\n"); + rc = AudioTestSvcClientTestSetBegin(&pTstEnv->u.Host.AtsClGuest, pTstEnv->szTag); + if (RT_FAILURE(rc)) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Beginning test set on guest failed with %Rrc\n", rc); + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Beginning test set on host (Validation Kit audio driver) failed with %Rrc\n", rc); + + if (RT_SUCCESS(rc)) + { + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + { + int rc2 = audioTestOne(pTstEnv, &g_aTests[i]); + if (RT_SUCCESS(rc)) + rc = rc2; + + if (g_fTerminate) + break; + } + + if (RT_SUCCESS(rc)) + { + /** @todo Fudge! */ + RTMSINTERVAL const msWait = RTRandU32Ex(RT_MS_1SEC, RT_MS_5SEC); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Waiting %RU32ms to let guest and the audio stack process remaining data ...\n", msWait); + RTThreadSleep(msWait); + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on guest ...\n"); + int rc2 = AudioTestSvcClientTestSetEnd(&pTstEnv->u.Host.AtsClGuest, pTstEnv->szTag); + if (RT_FAILURE(rc2)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on guest failed with %Rrc\n", rc2); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on host (Validation Kit audio driver) ...\n"); + rc2 = AudioTestSvcClientTestSetEnd(&pTstEnv->u.Host.AtsClValKit, pTstEnv->szTag); + if (RT_FAILURE(rc2)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Ending test set on host (Validation Kit audio driver) failed with %Rrc\n", rc2); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if ( !g_fTerminate + && RT_SUCCESS(rc)) + { + /* + * Download guest + Validation Kit audio driver test sets to our output directory. + */ + char szFileName[RTPATH_MAX]; + if (RTStrPrintf2(szFileName, sizeof(szFileName), "%s-guest.tar.gz", pTstEnv->szTag)) + { + rc = RTPathJoin(pTstEnv->u.Host.szPathTestSetGuest, sizeof(pTstEnv->u.Host.szPathTestSetGuest), + pTstEnv->szPathOut, szFileName); + if (RT_SUCCESS(rc)) + { + if (RTStrPrintf2(szFileName, sizeof(szFileName), "%s-host.tar.gz", pTstEnv->szTag)) + { + rc = RTPathJoin(pTstEnv->u.Host.szPathTestSetValKit, sizeof(pTstEnv->u.Host.szPathTestSetValKit), + pTstEnv->szPathOut, szFileName); + } + else + rc = VERR_BUFFER_OVERFLOW; + } + else + rc = VERR_BUFFER_OVERFLOW; + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Downloading guest test set to '%s'\n", + pTstEnv->u.Host.szPathTestSetGuest); + rc = AudioTestSvcClientTestSetDownload(&pTstEnv->u.Host.AtsClGuest, + pTstEnv->szTag, pTstEnv->u.Host.szPathTestSetGuest); + } + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Downloading host test set to '%s'\n", + pTstEnv->u.Host.szPathTestSetValKit); + rc = AudioTestSvcClientTestSetDownload(&pTstEnv->u.Host.AtsClValKit, + pTstEnv->szTag, pTstEnv->u.Host.szPathTestSetValKit); + } + } + else + rc = VERR_BUFFER_OVERFLOW; + + if ( RT_SUCCESS(rc) + && !pTstEnv->fSkipVerify) + { + rc = audioVerifyOne(pTstEnv->u.Host.szPathTestSetGuest, pTstEnv->u.Host.szPathTestSetValKit, NULL /* pOpts */); + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verification skipped\n"); + + if (!pTstEnv->fSkipVerify) + { + RTFileDelete(pTstEnv->u.Host.szPathTestSetGuest); + RTFileDelete(pTstEnv->u.Host.szPathTestSetValKit); + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Leaving test set files behind\n"); + } + } + } + else + rc = VERR_NOT_IMPLEMENTED; + + /* Clean up. */ + RTDirRemove(pTstEnv->szPathTemp); + RTDirRemove(pTstEnv->szPathOut); + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test worker failed with %Rrc", rc); + + return rc; +} + +/** Option help for the 'test' command. */ +static DECLCALLBACK(const char *) audioTestCmdTestHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case 'a': return "Exclude all tests from the list (useful to enable single tests later with --include)"; + case 'b': return "The audio backend to use"; + case 'd': return "Go via DrvAudio instead of directly interfacing with the backend"; + case 'e': return "Exclude the given test id from the list"; + case 'i': return "Include the given test id in the list"; + case VKAT_TEST_OPT_COUNT: return "Number of test iterations to perform for selected tests\n" + " Default: random number"; + case VKAT_TEST_OPT_DEV: return "Name of the input/output device to use\n" + " Default: default device"; + case VKAT_TEST_OPT_TONE_DURATION_MS: return "Test tone duration to play / record (ms)\n" + " Default: random duration"; + case VKAT_TEST_OPT_TONE_VOL_PERCENT: return "Test tone volume (percent)\n" + " Default: 100"; + case VKAT_TEST_OPT_GUEST_ATS_ADDR: return "Address of guest ATS to connect to\n" + " Default: " ATS_TCP_DEF_CONNECT_GUEST_STR; + case VKAT_TEST_OPT_GUEST_ATS_PORT: return "Port of guest ATS to connect to (needs NAT port forwarding)\n" + " Default: 6042"; /* ATS_TCP_DEF_CONNECT_PORT_GUEST */ + case VKAT_TEST_OPT_HOST_ATS_ADDR: return "Address of host ATS to connect to\n" + " Default: " ATS_TCP_DEF_CONNECT_HOST_ADDR_STR; + case VKAT_TEST_OPT_HOST_ATS_PORT: return "Port of host ATS to connect to\n" + " Default: 6052"; /* ATS_TCP_DEF_BIND_PORT_VALKIT */ + case VKAT_TEST_OPT_MODE: return "Test mode to use when running the tests\n" + " Available modes:\n" + " guest: Run as a guest-side ATS\n" + " host: Run as a host-side ATS"; + case VKAT_TEST_OPT_NO_AUDIO_OK: return "Enables running without any found audio hardware (e.g. servers)"; + case VKAT_TEST_OPT_NO_VERIFY: return "Skips the verification step"; + case VKAT_TEST_OPT_OUTDIR: return "Output directory to use"; + case VKAT_TEST_OPT_PAUSE: return "Not yet implemented"; + case VKAT_TEST_OPT_PCM_HZ: return "PCM Hertz (Hz) rate to use\n" + " Default: 44100"; + case VKAT_TEST_OPT_PCM_BIT: return "PCM sample bits (i.e. 16) to use\n" + " Default: 16"; + case VKAT_TEST_OPT_PCM_CHAN: return "PCM channels to use\n" + " Default: 2"; + case VKAT_TEST_OPT_PCM_SIGNED: return "PCM samples to use (signed = true, unsigned = false)\n" + " Default: true"; + case VKAT_TEST_OPT_PROBE_BACKENDS: return "Probes all (available) backends until a working one is found"; + case VKAT_TEST_OPT_TAG: return "Test set tag to use"; + case VKAT_TEST_OPT_TEMPDIR: return "Temporary directory to use"; + case VKAT_TEST_OPT_VOL: return "Audio volume (percent) to use"; + case VKAT_TEST_OPT_TCP_BIND_ADDRESS: return "TCP address listening to (server mode)"; + case VKAT_TEST_OPT_TCP_BIND_PORT: return "TCP port listening to (server mode)"; + case VKAT_TEST_OPT_TCP_CONNECT_ADDRESS: return "TCP address to connect to (client mode)"; + case VKAT_TEST_OPT_TCP_CONNECT_PORT: return "TCP port to connect to (client mode)"; + default: + break; + } + return NULL; +} + +/** + * Main (entry) function for the testing functionality of VKAT. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioTestMain(PRTGETOPTSTATE pGetState) +{ + AUDIOTESTENV TstEnv; + audioTestEnvInit(&TstEnv); + + int rc; + + PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend(); + uint8_t cPcmSampleBit = 0; + uint8_t cPcmChannels = 0; + uint32_t uPcmHz = 0; + bool fPcmSigned = true; + bool fProbeBackends = false; + bool fNoAudioOk = false; + + const char *pszGuestTcpAddr = NULL; + uint16_t uGuestTcpPort = ATS_TCP_DEF_BIND_PORT_GUEST; + const char *pszValKitTcpAddr = NULL; + uint16_t uValKitTcpPort = ATS_TCP_DEF_BIND_PORT_VALKIT; + + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion))) + { + switch (ch) + { + case 'a': + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + g_aTests[i].fExcluded = true; + break; + + case 'b': + pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz); + if (pDrvReg == NULL) + return RTEXITCODE_SYNTAX; + break; + + case 'd': + TstEnv.IoOpts.fWithDrvAudio = true; + break; + + case 'e': + if (ValueUnion.u32 >= RT_ELEMENTS(g_aTests)) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --exclude", ValueUnion.u32); + g_aTests[ValueUnion.u32].fExcluded = true; + break; + + case VKAT_TEST_OPT_GUEST_ATS_ADDR: + pszGuestTcpAddr = ValueUnion.psz; + break; + + case VKAT_TEST_OPT_GUEST_ATS_PORT: + uGuestTcpPort = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_HOST_ATS_ADDR: + pszValKitTcpAddr = ValueUnion.psz; + break; + + case VKAT_TEST_OPT_HOST_ATS_PORT: + uValKitTcpPort = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_MODE: + if (TstEnv.enmMode != AUDIOTESTMODE_UNKNOWN) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Test mode (guest / host) already specified"); + TstEnv.enmMode = RTStrICmp(ValueUnion.psz, "guest") == 0 ? AUDIOTESTMODE_GUEST : AUDIOTESTMODE_HOST; + break; + + case VKAT_TEST_OPT_NO_AUDIO_OK: + fNoAudioOk = true; + break; + + case VKAT_TEST_OPT_NO_VERIFY: + TstEnv.fSkipVerify = true; + break; + + case 'i': + if (ValueUnion.u32 >= RT_ELEMENTS(g_aTests)) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --include", ValueUnion.u32); + g_aTests[ValueUnion.u32].fExcluded = false; + break; + + case VKAT_TEST_OPT_COUNT: + TstEnv.cIterations = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_DEV: + rc = RTStrCopy(TstEnv.szDev, sizeof(TstEnv.szDev), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExitFailure("Failed to copy out device: %Rrc", rc); + break; + + case VKAT_TEST_OPT_TONE_DURATION_MS: + TstEnv.ToneParms.msDuration = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_TONE_VOL_PERCENT: + TstEnv.ToneParms.uVolumePercent = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_PAUSE: + return RTMsgErrorExitFailure("Not yet implemented!"); + + case VKAT_TEST_OPT_OUTDIR: + rc = RTStrCopy(TstEnv.szPathOut, sizeof(TstEnv.szPathOut), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExitFailure("Failed to copy out directory: %Rrc", rc); + break; + + case VKAT_TEST_OPT_PCM_BIT: + cPcmSampleBit = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_PCM_CHAN: + cPcmChannels = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_PCM_HZ: + uPcmHz = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_PCM_SIGNED: + fPcmSigned = ValueUnion.f; + break; + + case VKAT_TEST_OPT_PROBE_BACKENDS: + fProbeBackends = true; + break; + + case VKAT_TEST_OPT_TAG: + rc = RTStrCopy(TstEnv.szTag, sizeof(TstEnv.szTag), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Tag invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_TEMPDIR: + rc = RTStrCopy(TstEnv.szPathTemp, sizeof(TstEnv.szPathTemp), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Temp dir invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_VOL: + TstEnv.IoOpts.uVolumePercent = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_TCP_BIND_ADDRESS: + rc = RTStrCopy(TstEnv.TcpOpts.szBindAddr, sizeof(TstEnv.TcpOpts.szBindAddr), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Bind address invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_TCP_BIND_PORT: + TstEnv.TcpOpts.uBindPort = ValueUnion.u16; + break; + + case VKAT_TEST_OPT_TCP_CONNECT_ADDRESS: + rc = RTStrCopy(TstEnv.TcpOpts.szConnectAddr, sizeof(TstEnv.TcpOpts.szConnectAddr), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Connect address invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_TCP_CONNECT_PORT: + TstEnv.TcpOpts.uConnectPort = ValueUnion.u16; + break; + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdTest); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + /* + * Start testing. + */ + RTTestBanner(g_hTest); + + if (TstEnv.enmMode == AUDIOTESTMODE_UNKNOWN) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "No test mode (--mode) specified!\n"); + + /* Validate TCP options. */ + if ( TstEnv.TcpOpts.szBindAddr[0] + && TstEnv.TcpOpts.szConnectAddr[0]) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Only one TCP connection mode (connect as client *or* bind as server) can be specified) at a time!\n"); + + /* Set new (override standard) I/O PCM properties if set by the user. */ + if ( cPcmSampleBit + || cPcmChannels + || uPcmHz) + { + PDMAudioPropsInit(&TstEnv.IoOpts.Props, + cPcmSampleBit ? cPcmSampleBit / 2 : 2 /* 16-bit */, fPcmSigned /* fSigned */, + cPcmChannels ? cPcmChannels : 2 /* Stereo */, uPcmHz ? uPcmHz : 44100); + } + + /* Do this first before everything else below. */ + rc = AudioTestDriverStackPerformSelftest(); + if (RT_FAILURE(rc)) + { + if (!fNoAudioOk) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Testing driver stack failed: %Rrc\n", rc); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Warning: Testing driver stack not possible (%Rrc), but --no-audio-ok was specified. Running on a server without audio hardware?\n", rc); + } + + AUDIOTESTDRVSTACK DrvStack; + if (fProbeBackends) + rc = audioTestDriverStackProbe(&DrvStack, pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, TstEnv.IoOpts.fWithDrvAudio); /** @todo Make in/out configurable, too. */ + else + rc = audioTestDriverStackInitEx(&DrvStack, pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, TstEnv.IoOpts.fWithDrvAudio); /** @todo Make in/out configurable, too. */ + if (RT_FAILURE(rc)) + { + if (!fNoAudioOk) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Unable to init driver stack: %Rrc\n", rc); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Warning: Initializing driver stack not possible (%Rrc), but --no-audio-ok was specified. Running on a server without audio hardware?\n", rc); + } + + PPDMAUDIOHOSTDEV pDev; + rc = audioTestDevicesEnumerateAndCheck(&DrvStack, TstEnv.szDev, &pDev); + if (RT_FAILURE(rc)) + { + if (!fNoAudioOk) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Enumerating device(s) failed: %Rrc\n", rc); + } + + /* For now all tests have the same test environment and driver stack. */ + rc = audioTestEnvCreate(&TstEnv, &DrvStack); + if (RT_SUCCESS(rc)) + rc = audioTestWorker(&TstEnv); + + audioTestEnvDestroy(&TstEnv); + audioTestDriverStackDelete(&DrvStack); + + if (RT_FAILURE(rc)) /* Let us know that something went wrong in case we forgot to mention it. */ + RTTestFailed(g_hTest, "Testing failed with %Rrc\n", rc); + + /* + * Print summary and exit. + */ + return RTTestSummaryAndDestroy(g_hTest); +} + + +const VKATCMD g_CmdTest = +{ + "test", + audioTestMain, + "Runs audio tests and creates an audio test set.", + g_aCmdTestOptions, + RT_ELEMENTS(g_aCmdTestOptions), + audioTestCmdTestHelp, + true /* fNeedsTransport */ +}; + + +/********************************************************************************************************************************* +* Command: verify * +*********************************************************************************************************************************/ + +static int audioVerifyOpenTestSet(const char *pszPathSet, PAUDIOTESTSET pSet) +{ + int rc; + + char szPathExtracted[RTPATH_MAX]; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Opening test set '%s'\n", pszPathSet); + + const bool fPacked = AudioTestSetIsPacked(pszPathSet); + + if (fPacked) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set is an archive and needs to be unpacked\n"); + + if (!RTFileExists(pszPathSet)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set '%s' does not exist\n", pszPathSet); + rc = VERR_FILE_NOT_FOUND; + } + else + rc = VINF_SUCCESS; + + if (RT_SUCCESS(rc)) + { + char szPathTemp[RTPATH_MAX]; + rc = RTPathTemp(szPathTemp, sizeof(szPathTemp)); + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using temporary directory '%s'\n", szPathTemp); + + rc = RTPathJoin(szPathExtracted, sizeof(szPathExtracted), szPathTemp, "vkat-testset-XXXX"); + if (RT_SUCCESS(rc)) + { + rc = RTDirCreateTemp(szPathExtracted, 0755); + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Unpacking archive to '%s'\n", szPathExtracted); + rc = AudioTestSetUnpack(pszPathSet, szPathExtracted); + if (RT_SUCCESS(rc)) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Archive successfully unpacked\n"); + } + } + } + } + } + else + rc = VINF_SUCCESS; + + if (RT_SUCCESS(rc)) + rc = AudioTestSetOpen(pSet, fPacked ? szPathExtracted : pszPathSet); + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Unable to open / unpack test set archive: %Rrc", rc); + + return rc; +} + +/** + * Verifies one test set pair. + * + * @returns VBox status code. + * @param pszPathSetA Absolute path to test set A. + * @param pszPathSetB Absolute path to test set B. + * @param pOpts Verification options to use. Optional. + * When NULL, the (very strict) defaults will be used. + */ +static int audioVerifyOne(const char *pszPathSetA, const char *pszPathSetB, PAUDIOTESTVERIFYOPTS pOpts) +{ + RTTestSubF(g_hTest, "Verifying"); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verifying test set '%s' with test set '%s'\n", pszPathSetA, pszPathSetB); + + AUDIOTESTSET SetA, SetB; + int rc = audioVerifyOpenTestSet(pszPathSetA, &SetA); + if (RT_SUCCESS(rc)) + { + rc = audioVerifyOpenTestSet(pszPathSetB, &SetB); + if (RT_SUCCESS(rc)) + { + AUDIOTESTERRORDESC errDesc; + if (pOpts) + rc = AudioTestSetVerifyEx(&SetA, &SetB, pOpts, &errDesc); + else + rc = AudioTestSetVerify(&SetA, &SetB, &errDesc); + if (RT_SUCCESS(rc)) + { + uint32_t const cErr = AudioTestErrorDescCount(&errDesc); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "%RU32 errors occurred while verifying\n", cErr); + + /** @todo Use some AudioTestErrorXXX API for enumeration here later. */ + PAUDIOTESTERRORENTRY pErrEntry; + RTListForEach(&errDesc.List, pErrEntry, AUDIOTESTERRORENTRY, Node) + { + if (RT_FAILURE(pErrEntry->rc)) + RTTestFailed(g_hTest, "%s\n", pErrEntry->szDesc); + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "%s\n", pErrEntry->szDesc); + } + + if (cErr == 0) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verification successful\n"); + + AudioTestErrorDescDestroy(&errDesc); + } + else + RTTestFailed(g_hTest, "Verification failed with %Rrc", rc); + +#ifdef DEBUG + if (g_fDrvAudioDebug) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "\n" + "Use the following command line to re-run verification in the debugger:\n" + "gdb --args ./VBoxAudioTest -vvvv --debug-audio verify \"%s\" \"%s\"\n", + SetA.szPathAbs, SetB.szPathAbs); +#endif + if (!g_fDrvAudioDebug) /* Don't wipe stuff when debugging. Can be useful for introspecting data. */ + AudioTestSetWipe(&SetB); + AudioTestSetClose(&SetB); + } + + if (!g_fDrvAudioDebug) /* Ditto. */ + AudioTestSetWipe(&SetA); + AudioTestSetClose(&SetA); + } + + RTTestSubDone(g_hTest); + + return rc; +} + +/** Option help for the 'verify' command. */ +static DECLCALLBACK(const char *) audioTestCmdVerifyHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case VKAT_VERIFY_OPT_MAX_DIFF_COUNT: return "Specifies the maximum number of differences\n" + " Default: 0 (strict)"; + case VKAT_VERIFY_OPT_MAX_DIFF_PERCENT: return "Specifies the maximum difference (percent)\n" + " Default: 0 (strict)"; + case VKAT_VERIFY_OPT_MAX_SIZE_PERCENT: return "Specifies the maximum size difference (percent)\n" + " Default: 1 (strict)"; + case VKAT_VERIFY_OPT_NORMALIZE: return "Enables / disables audio data normalization\n" + " Default: false"; + default: + break; + } + return NULL; +} + +/** + * Main (entry) function for the verification functionality of VKAT. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioVerifyMain(PRTGETOPTSTATE pGetState) +{ + /* + * Parse options and process arguments. + */ + const char *apszSets[2] = { NULL, NULL }; + unsigned iTestSet = 0; + + AUDIOTESTVERIFYOPTS Opts; + AudioTestSetVerifyOptsInit(&Opts); + + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion))) + { + switch (ch) + { + case VKAT_VERIFY_OPT_MAX_DIFF_COUNT: + Opts.cMaxDiff = ValueUnion.u32; + break; + + case VKAT_VERIFY_OPT_MAX_DIFF_PERCENT: + Opts.uMaxDiffPercent = ValueUnion.u8; + break; + + case VKAT_VERIFY_OPT_MAX_SIZE_PERCENT: + Opts.uMaxSizePercent = ValueUnion.u8; + break; + + case VKAT_VERIFY_OPT_NORMALIZE: + Opts.fNormalize = ValueUnion.f; + break; + + case VINF_GETOPT_NOT_OPTION: + if (iTestSet == 0) + RTTestBanner(g_hTest); + if (iTestSet >= RT_ELEMENTS(apszSets)) + return RTMsgErrorExitFailure("Only two test sets can be verified at one time"); + apszSets[iTestSet++] = ValueUnion.psz; + break; + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdVerify); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + if (!iTestSet) + return RTMsgErrorExitFailure("At least one test set must be specified"); + + int rc = VINF_SUCCESS; + + /* + * If only test set A is given, default to the current directory + * for test set B. + */ + char szDirCur[RTPATH_MAX]; + if (iTestSet == 1) + { + rc = RTPathGetCurrent(szDirCur, sizeof(szDirCur)); + if (RT_SUCCESS(rc)) + apszSets[1] = szDirCur; + else + RTTestFailed(g_hTest, "Failed to retrieve current directory: %Rrc", rc); + } + + if (RT_SUCCESS(rc)) + audioVerifyOne(apszSets[0], apszSets[1], &Opts); + + /* + * Print summary and exit. + */ + return RTTestSummaryAndDestroy(g_hTest); +} + + +const VKATCMD g_CmdVerify = +{ + "verify", + audioVerifyMain, + "Verifies a formerly created audio test set.", + g_aCmdVerifyOptions, + RT_ELEMENTS(g_aCmdVerifyOptions), + audioTestCmdVerifyHelp, + false /* fNeedsTransport */ +}; + + +/********************************************************************************************************************************* +* Main * +*********************************************************************************************************************************/ + +/** + * Ctrl-C signal handler. + * + * This just sets g_fTerminate and hope it will be noticed soon. + * + * On non-Windows it restores the SIGINT action to default, so that a second + * Ctrl-C will have the normal effect (just in case the code doesn't respond to + * g_fTerminate). + */ +#ifdef RT_OS_WINDOWS +static BOOL CALLBACK audioTestConsoleCtrlHandler(DWORD dwCtrlType) RT_NOEXCEPT +{ + if (dwCtrlType != CTRL_C_EVENT && dwCtrlType != CTRL_BREAK_EVENT) + return false; + RTPrintf(dwCtrlType == CTRL_C_EVENT ? "Ctrl-C!\n" : "Ctrl-Break!\n"); + + ASMAtomicWriteBool(&g_fTerminate, true); + + return true; +} +#else +static void audioTestSignalHandler(int iSig) RT_NOEXCEPT +{ + Assert(iSig == SIGINT); RT_NOREF(iSig); + RTPrintf("Ctrl-C!\n"); + + ASMAtomicWriteBool(&g_fTerminate, true); + + signal(SIGINT, SIG_DFL); +} +#endif + +/** + * Commands. + */ +static const VKATCMD * const g_apCommands[] = +{ + &g_CmdTest, + &g_CmdVerify, + &g_CmdBackends, + &g_CmdEnum, + &g_CmdPlay, + &g_CmdRec, + &g_CmdSelfTest +}; + +/** + * Shows tool usage text. + */ +RTEXITCODE audioTestUsage(PRTSTREAM pStrm, PCVKATCMD pOnlyCmd) +{ + RTStrmPrintf(pStrm, "usage: %s [global options] <command> [command-options]\n", RTProcShortName()); + RTStrmPrintf(pStrm, + "\n" + "Global Options:\n" + " --debug-audio\n" + " Enables (DrvAudio) debugging\n" + " --debug-audio-path=<path>\n" + " Tells DrvAudio where to put its debug output (wav-files)\n" + " -q, --quiet\n" + " Sets verbosity to zero\n" + " -v, --verbose\n" + " Increase verbosity\n" + " -V, --version\n" + " Displays version\n" + " -h, -?, --help\n" + " Displays help\n" + ); + + for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++) + { + PCVKATCMD const pCmd = g_apCommands[iCmd]; + if (!pOnlyCmd || pCmd == pOnlyCmd) + { + RTStrmPrintf(pStrm, + "\n" + "Command '%s':\n" + " %s\n" + "Options for '%s':\n", + pCmd->pszCommand, pCmd->pszDesc, pCmd->pszCommand); + PCRTGETOPTDEF const paOptions = pCmd->paOptions; + for (unsigned i = 0; i < pCmd->cOptions; i++) + { + if (RT_C_IS_PRINT(paOptions[i].iShort)) + RTStrmPrintf(pStrm, " -%c, %s\n", paOptions[i].iShort, paOptions[i].pszLong); + else + RTStrmPrintf(pStrm, " %s\n", paOptions[i].pszLong); + + const char *pszHelp = NULL; + if (pCmd->pfnOptionHelp) + pszHelp = pCmd->pfnOptionHelp(&paOptions[i]); + if (pszHelp) + RTStrmPrintf(pStrm, " %s\n", pszHelp); + } + + if (pCmd->fNeedsTransport) + for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++) + g_apTransports[iTx]->pfnUsage(pStrm); + } + } + + return RTEXITCODE_SUCCESS; +} + +/** + * Lists the commands and their descriptions. + */ +static RTEXITCODE audioTestListCommands(PRTSTREAM pStrm) +{ + RTStrmPrintf(pStrm, "Commands:\n"); + for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++) + RTStrmPrintf(pStrm, "%8s - %s\n", g_apCommands[iCmd]->pszCommand, g_apCommands[iCmd]->pszDesc); + return RTEXITCODE_SUCCESS; +} + +/** + * Shows tool version. + */ +RTEXITCODE audioTestVersion(void) +{ + RTPrintf("%s\n", RTBldCfgRevisionStr()); + return RTEXITCODE_SUCCESS; +} + +/** + * Shows the logo. + * + * @param pStream Output stream to show logo on. + */ +void audioTestShowLogo(PRTSTREAM pStream) +{ + RTStrmPrintf(pStream, VBOX_PRODUCT " VKAT (Validation Kit Audio Test) Version " VBOX_VERSION_STRING " - r%s\n" + "Copyright (C) " VBOX_C_YEAR " " VBOX_VENDOR "\n\n", RTBldCfgRevisionStr()); +} + +int main(int argc, char **argv) +{ + /* + * Init IPRT. + */ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Handle special command line options which need parsing before + * everything else. + */ + /** @todo r=bird: this isn't at all syntactically sane, because you don't know + * how to parse past the command (can almost be done safely thought, since + * you've got the option definitions for every command at hand). So, if someone + * wants to play a file named "-v.wav", you'll incorrectly take that as two 'v' + * options. The parsing has to stop when you get to the command, i.e. first + * VINF_GETOPT_NOT_OPTION or anything that isn't a common option. Daemonizing + * when for instance encountering an invalid command, is not correct. + * + * Btw. you MUST however process the 'q' option in parallel to 'v' here, they + * are oposites. For instance '-vqvvv' is supposed to give you level 3 logging, + * not quiet! So, either you process both 'v' and 'q' here, or you pospone them + * (better option). + */ + /** @todo r=bird: Is the daemonizing needed? The testcase doesn't seem to use + * it... If you don't need it, drop it as it make the parsing complex + * and illogical. The --daemonized / --damonize options should be + * required to before the command, then okay. */ + bool fDaemonize = false; + bool fDaemonized = false; + + RTGETOPTSTATE GetState; + rc = RTGetOptInit(&GetState, argc, argv, g_aCmdCommonOptions, + RT_ELEMENTS(g_aCmdCommonOptions), 1 /*idxFirst*/, 0 /*fFlags - must not sort! */); + AssertRCReturn(rc, RTEXITCODE_INIT); + + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + switch (ch) + { + case AUDIO_TEST_OPT_CMN_DAEMONIZE: + fDaemonize = true; + break; + + case AUDIO_TEST_OPT_CMN_DAEMONIZED: + fDaemonized = true; + break; + + /* Has to be defined here and not in AUDIO_TEST_COMMON_OPTION_CASES, to get the logger + * configured before the specific command handlers down below come into play. */ + case 'v': + g_uVerbosity++; + break; + + default: + break; + } + } + + /** @todo add something to suppress this stuff. */ + audioTestShowLogo(g_pStdOut); + + if (fDaemonize) + { + if (!fDaemonized) + { + rc = RTProcDaemonize(argv, "--daemonized"); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcDaemonize() failed with %Rrc\n", rc); + + RTMsgInfo("Starting in background (daemonizing) ..."); + return RTEXITCODE_SUCCESS; + } + /* else continue running in background. */ + } + + /* + * Init test and globals. + * Note: Needs to be done *after* daemonizing, otherwise the child will fail! + */ + rc = RTTestCreate("AudioTest", &g_hTest); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTTestCreate() failed with %Rrc\n", rc); + +#ifdef RT_OS_WINDOWS + HRESULT hrc = CoInitializeEx(NULL /*pReserved*/, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY | COINIT_DISABLE_OLE1DDE); + if (FAILED(hrc)) + RTMsgWarning("CoInitializeEx failed: %#x", hrc); +#endif + + /* + * Configure release logging to go to stdout. + */ + RTUINT fFlags = RTLOGFLAGS_PREFIX_THREAD | RTLOGFLAGS_PREFIX_TIME_PROG; +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + fFlags |= RTLOGFLAGS_USECRLF; +#endif + static const char * const s_apszLogGroups[] = VBOX_LOGGROUP_NAMES; + rc = RTLogCreate(&g_pRelLogger, fFlags, "all.e.l", "VKAT_RELEASE_LOG", + RT_ELEMENTS(s_apszLogGroups), s_apszLogGroups, RTLOGDEST_STDOUT, NULL /*"vkat-release.log"*/); + if (RT_SUCCESS(rc)) + { + RTLogRelSetDefaultInstance(g_pRelLogger); + if (g_uVerbosity) + { + RTMsgInfo("Setting verbosity logging to level %u\n", g_uVerbosity); + switch (g_uVerbosity) /* Not very elegant, but has to do it for now. */ + { + case 1: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l+drv_host_audio.e.l+" + "audio_mixer.e.l+audio_test.e.l"); + break; + + case 2: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l.l2+drv_host_audio.e.l.l2+" + "audio_mixer.e.l.l2+audio_test.e.l.l2"); + break; + + case 3: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l.l2.l3+drv_host_audio.e.l.l2.l3+" + "audio_mixer.e.l.l2.l3+audio_test.e.l.l2.l3"); + break; + + case 4: + RT_FALL_THROUGH(); + default: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l.l2.l3.l4.f+drv_host_audio.e.l.l2.l3.l4.f+" + "audio_mixer.e.l.l2.l3.l4.f+audio_test.e.l.l2.l3.l4.f"); + break; + } + if (RT_FAILURE(rc)) + RTMsgError("Setting debug logging failed, rc=%Rrc\n", rc); + } + } + else + RTMsgWarning("Failed to create release logger: %Rrc", rc); + + /* + * Install a Ctrl-C signal handler. + */ +#ifdef RT_OS_WINDOWS + SetConsoleCtrlHandler(audioTestConsoleCtrlHandler, TRUE); +#else + struct sigaction sa; + RT_ZERO(sa); + sa.sa_handler = audioTestSignalHandler; + sigaction(SIGINT, &sa, NULL); +#endif + + /* + * Process common options. + */ + RT_ZERO(GetState); + rc = RTGetOptInit(&GetState, argc, argv, g_aCmdCommonOptions, + RT_ELEMENTS(g_aCmdCommonOptions), 1 /*idxFirst*/, 0 /*fFlags - must not sort! */); + AssertRCReturn(rc, RTEXITCODE_INIT); + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + switch (ch) + { + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, NULL); + + case VINF_GETOPT_NOT_OPTION: + { + for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++) + { + PCVKATCMD const pCmd = g_apCommands[iCmd]; + if (strcmp(ValueUnion.psz, pCmd->pszCommand) == 0) + { + /* Count the combined option definitions: */ + size_t cCombinedOptions = pCmd->cOptions + RT_ELEMENTS(g_aCmdCommonOptions); + if (pCmd->fNeedsTransport) + for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++) + cCombinedOptions += g_apTransports[iTx]->cOpts; + + /* Combine the option definitions: */ + PRTGETOPTDEF paCombinedOptions = (PRTGETOPTDEF)RTMemAlloc(cCombinedOptions * sizeof(RTGETOPTDEF)); + if (paCombinedOptions) + { + uint32_t idxOpts = 0; + memcpy(paCombinedOptions, g_aCmdCommonOptions, sizeof(g_aCmdCommonOptions)); + idxOpts += RT_ELEMENTS(g_aCmdCommonOptions); + + memcpy(&paCombinedOptions[idxOpts], pCmd->paOptions, pCmd->cOptions * sizeof(RTGETOPTDEF)); + idxOpts += (uint32_t)pCmd->cOptions; + + if (pCmd->fNeedsTransport) + for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++) + { + memcpy(&paCombinedOptions[idxOpts], + g_apTransports[iTx]->paOpts, g_apTransports[iTx]->cOpts * sizeof(RTGETOPTDEF)); + idxOpts += (uint32_t)g_apTransports[iTx]->cOpts; + } + + /* Re-initialize the option getter state and pass it to the command handler. */ + rc = RTGetOptInit(&GetState, argc, argv, paCombinedOptions, cCombinedOptions, + GetState.iNext /*idxFirst*/, RTGETOPTINIT_FLAGS_OPTS_FIRST); + if (RT_SUCCESS(rc)) + { + RTEXITCODE rcExit = pCmd->pfnHandler(&GetState); + RTMemFree(paCombinedOptions); + return rcExit; + } + RTMemFree(paCombinedOptions); + return RTMsgErrorExitFailure("RTGetOptInit failed for '%s': %Rrc", ValueUnion.psz, rc); + } + return RTMsgErrorExitFailure("Out of memory!"); + } + } + RTMsgError("Unknown command '%s'!\n", ValueUnion.psz); + audioTestListCommands(g_pStdErr); + return RTEXITCODE_SYNTAX; + } + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + RTMsgError("No command specified!\n"); + audioTestListCommands(g_pStdErr); + return RTEXITCODE_SYNTAX; +} diff --git a/src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp b/src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp new file mode 100644 index 00000000..18eb9487 --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/vkatCmdGeneric.cpp @@ -0,0 +1,1169 @@ +/* $Id: vkatCmdGeneric.cpp $ */ +/** @file + * Validation Kit Audio Test (VKAT) utility for testing and validating the audio stack. + */ + +/* + * Copyright (C) 2021-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/errcore.h> +#include <iprt/message.h> +#include <iprt/rand.h> +#include <iprt/test.h> + +#include "vkatInternal.h" + + +/********************************************************************************************************************************* +* Command: backends * +*********************************************************************************************************************************/ + +/** + * Options for 'backends'. + */ +static const RTGETOPTDEF g_aCmdBackendsOptions[] = +{ + { "--dummy", 'd', RTGETOPT_REQ_NOTHING }, /* just a placeholder */ +}; + + +/** The 'backends' command option help. */ +static DECLCALLBACK(const char *) audioTestCmdBackendsHelp(PCRTGETOPTDEF pOpt) +{ + RT_NOREF(pOpt); + return NULL; +} + +/** + * The 'backends' command handler. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioTestCmdBackendsHandler(PRTGETOPTSTATE pGetState) +{ + /* + * Parse options. + */ + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0) + { + switch (ch) + { + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdBackends); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + /* + * List the backends. + */ + RTPrintf("Backends (%u):\n", g_cBackends); + for (size_t i = 0; i < g_cBackends; i++) + RTPrintf(" %12s - %s\n", g_aBackends[i].pszName, g_aBackends[i].pDrvReg->pszDescription); + + return RTEXITCODE_SUCCESS; +} + + +/** + * Command table entry for 'backends'. + */ +const VKATCMD g_CmdBackends = +{ + /* .pszCommand = */ "backends", + /* .pfnHandler = */ audioTestCmdBackendsHandler, + /* .pszDesc = */ "Lists the compiled in audio backends.", + /* .paOptions = */ g_aCmdBackendsOptions, + /* .cOptions = */ 0 /*RT_ELEMENTS(g_aCmdBackendsOptions)*/, + /* .pfnOptionHelp = */ audioTestCmdBackendsHelp, + /* .fNeedsTransport = */ false +}; + + +/********************************************************************************************************************************* +* Command: enum * +*********************************************************************************************************************************/ + + + +/** + * Long option values for the 'enum' command. + */ +enum +{ + VKAT_ENUM_OPT_PROBE_BACKENDS = 900 +}; + +/** + * Options for 'enum'. + */ +static const RTGETOPTDEF g_aCmdEnumOptions[] = +{ + { "--backend", 'b', RTGETOPT_REQ_STRING }, + { "--probe-backends", VKAT_ENUM_OPT_PROBE_BACKENDS, RTGETOPT_REQ_NOTHING } +}; + + +/** The 'enum' command option help. */ +static DECLCALLBACK(const char *) audioTestCmdEnumHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case 'b': return "The audio backend to use"; + case VKAT_ENUM_OPT_PROBE_BACKENDS: return "Probes all (available) backends until a working one is found"; + default: return NULL; + } +} + +/** + * The 'enum' command handler. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioTestCmdEnumHandler(PRTGETOPTSTATE pGetState) +{ + /* + * Parse options. + */ + /* Option values: */ + PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend(); + bool fProbeBackends = false; + + /* Argument processing loop: */ + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0) + { + switch (ch) + { + case 'b': + pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz); + if (pDrvReg == NULL) + return RTEXITCODE_SYNTAX; + break; + + case VKAT_ENUM_OPT_PROBE_BACKENDS: + fProbeBackends = true; + break; + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdEnum); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + int rc; + + AUDIOTESTDRVSTACK DrvStack; + if (fProbeBackends) + rc = audioTestDriverStackProbe(&DrvStack, pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, false /* fWithDrvAudio */); + else + rc = audioTestDriverStackInitEx(&DrvStack, pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, false /* fWithDrvAudio */); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unable to init driver stack: %Rrc\n", rc); + + /* + * Do the enumeration. + */ + RTEXITCODE rcExit = RTEXITCODE_FAILURE; + + if (DrvStack.pIHostAudio->pfnGetDevices) + { + PDMAUDIOHOSTENUM Enum; + rc = DrvStack.pIHostAudio->pfnGetDevices(DrvStack.pIHostAudio, &Enum); + if (RT_SUCCESS(rc)) + { + RTPrintf("Found %u device%s\n", Enum.cDevices, Enum.cDevices != 1 ? "s" : ""); + + PPDMAUDIOHOSTDEV pHostDev; + RTListForEach(&Enum.LstDevices, pHostDev, PDMAUDIOHOSTDEV, ListEntry) + { + RTPrintf("\nDevice \"%s\":\n", pHostDev->pszName); + + char szFlags[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN]; + if (pHostDev->cMaxInputChannels && !pHostDev->cMaxOutputChannels && pHostDev->enmUsage == PDMAUDIODIR_IN) + RTPrintf(" Input: max %u channels (%s)\n", + pHostDev->cMaxInputChannels, PDMAudioHostDevFlagsToString(szFlags, pHostDev->fFlags)); + else if (!pHostDev->cMaxInputChannels && pHostDev->cMaxOutputChannels && pHostDev->enmUsage == PDMAUDIODIR_OUT) + RTPrintf(" Output: max %u channels (%s)\n", + pHostDev->cMaxOutputChannels, PDMAudioHostDevFlagsToString(szFlags, pHostDev->fFlags)); + else + RTPrintf(" %s: max %u output channels, max %u input channels (%s)\n", + PDMAudioDirGetName(pHostDev->enmUsage), pHostDev->cMaxOutputChannels, + pHostDev->cMaxInputChannels, PDMAudioHostDevFlagsToString(szFlags, pHostDev->fFlags)); + + if (pHostDev->pszId && *pHostDev->pszId) + RTPrintf(" ID: \"%s\"\n", pHostDev->pszId); + } + + PDMAudioHostEnumDelete(&Enum); + } + else + rcExit = RTMsgErrorExitFailure("Enumeration failed: %Rrc\n", rc); + } + else + rcExit = RTMsgErrorExitFailure("Enumeration not supported by backend '%s'\n", pDrvReg->szName); + audioTestDriverStackDelete(&DrvStack); + + return RTEXITCODE_SUCCESS; +} + + +/** + * Command table entry for 'enum'. + */ +const VKATCMD g_CmdEnum = +{ + "enum", + audioTestCmdEnumHandler, + "Enumerates audio devices.", + g_aCmdEnumOptions, + RT_ELEMENTS(g_aCmdEnumOptions), + audioTestCmdEnumHelp, + false /* fNeedsTransport */ +}; + + + + +/********************************************************************************************************************************* +* Command: play * +*********************************************************************************************************************************/ + +/** + * Worker for audioTestPlayOne implementing the play loop. + */ +static RTEXITCODE audioTestPlayOneInner(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTWAVEFILE pWaveFile, + PCPDMAUDIOSTREAMCFG pCfgAcq, const char *pszFile) +{ + uint32_t const cbPreBuffer = PDMAudioPropsFramesToBytes(pMix->pProps, pCfgAcq->Backend.cFramesPreBuffering); + uint64_t const nsStarted = RTTimeNanoTS(); + uint64_t nsDonePreBuffering = 0; + + /* + * Transfer data as quickly as we're allowed. + */ + uint8_t abSamples[16384]; + uint32_t const cbSamplesAligned = PDMAudioPropsFloorBytesToFrame(pMix->pProps, sizeof(abSamples)); + uint64_t offStream = 0; + while (!g_fTerminate) + { + /* Read a chunk from the wave file. */ + size_t cbSamples = 0; + int rc = AudioTestWaveFileRead(pWaveFile, abSamples, cbSamplesAligned, &cbSamples); + if (RT_SUCCESS(rc) && cbSamples > 0) + { + /* Pace ourselves a little. */ + if (offStream >= cbPreBuffer) + { + if (!nsDonePreBuffering) + nsDonePreBuffering = RTTimeNanoTS(); + uint64_t const cNsWritten = PDMAudioPropsBytesToNano64(pMix->pProps, offStream - cbPreBuffer); + uint64_t const cNsElapsed = RTTimeNanoTS() - nsStarted; + if (cNsWritten > cNsElapsed + RT_NS_10MS) + RTThreadSleep((cNsWritten - cNsElapsed - RT_NS_10MS / 2) / RT_NS_1MS); + } + + /* Transfer the data to the audio stream. */ + for (uint32_t offSamples = 0; offSamples < cbSamples;) + { + uint32_t const cbCanWrite = AudioTestMixStreamGetWritable(pMix); + if (cbCanWrite > 0) + { + uint32_t const cbToPlay = RT_MIN(cbCanWrite, (uint32_t)cbSamples - offSamples); + uint32_t cbPlayed = 0; + rc = AudioTestMixStreamPlay(pMix, &abSamples[offSamples], cbToPlay, &cbPlayed); + if (RT_SUCCESS(rc)) + { + if (cbPlayed) + { + offSamples += cbPlayed; + offStream += cbPlayed; + } + else + return RTMsgErrorExitFailure("Played zero bytes - %#x bytes reported playable!\n", cbCanWrite); + } + else + return RTMsgErrorExitFailure("Failed to play %#x bytes: %Rrc\n", cbToPlay, rc); + } + else if (AudioTestMixStreamIsOkay(pMix)) + RTThreadSleep(RT_MIN(RT_MAX(1, pCfgAcq->Device.cMsSchedulingHint), 256)); + else + return RTMsgErrorExitFailure("Stream is not okay!\n"); + } + } + else if (RT_SUCCESS(rc) && cbSamples == 0) + break; + else + return RTMsgErrorExitFailure("Error reading wav file '%s': %Rrc", pszFile, rc); + } + + /* + * Drain the stream. + */ + if (g_uVerbosity > 0) + RTMsgInfo("%'RU64 ns: Draining...\n", RTTimeNanoTS() - nsStarted); + int rc = AudioTestMixStreamDrain(pMix, true /*fSync*/); + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + RTMsgInfo("%'RU64 ns: Done\n", RTTimeNanoTS() - nsStarted); + } + else + return RTMsgErrorExitFailure("Draining failed: %Rrc", rc); + + return RTEXITCODE_SUCCESS; +} + +/** + * Worker for audioTestCmdPlayHandler that plays one file. + */ +static RTEXITCODE audioTestPlayOne(const char *pszFile, PCPDMDRVREG pDrvReg, const char *pszDevId, + PAUDIOTESTIOOPTS pIoOpts) +{ + char szTmp[128]; + + /* + * First we must open the file and determin the format. + */ + RTERRINFOSTATIC ErrInfo; + AUDIOTESTWAVEFILE WaveFile; + int rc = AudioTestWaveFileOpen(pszFile, &WaveFile, RTErrInfoInitStatic(&ErrInfo)); + if (RT_FAILURE(rc)) + return RTMsgErrorExitFailure("Failed to open '%s': %Rrc%#RTeim", pszFile, rc, &ErrInfo.Core); + + if (g_uVerbosity > 0) + { + RTMsgInfo("Opened '%s' for playing\n", pszFile); + RTMsgInfo("Format: %s\n", PDMAudioPropsToString(&WaveFile.Props, szTmp, sizeof(szTmp))); + RTMsgInfo("Size: %'RU32 bytes / %#RX32 / %'RU32 frames / %'RU64 ns\n", + WaveFile.cbSamples, WaveFile.cbSamples, + PDMAudioPropsBytesToFrames(&WaveFile.Props, WaveFile.cbSamples), + PDMAudioPropsBytesToNano(&WaveFile.Props, WaveFile.cbSamples)); + } + + /* + * Construct the driver stack. + */ + RTEXITCODE rcExit = RTEXITCODE_FAILURE; + AUDIOTESTDRVSTACK DrvStack; + rc = audioTestDriverStackInit(&DrvStack, pDrvReg, pIoOpts->fWithDrvAudio); + if (RT_SUCCESS(rc)) + { + /* + * Set the output device if one is specified. + */ + rc = audioTestDriverStackSetDevice(&DrvStack, PDMAUDIODIR_OUT, pszDevId); + if (RT_SUCCESS(rc)) + { + /* + * Open a stream for the output. + */ + uint8_t const cChannels = PDMAudioPropsChannels(&pIoOpts->Props); + + PDMAUDIOPCMPROPS ReqProps = WaveFile.Props; + if (cChannels != 0 && PDMAudioPropsChannels(&ReqProps) != cChannels) + PDMAudioPropsSetChannels(&ReqProps, cChannels); + + uint8_t const cbSample = PDMAudioPropsSampleSize(&pIoOpts->Props); + if (cbSample != 0) + PDMAudioPropsSetSampleSize(&ReqProps, cbSample); + + uint32_t const uHz = PDMAudioPropsHz(&pIoOpts->Props); + if (uHz != 0) + ReqProps.uHz = uHz; + + PDMAUDIOSTREAMCFG CfgAcq; + PPDMAUDIOSTREAM pStream = NULL; + rc = audioTestDriverStackStreamCreateOutput(&DrvStack, &ReqProps, pIoOpts->cMsBufferSize, + pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream, &CfgAcq); + if (RT_SUCCESS(rc)) + { + /* + * Automatically enable the mixer if the wave file and the + * output parameters doesn't match. + */ + if ( !pIoOpts->fWithMixer + && ( !PDMAudioPropsAreEqual(&WaveFile.Props, &pStream->Cfg.Props) + || pIoOpts->uVolumePercent != 100) + ) + { + RTMsgInfo("Enabling the mixer buffer.\n"); + pIoOpts->fWithMixer = true; + } + + /* + * Create a mixer wrapper. This is just a thin wrapper if fWithMixer + * is false, otherwise it's doing mixing, resampling and recoding. + */ + AUDIOTESTDRVMIXSTREAM Mix; + rc = AudioTestMixStreamInit(&Mix, &DrvStack, pStream, pIoOpts->fWithMixer ? &WaveFile.Props : NULL, 100 /*ms*/); + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + RTMsgInfo("Stream: %s cbBackend=%#RX32%s\n", + PDMAudioPropsToString(&pStream->Cfg.Props, szTmp, sizeof(szTmp)), + pStream->cbBackend, pIoOpts->fWithMixer ? " mixed" : ""); + + if (pIoOpts->fWithMixer) + AudioTestMixStreamSetVolume(&Mix, pIoOpts->uVolumePercent); + + /* + * Enable the stream and start playing. + */ + rc = AudioTestMixStreamEnable(&Mix); + if (RT_SUCCESS(rc)) + rcExit = audioTestPlayOneInner(&Mix, &WaveFile, &CfgAcq, pszFile); + else + rcExit = RTMsgErrorExitFailure("Enabling the output stream failed: %Rrc", rc); + + /* + * Clean up. + */ + AudioTestMixStreamTerm(&Mix); + } + audioTestDriverStackStreamDestroy(&DrvStack, pStream); + pStream = NULL; + } + else + rcExit = RTMsgErrorExitFailure("Creating output stream failed: %Rrc", rc); + } + else + rcExit = RTMsgErrorExitFailure("Failed to set output device to '%s': %Rrc", pszDevId, rc); + audioTestDriverStackDelete(&DrvStack); + } + else + rcExit = RTMsgErrorExitFailure("Driver stack construction failed: %Rrc", rc); + AudioTestWaveFileClose(&WaveFile); + return rcExit; +} + +/** + * Worker for audioTestCmdPlayHandler that plays one test tone. + */ +static RTEXITCODE audioTestPlayTestToneOne(PAUDIOTESTTONEPARMS pToneParms, + PCPDMDRVREG pDrvReg, const char *pszDevId, + PAUDIOTESTIOOPTS pIoOpts) +{ + char szTmp[128]; + + AUDIOTESTSTREAM TstStream; + RT_ZERO(TstStream); + + /* + * Construct the driver stack. + */ + RTEXITCODE rcExit = RTEXITCODE_FAILURE; + AUDIOTESTDRVSTACK DrvStack; + int rc = audioTestDriverStackInit(&DrvStack, pDrvReg, pIoOpts->fWithDrvAudio); + if (RT_SUCCESS(rc)) + { + /* + * Set the output device if one is specified. + */ + rc = audioTestDriverStackSetDevice(&DrvStack, PDMAUDIODIR_OUT, pszDevId); + if (RT_SUCCESS(rc)) + { + /* + * Open a stream for the output. + */ + uint8_t const cChannels = PDMAudioPropsChannels(&pIoOpts->Props); + + PDMAUDIOPCMPROPS ReqProps = pToneParms->Props; + if (cChannels != 0 && PDMAudioPropsChannels(&ReqProps) != cChannels) + PDMAudioPropsSetChannels(&ReqProps, cChannels); + + uint8_t const cbSample = PDMAudioPropsSampleSize(&pIoOpts->Props); + if (cbSample != 0) + PDMAudioPropsSetSampleSize(&ReqProps, cbSample); + + uint32_t const uHz = PDMAudioPropsHz(&pIoOpts->Props); + if (uHz != 0) + ReqProps.uHz = uHz; + + rc = audioTestDriverStackStreamCreateOutput(&DrvStack, &ReqProps, pIoOpts->cMsBufferSize, + pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &TstStream.pStream, &TstStream.Cfg); + if (RT_SUCCESS(rc)) + { + /* + * Automatically enable the mixer if the wave file and the + * output parameters doesn't match. + */ + if ( !pIoOpts->fWithMixer + && ( !PDMAudioPropsAreEqual(&pToneParms->Props, &TstStream.pStream->Cfg.Props) + || pToneParms->uVolumePercent != 100) + ) + { + RTMsgInfo("Enabling the mixer buffer.\n"); + pIoOpts->fWithMixer = true; + } + + /* + * Create a mixer wrapper. This is just a thin wrapper if fWithMixer + * is false, otherwise it's doing mixing, resampling and recoding. + */ + rc = AudioTestMixStreamInit(&TstStream.Mix, &DrvStack, TstStream.pStream, + pIoOpts->fWithMixer ? &pToneParms->Props : NULL, 100 /*ms*/); + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + RTMsgInfo("Stream: %s cbBackend=%#RX32%s\n", + PDMAudioPropsToString(&TstStream.pStream->Cfg.Props, szTmp, sizeof(szTmp)), + TstStream.pStream->cbBackend, pIoOpts->fWithMixer ? " mixed" : ""); + + /* + * Enable the stream and start playing. + */ + rc = AudioTestMixStreamEnable(&TstStream.Mix); + if (RT_SUCCESS(rc)) + { + if (pIoOpts->fWithMixer) + AudioTestMixStreamSetVolume(&TstStream.Mix, pToneParms->uVolumePercent); + + rc = audioTestPlayTone(pIoOpts, NULL /* pTstEnv */, &TstStream, pToneParms); + if (RT_SUCCESS(rc)) + rcExit = RTEXITCODE_SUCCESS; + } + else + rcExit = RTMsgErrorExitFailure("Enabling the output stream failed: %Rrc", rc); + + /* + * Clean up. + */ + AudioTestMixStreamTerm(&TstStream.Mix); + } + audioTestDriverStackStreamDestroy(&DrvStack, TstStream.pStream); + TstStream.pStream = NULL; + } + else + rcExit = RTMsgErrorExitFailure("Creating output stream failed: %Rrc", rc); + } + else + rcExit = RTMsgErrorExitFailure("Failed to set output device to '%s': %Rrc", pszDevId, rc); + audioTestDriverStackDelete(&DrvStack); + } + else + rcExit = RTMsgErrorExitFailure("Driver stack construction failed: %Rrc", rc); + return rcExit; +} + + +/** + * Long option values for the 'play' command. + */ +enum +{ + VKAT_PLAY_OPT_TONE_DUR = 900, + VKAT_PLAY_OPT_TONE_FREQ, + VKAT_PLAY_OPT_TONE_VOL, + VKAT_PLAY_OPT_VOL +}; + + +/** + * Options for 'play'. + */ +static const RTGETOPTDEF g_aCmdPlayOptions[] = +{ + { "--backend", 'b', RTGETOPT_REQ_STRING }, + { "--channels", 'c', RTGETOPT_REQ_UINT8 }, + { "--hz", 'f', RTGETOPT_REQ_UINT32 }, + { "--frequency", 'f', RTGETOPT_REQ_UINT32 }, + { "--sample-size", 'z', RTGETOPT_REQ_UINT8 }, + { "--test-tone", 't', RTGETOPT_REQ_NOTHING }, + { "--tone-dur", VKAT_PLAY_OPT_TONE_DUR, RTGETOPT_REQ_UINT32 }, + { "--tone-freq", VKAT_PLAY_OPT_TONE_FREQ, RTGETOPT_REQ_UINT32 }, + { "--tone-vol", VKAT_PLAY_OPT_TONE_VOL, RTGETOPT_REQ_UINT32 }, + { "--output-device", 'o', RTGETOPT_REQ_STRING }, + { "--with-drv-audio", 'd', RTGETOPT_REQ_NOTHING }, + { "--with-mixer", 'm', RTGETOPT_REQ_NOTHING }, + { "--vol", VKAT_PLAY_OPT_VOL, RTGETOPT_REQ_UINT8 } +}; + + +/** The 'play' command option help. */ +static DECLCALLBACK(const char *) audioTestCmdPlayHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case 'b': return "The audio backend to use"; + case 'c': return "Number of backend output channels"; + case 'd': return "Go via DrvAudio instead of directly interfacing with the backend"; + case 'f': return "Output frequency (Hz)"; + case 'z': return "Output sample size (bits)"; + case 't': return "Plays a test tone. Can be specified multiple times"; + case 'm': return "Go via the mixer"; + case 'o': return "The ID of the output device to use"; + case VKAT_PLAY_OPT_TONE_DUR: return "Test tone duration (ms)"; + case VKAT_PLAY_OPT_TONE_FREQ: return "Test tone frequency (Hz)"; + case VKAT_PLAY_OPT_TONE_VOL: return "Test tone volume (percent)"; + case VKAT_PLAY_OPT_VOL: return "Playback volume (percent)"; + default: return NULL; + } +} + + +/** + * The 'play' command handler. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioTestCmdPlayHandler(PRTGETOPTSTATE pGetState) +{ + /* Option values: */ + PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend(); + const char *pszDevId = NULL; + uint32_t cTestTones = 0; + uint8_t cbSample = 0; + uint8_t cChannels = 0; + uint32_t uHz = 0; + + AUDIOTESTIOOPTS IoOpts; + audioTestIoOptsInitDefaults(&IoOpts); + + AUDIOTESTTONEPARMS ToneParms; + audioTestToneParmsInit(&ToneParms); + + /* Argument processing loop: */ + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0) + { + switch (ch) + { + case 'b': + pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz); + if (pDrvReg == NULL) + return RTEXITCODE_SYNTAX; + break; + + case 'c': + cChannels = ValueUnion.u8; + break; + + case 'd': + IoOpts.fWithDrvAudio = true; + break; + + case 'f': + uHz = ValueUnion.u32; + break; + + case 'm': + IoOpts.fWithMixer = true; + break; + + case 'o': + pszDevId = ValueUnion.psz; + break; + + case 't': + cTestTones++; + break; + + case 'z': + cbSample = ValueUnion.u8 / 8; + break; + + case VKAT_PLAY_OPT_TONE_DUR: + ToneParms.msDuration = ValueUnion.u32; + break; + + case VKAT_PLAY_OPT_TONE_FREQ: + ToneParms.dbFreqHz = ValueUnion.u32; + break; + + case VKAT_PLAY_OPT_TONE_VOL: + ToneParms.uVolumePercent = ValueUnion.u8; + if (ToneParms.uVolumePercent > 100) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid tonevolume (0-100)"); + break; + + case VKAT_PLAY_OPT_VOL: + IoOpts.uVolumePercent = ValueUnion.u8; + if (IoOpts.uVolumePercent > 100) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid playback volume (0-100)"); + break; + + case VINF_GETOPT_NOT_OPTION: + { + if (cTestTones) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Playing test tones (-t) cannot be combined with playing files"); + + /* Set new (override standard) I/O PCM properties if set by the user. */ + PDMAudioPropsInit(&IoOpts.Props, + cbSample ? cbSample : 2 /* 16-bit */, true /* fSigned */, + cChannels ? cChannels : 2 /* Stereo */, uHz ? uHz : 44100); + + RTEXITCODE rcExit = audioTestPlayOne(ValueUnion.psz, pDrvReg, pszDevId, &IoOpts); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + break; + } + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdPlay); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + while (cTestTones--) + { + /* Use some sane defaults if no PCM props are set by the user. */ + PDMAudioPropsInit(&ToneParms.Props, + cbSample ? cbSample : 2 /* 16-bit */, true /* fSigned */, + cChannels ? cChannels : 2 /* Stereo */, uHz ? uHz : 44100); + + RTEXITCODE rcExit = audioTestPlayTestToneOne(&ToneParms, pDrvReg, pszDevId, &IoOpts); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + } + + return RTEXITCODE_SUCCESS; +} + + +/** + * Command table entry for 'play'. + */ +const VKATCMD g_CmdPlay = +{ + "play", + audioTestCmdPlayHandler, + "Plays one or more wave files.", + g_aCmdPlayOptions, + RT_ELEMENTS(g_aCmdPlayOptions), + audioTestCmdPlayHelp, + false /* fNeedsTransport */ +}; + + +/********************************************************************************************************************************* +* Command: rec * +*********************************************************************************************************************************/ + +/** + * Worker for audioTestRecOne implementing the recording loop. + */ +static RTEXITCODE audioTestRecOneInner(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTWAVEFILE pWaveFile, + PCPDMAUDIOSTREAMCFG pCfgAcq, uint64_t cMaxFrames, const char *pszFile) +{ + int rc; + uint64_t const nsStarted = RTTimeNanoTS(); + + /* + * Transfer data as quickly as we're allowed. + */ + uint8_t abSamples[16384]; + uint32_t const cbSamplesAligned = PDMAudioPropsFloorBytesToFrame(pMix->pProps, sizeof(abSamples)); + uint64_t cFramesCapturedTotal = 0; + while (!g_fTerminate && cFramesCapturedTotal < cMaxFrames) + { + /* + * Anything we can read? + */ + uint32_t const cbCanRead = AudioTestMixStreamGetReadable(pMix); + if (cbCanRead) + { + uint32_t const cbToRead = RT_MIN(cbCanRead, cbSamplesAligned); + uint32_t cbCaptured = 0; + rc = AudioTestMixStreamCapture(pMix, abSamples, cbToRead, &cbCaptured); + if (RT_SUCCESS(rc)) + { + if (cbCaptured) + { + uint32_t cFramesCaptured = PDMAudioPropsBytesToFrames(pMix->pProps, cbCaptured); + if (cFramesCaptured + cFramesCaptured < cMaxFrames) + { /* likely */ } + else + { + cFramesCaptured = cMaxFrames - cFramesCaptured; + cbCaptured = PDMAudioPropsFramesToBytes(pMix->pProps, cFramesCaptured); + } + + rc = AudioTestWaveFileWrite(pWaveFile, abSamples, cbCaptured); + if (RT_SUCCESS(rc)) + cFramesCapturedTotal += cFramesCaptured; + else + return RTMsgErrorExitFailure("Error writing to '%s': %Rrc", pszFile, rc); + } + else + return RTMsgErrorExitFailure("Captured zero bytes - %#x bytes reported readable!\n", cbCanRead); + } + else + return RTMsgErrorExitFailure("Failed to capture %#x bytes: %Rrc (%#x available)\n", cbToRead, rc, cbCanRead); + } + else if (AudioTestMixStreamIsOkay(pMix)) + RTThreadSleep(RT_MIN(RT_MAX(1, pCfgAcq->Device.cMsSchedulingHint), 256)); + else + return RTMsgErrorExitFailure("Stream is not okay!\n"); + } + + /* + * Disable the stream. + */ + rc = AudioTestMixStreamDisable(pMix); + if (RT_SUCCESS(rc) && g_uVerbosity > 0) + RTMsgInfo("%'RU64 ns: Stopped after recording %RU64 frames%s\n", RTTimeNanoTS() - nsStarted, cFramesCapturedTotal, + g_fTerminate ? " - Ctrl-C" : "."); + else if (RT_FAILURE(rc)) + return RTMsgErrorExitFailure("Disabling stream failed: %Rrc", rc); + + return RTEXITCODE_SUCCESS; +} + + +/** + * Worker for audioTestCmdRecHandler that recs one file. + */ +static RTEXITCODE audioTestRecOne(const char *pszFile, uint8_t cWaveChannels, uint8_t cbWaveSample, uint32_t uWaveHz, + PCPDMDRVREG pDrvReg, const char *pszDevId, PAUDIOTESTIOOPTS pIoOpts, + uint64_t cMaxFrames, uint64_t cNsMaxDuration) +{ + /* + * Construct the driver stack. + */ + RTEXITCODE rcExit = RTEXITCODE_FAILURE; + AUDIOTESTDRVSTACK DrvStack; + int rc = audioTestDriverStackInit(&DrvStack, pDrvReg, pIoOpts->fWithDrvAudio); + if (RT_SUCCESS(rc)) + { + /* + * Set the input device if one is specified. + */ + rc = audioTestDriverStackSetDevice(&DrvStack, PDMAUDIODIR_IN, pszDevId); + if (RT_SUCCESS(rc)) + { + /* + * Create an input stream. + */ + PDMAUDIOPCMPROPS ReqProps; + PDMAudioPropsInit(&ReqProps, + pIoOpts->Props.cbSampleX ? pIoOpts->Props.cbSampleX : cbWaveSample ? cbWaveSample : 2, + pIoOpts->Props.fSigned, + pIoOpts->Props.cChannelsX ? pIoOpts->Props.cChannelsX : cWaveChannels ? cWaveChannels : 2, + pIoOpts->Props.uHz ? pIoOpts->Props.uHz : uWaveHz ? uWaveHz : 44100); + + PDMAUDIOSTREAMCFG CfgAcq; + PPDMAUDIOSTREAM pStream = NULL; + rc = audioTestDriverStackStreamCreateInput(&DrvStack, &ReqProps, pIoOpts->cMsBufferSize, + pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream, &CfgAcq); + if (RT_SUCCESS(rc)) + { + /* + * Determine the wave file properties. If it differs from the stream + * properties, make sure the mixer is enabled. + */ + PDMAUDIOPCMPROPS WaveProps; + PDMAudioPropsInit(&WaveProps, + cbWaveSample ? cbWaveSample : PDMAudioPropsSampleSize(&CfgAcq.Props), + true /*fSigned*/, + cWaveChannels ? cWaveChannels : PDMAudioPropsChannels(&CfgAcq.Props), + uWaveHz ? uWaveHz : PDMAudioPropsHz(&CfgAcq.Props)); + if (!pIoOpts->fWithMixer && !PDMAudioPropsAreEqual(&WaveProps, &CfgAcq.Props)) + { + RTMsgInfo("Enabling the mixer buffer.\n"); + pIoOpts->fWithMixer = true; + } + + /* Console the max duration into frames now that we've got the wave file format. */ + if (cMaxFrames != UINT64_MAX && cNsMaxDuration != UINT64_MAX) + { + uint64_t cMaxFrames2 = PDMAudioPropsNanoToBytes64(&WaveProps, cNsMaxDuration); + cMaxFrames = RT_MAX(cMaxFrames, cMaxFrames2); + } + else if (cNsMaxDuration != UINT64_MAX) + cMaxFrames = PDMAudioPropsNanoToBytes64(&WaveProps, cNsMaxDuration); + + /* + * Create a mixer wrapper. This is just a thin wrapper if fWithMixer + * is false, otherwise it's doing mixing, resampling and recoding. + */ + AUDIOTESTDRVMIXSTREAM Mix; + rc = AudioTestMixStreamInit(&Mix, &DrvStack, pStream, pIoOpts->fWithMixer ? &WaveProps : NULL, 100 /*ms*/); + if (RT_SUCCESS(rc)) + { + char szTmp[128]; + if (g_uVerbosity > 0) + RTMsgInfo("Stream: %s cbBackend=%#RX32%s\n", + PDMAudioPropsToString(&pStream->Cfg.Props, szTmp, sizeof(szTmp)), + pStream->cbBackend, pIoOpts->fWithMixer ? " mixed" : ""); + + /* + * Open the wave output file. + */ + AUDIOTESTWAVEFILE WaveFile; + RTERRINFOSTATIC ErrInfo; + rc = AudioTestWaveFileCreate(pszFile, &WaveProps, &WaveFile, RTErrInfoInitStatic(&ErrInfo)); + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + { + RTMsgInfo("Opened '%s' for playing\n", pszFile); + RTMsgInfo("Format: %s\n", PDMAudioPropsToString(&WaveFile.Props, szTmp, sizeof(szTmp))); + } + + /* + * Enable the stream and start recording. + */ + rc = AudioTestMixStreamEnable(&Mix); + if (RT_SUCCESS(rc)) + rcExit = audioTestRecOneInner(&Mix, &WaveFile, &CfgAcq, cMaxFrames, pszFile); + else + rcExit = RTMsgErrorExitFailure("Enabling the input stream failed: %Rrc", rc); + if (rcExit != RTEXITCODE_SUCCESS) + AudioTestMixStreamDisable(&Mix); + + /* + * Clean up. + */ + rc = AudioTestWaveFileClose(&WaveFile); + if (RT_FAILURE(rc)) + rcExit = RTMsgErrorExitFailure("Error closing '%s': %Rrc", pszFile, rc); + } + else + rcExit = RTMsgErrorExitFailure("Failed to open '%s': %Rrc%#RTeim", pszFile, rc, &ErrInfo.Core.pszMsg); + + AudioTestMixStreamTerm(&Mix); + } + audioTestDriverStackStreamDestroy(&DrvStack, pStream); + pStream = NULL; + } + else + rcExit = RTMsgErrorExitFailure("Creating output stream failed: %Rrc", rc); + } + else + rcExit = RTMsgErrorExitFailure("Failed to set output device to '%s': %Rrc", pszDevId, rc); + audioTestDriverStackDelete(&DrvStack); + } + else + rcExit = RTMsgErrorExitFailure("Driver stack construction failed: %Rrc", rc); + return rcExit; +} + + +/** + * Options for 'rec'. + */ +static const RTGETOPTDEF g_aCmdRecOptions[] = +{ + { "--backend", 'b', RTGETOPT_REQ_STRING }, + { "--channels", 'c', RTGETOPT_REQ_UINT8 }, + { "--hz", 'f', RTGETOPT_REQ_UINT32 }, + { "--frequency", 'f', RTGETOPT_REQ_UINT32 }, + { "--sample-size", 'z', RTGETOPT_REQ_UINT8 }, + { "--input-device", 'i', RTGETOPT_REQ_STRING }, + { "--wav-channels", 'C', RTGETOPT_REQ_UINT8 }, + { "--wav-hz", 'F', RTGETOPT_REQ_UINT32 }, + { "--wav-frequency", 'F', RTGETOPT_REQ_UINT32 }, + { "--wav-sample-size", 'Z', RTGETOPT_REQ_UINT8 }, + { "--with-drv-audio", 'd', RTGETOPT_REQ_NOTHING }, + { "--with-mixer", 'm', RTGETOPT_REQ_NOTHING }, + { "--max-frames", 'r', RTGETOPT_REQ_UINT64 }, + { "--max-sec", 's', RTGETOPT_REQ_UINT64 }, + { "--max-seconds", 's', RTGETOPT_REQ_UINT64 }, + { "--max-ms", 't', RTGETOPT_REQ_UINT64 }, + { "--max-milliseconds", 't', RTGETOPT_REQ_UINT64 }, + { "--max-ns", 'T', RTGETOPT_REQ_UINT64 }, + { "--max-nanoseconds", 'T', RTGETOPT_REQ_UINT64 }, +}; + + +/** The 'rec' command option help. */ +static DECLCALLBACK(const char *) audioTestCmdRecHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case 'b': return "The audio backend to use."; + case 'c': return "Number of backend input channels"; + case 'C': return "Number of wave-file channels"; + case 'd': return "Go via DrvAudio instead of directly interfacing with the backend."; + case 'f': return "Input frequency (Hz)"; + case 'F': return "Wave-file frequency (Hz)"; + case 'z': return "Input sample size (bits)"; + case 'Z': return "Wave-file sample size (bits)"; + case 'm': return "Go via the mixer."; + case 'i': return "The ID of the input device to use."; + case 'r': return "Max recording duration in frames."; + case 's': return "Max recording duration in seconds."; + case 't': return "Max recording duration in milliseconds."; + case 'T': return "Max recording duration in nanoseconds."; + default: return NULL; + } +} + + +/** + * The 'rec' command handler. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioTestCmdRecHandler(PRTGETOPTSTATE pGetState) +{ + /* Option values: */ + PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend(); + const char *pszDevId = NULL; + uint8_t cbSample = 0; + uint8_t cChannels = 0; + uint32_t uHz = 0; + uint8_t cbWaveSample = 0; + uint8_t cWaveChannels = 0; + uint32_t uWaveHz = 0; + uint64_t cMaxFrames = UINT64_MAX; + uint64_t cNsMaxDuration = UINT64_MAX; + + AUDIOTESTIOOPTS IoOpts; + audioTestIoOptsInitDefaults(&IoOpts); + + /* Argument processing loop: */ + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion)) != 0) + { + switch (ch) + { + case 'b': + pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz); + if (pDrvReg == NULL) + return RTEXITCODE_SYNTAX; + break; + + case 'c': + cChannels = ValueUnion.u8; + break; + + case 'C': + cWaveChannels = ValueUnion.u8; + break; + + case 'd': + IoOpts.fWithDrvAudio = true; + break; + + case 'f': + uHz = ValueUnion.u32; + break; + + case 'F': + uWaveHz = ValueUnion.u32; + break; + + case 'i': + pszDevId = ValueUnion.psz; + break; + + case 'm': + IoOpts.fWithMixer = true; + break; + + case 'r': + cMaxFrames = ValueUnion.u64; + break; + + case 's': + cNsMaxDuration = ValueUnion.u64 >= UINT64_MAX / RT_NS_1SEC ? UINT64_MAX : ValueUnion.u64 * RT_NS_1SEC; + break; + + case 't': + cNsMaxDuration = ValueUnion.u64 >= UINT64_MAX / RT_NS_1MS ? UINT64_MAX : ValueUnion.u64 * RT_NS_1MS; + break; + + case 'T': + cNsMaxDuration = ValueUnion.u64; + break; + + case 'z': + cbSample = ValueUnion.u8 / 8; + break; + + case 'Z': + cbWaveSample = ValueUnion.u8 / 8; + break; + + case VINF_GETOPT_NOT_OPTION: + { + if ( cbSample + || cChannels + || uHz) + { + /* Set new (override standard) I/O PCM properties if set by the user. */ + PDMAudioPropsInit(&IoOpts.Props, + cbSample ? cbSample : 2 /* 16-bit */, true /* fSigned */, + cChannels ? cChannels : 2 /* Stereo */, uHz ? uHz : 44100); + } + + RTEXITCODE rcExit = audioTestRecOne(ValueUnion.psz, cWaveChannels, cbWaveSample, uWaveHz, + pDrvReg, pszDevId, &IoOpts, + cMaxFrames, cNsMaxDuration); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + break; + } + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdRec); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + return RTEXITCODE_SUCCESS; +} + + +/** + * Command table entry for 'rec'. + */ +const VKATCMD g_CmdRec = +{ + "rec", + audioTestCmdRecHandler, + "Records audio to a wave file.", + g_aCmdRecOptions, + RT_ELEMENTS(g_aCmdRecOptions), + audioTestCmdRecHelp, + false /* fNeedsTransport */ +}; + diff --git a/src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp b/src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp new file mode 100644 index 00000000..3259398d --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp @@ -0,0 +1,481 @@ +/* $Id: vkatCmdSelfTest.cpp $ */ +/** @file + * Validation Kit Audio Test (VKAT) - Self test. + * + * Self-test which does a complete audio testing framework run without the need + * of a VM or other infrastructure, i.e. all required parts are running locally + * on the same machine. + * + * This self-test does the following: + * - 1. Creates a separate thread for the guest side VKAT and connects to the ATS instance on + * the host side at port 6052 (ATS_TCP_DEF_BIND_PORT_HOST). + * - 2. Uses the Validation Kit audio backend, which in turn creates an ATS instance + * listening at port 6062 (ATS_TCP_DEF_BIND_PORT_VALKIT). + * - 3. Uses the host test environment which creates an ATS instance + * listening at port 6052 (ATS_TCP_DEF_BIND_PORT_HOST). + * - 4. Executes a complete test run locally (e.g. without any guest (VM) involved). + */ + +/* + * Copyright (C) 2021-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ + +#include <iprt/ctype.h> +#include <iprt/errcore.h> +#include <iprt/getopt.h> +#include <iprt/message.h> +#include <iprt/rand.h> +#include <iprt/test.h> + +#include "Audio/AudioHlp.h" +#include "Audio/AudioTest.h" +#include "Audio/AudioTestService.h" +#include "Audio/AudioTestServiceClient.h" + +#include "vkatInternal.h" + + +/********************************************************************************************************************************* +* Internal structures * +*********************************************************************************************************************************/ + +/** + * Structure for keeping a VKAT self test context. + */ +typedef struct SELFTESTCTX +{ + /** Common tag for guest and host side. */ + char szTag[AUDIOTEST_TAG_MAX]; + /** The driver stack in use. */ + AUDIOTESTDRVSTACK DrvStack; + /** Audio driver to use. + * Defaults to the platform's default driver. */ + PCPDMDRVREG pDrvReg; + struct + { + AUDIOTESTENV TstEnv; + /** Where to bind the address of the guest ATS instance to. + * Defaults to localhost (127.0.0.1) if empty. */ + char szAtsAddr[64]; + /** Port of the guest ATS instance. + * Defaults to ATS_ALT_PORT if not set. */ + uint32_t uAtsPort; + } Guest; + struct + { + AUDIOTESTENV TstEnv; + /** Address of the guest ATS instance. + * Defaults to localhost (127.0.0.1) if not set. */ + char szGuestAtsAddr[64]; + /** Port of the guest ATS instance. + * Defaults to ATS_DEFAULT_PORT if not set. */ + uint32_t uGuestAtsPort; + /** Address of the Validation Kit audio driver ATS instance. + * Defaults to localhost (127.0.0.1) if not set. */ + char szValKitAtsAddr[64]; + /** Port of the Validation Kit audio driver ATS instance. + * Defaults to ATS_ALT_PORT if not set. */ + uint32_t uValKitAtsPort; + } Host; +} SELFTESTCTX; +/** Pointer to a VKAT self test context. */ +typedef SELFTESTCTX *PSELFTESTCTX; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +/** The global self-text context. */ +static SELFTESTCTX g_Ctx; + + +/********************************************************************************************************************************* +* Driver stack self-test implementation * +*********************************************************************************************************************************/ + +/** + * Performs a (quick) audio driver stack self test. + * + * Local only, no guest/host communication involved. + * + * @returns VBox status code. + */ +int AudioTestDriverStackPerformSelftest(void) +{ + PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend(); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Testing driver stack started\n"); + + AUDIOTESTDRVSTACK DrvStack; + int rc = audioTestDriverStackProbe(&DrvStack, pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, false /* fWithDrvAudio */); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc); + + AUDIOTESTIOOPTS IoOpts; + audioTestIoOptsInitDefaults(&IoOpts); + + PPDMAUDIOSTREAM pStream; + PDMAUDIOSTREAMCFG CfgAcq; + rc = audioTestDriverStackStreamCreateOutput(&DrvStack, &IoOpts.Props, + IoOpts.cMsBufferSize, IoOpts.cMsPreBuffer, IoOpts.cMsSchedulingHint, + &pStream, &CfgAcq); + AssertRCReturn(rc, rc); + + rc = audioTestDriverStackStreamEnable(&DrvStack, pStream); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc); + + RTTEST_CHECK_RET(g_hTest, audioTestDriverStackStreamIsOkay(&DrvStack, pStream), VERR_AUDIO_STREAM_NOT_READY); + + uint8_t abBuf[_4K]; + memset(abBuf, 0x42, sizeof(abBuf)); + + uint32_t cbWritten; + rc = audioTestDriverStackStreamPlay(&DrvStack, pStream, abBuf, sizeof(abBuf), &cbWritten); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc); + RTTEST_CHECK_RET(g_hTest, cbWritten == sizeof(abBuf), VERR_AUDIO_STREAM_NOT_READY); + + audioTestDriverStackStreamDrain(&DrvStack, pStream, true /* fSync */); + audioTestDriverStackStreamDestroy(&DrvStack, pStream); + + audioTestDriverStackDelete(&DrvStack); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Testing driver stack ended with %Rrc\n", rc); + return rc; +} + + +/********************************************************************************************************************************* +* Self-test implementation * +*********************************************************************************************************************************/ + +/** + * Thread callback for mocking the guest (VM) side of things. + * + * @returns VBox status code. + * @param hThread Thread handle. + * @param pvUser Pointer to user-supplied data. + */ +static DECLCALLBACK(int) audioTestSelftestGuestAtsThread(RTTHREAD hThread, void *pvUser) +{ + RT_NOREF(hThread); + PSELFTESTCTX pCtx = (PSELFTESTCTX)pvUser; + + PAUDIOTESTENV pTstEnvGst = &pCtx->Guest.TstEnv; + + audioTestEnvInit(pTstEnvGst); + + /* Flag the environment for self test mode. */ + pTstEnvGst->fSelftest = true; + + /* Tweak the address the guest ATS is trying to connect to the host if anything else is specified. + * Note: The host also runs on the same host (this self-test is completely self-contained and does not need a VM). */ + if (!pTstEnvGst->TcpOpts.szConnectAddr[0]) + RTStrCopy(pTstEnvGst->TcpOpts.szConnectAddr, sizeof(pTstEnvGst->TcpOpts.szConnectAddr), "127.0.0.1"); + + /* Generate tag for guest side. */ + int rc = RTStrCopy(pTstEnvGst->szTag, sizeof(pTstEnvGst->szTag), pCtx->szTag); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc); + + rc = AudioTestPathCreateTemp(pTstEnvGst->szPathTemp, sizeof(pTstEnvGst->szPathTemp), "selftest-guest"); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc); + + rc = AudioTestPathCreateTemp(pTstEnvGst->szPathOut, sizeof(pTstEnvGst->szPathOut), "selftest-out"); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc); + + pTstEnvGst->enmMode = AUDIOTESTMODE_GUEST; + + rc = audioTestEnvCreate(pTstEnvGst, &pCtx->DrvStack); + if (RT_SUCCESS(rc)) + { + RTThreadUserSignal(hThread); + + rc = audioTestWorker(pTstEnvGst); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc); + + audioTestEnvDestroy(pTstEnvGst); + } + + return rc; +} + +/** + * Main function for performing the self test. + * + * @returns RTEXITCODE + * @param pCtx Self test context to use. + */ +RTEXITCODE audioTestDoSelftest(PSELFTESTCTX pCtx) +{ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Running self test ...\n"); + + /* Generate a common tag for guest and host side. */ + int rc = AudioTestGenTag(pCtx->szTag, sizeof(pCtx->szTag)); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE); + + PAUDIOTESTENV pTstEnvHst = &pCtx->Host.TstEnv; + + audioTestEnvInit(pTstEnvHst); + + /* Flag the environment for self test mode. */ + pTstEnvHst->fSelftest = true; + + /* One test iteration with a 5s maximum test tone is enough for a (quick) self test. */ + pTstEnvHst->cIterations = 1; + pTstEnvHst->ToneParms.msDuration = RTRandU32Ex(500, RT_MS_5SEC); + + /* Generate tag for host side. */ + rc = RTStrCopy(pTstEnvHst->szTag, sizeof(pTstEnvHst->szTag), pCtx->szTag); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE); + + rc = AudioTestPathCreateTemp(pTstEnvHst->szPathTemp, sizeof(pTstEnvHst->szPathTemp), "selftest-tmp"); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE); + + rc = AudioTestPathCreateTemp(pTstEnvHst->szPathOut, sizeof(pTstEnvHst->szPathOut), "selftest-out"); + RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE); + + /* + * Step 1. + */ + RTTHREAD hThreadGstAts = NIL_RTTHREAD; + + bool const fStartGuestAts = RTStrNLen(pCtx->Host.szGuestAtsAddr, sizeof(pCtx->Host.szGuestAtsAddr)) == 0; + if (fStartGuestAts) + { + /* Step 1b. */ + rc = RTThreadCreate(&hThreadGstAts, audioTestSelftestGuestAtsThread, pCtx, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, + "VKATGstAts"); + if (RT_SUCCESS(rc)) + rc = RTThreadUserWait(hThreadGstAts, RT_MS_30SEC); + } + + RTThreadSleep(2000); /* Fudge: Wait until guest ATS is up. 2 seconds should be enough (tm). */ + + if (RT_SUCCESS(rc)) + { + /* + * Steps 2 + 3. + */ + pTstEnvHst->enmMode = AUDIOTESTMODE_HOST; + + rc = audioTestEnvCreate(pTstEnvHst, &pCtx->DrvStack); + if (RT_SUCCESS(rc)) + { + /* + * Step 4. + */ + rc = audioTestWorker(pTstEnvHst); + RTTEST_CHECK_RC_OK(g_hTest, rc); + + audioTestEnvDestroy(pTstEnvHst); + } + } + + /* + * Shutting down. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Shutting down self test\n"); + + /* If we started the guest ATS ourselves, wait for it to terminate properly. */ + if (fStartGuestAts) + { + int rcThread; + int rc2 = RTThreadWait(hThreadGstAts, RT_MS_30SEC, &rcThread); + if (RT_SUCCESS(rc2)) + rc2 = rcThread; + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Shutting down guest ATS failed with %Rrc\n", rc2); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Self test failed with %Rrc\n", rc); + + return RT_SUCCESS(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; +} + + +/********************************************************************************************************************************* +* Command: selftest * +*********************************************************************************************************************************/ + +/** + * Command line parameters for self-test mode. + */ +static const RTGETOPTDEF s_aCmdSelftestOptions[] = +{ + { "--exclude-all", 'a', RTGETOPT_REQ_NOTHING }, + { "--backend", 'b', RTGETOPT_REQ_STRING }, + { "--with-drv-audio", 'd', RTGETOPT_REQ_NOTHING }, + { "--with-mixer", 'm', RTGETOPT_REQ_NOTHING }, + { "--exclude", 'e', RTGETOPT_REQ_UINT32 }, + { "--include", 'i', RTGETOPT_REQ_UINT32 } +}; + +/** the 'selftest' command option help. */ +static DECLCALLBACK(const char *) audioTestCmdSelftestHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case 'a': return "Exclude all tests from the list (useful to enable single tests later with --include)"; + case 'b': return "The audio backend to use"; + case 'd': return "Go via DrvAudio instead of directly interfacing with the backend"; + case 'e': return "Exclude the given test id from the list"; + case 'i': return "Include the given test id in the list"; + case 'm': return "Use the internal mixing engine explicitly"; + default: return NULL; + } +} + +/** + * The 'selftest' command handler. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +DECLCALLBACK(RTEXITCODE) audioTestCmdSelftestHandler(PRTGETOPTSTATE pGetState) +{ + RT_ZERO(g_Ctx); + + audioTestEnvInit(&g_Ctx.Guest.TstEnv); + audioTestEnvInit(&g_Ctx.Host.TstEnv); + + AUDIOTESTIOOPTS IoOpts; + audioTestIoOptsInitDefaults(&IoOpts); + + /* Argument processing loop: */ + int rc; + RTGETOPTUNION ValueUnion; + while ((rc = RTGetOpt(pGetState, &ValueUnion)) != 0) + { + switch (rc) + { + case 'a': + for (unsigned i = 0; i < g_cTests; i++) + g_aTests[i].fExcluded = true; + break; + + case 'b': + g_Ctx.pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz); + if (g_Ctx.pDrvReg == NULL) + return RTEXITCODE_SYNTAX; + break; + + case 'd': + IoOpts.fWithDrvAudio = true; + break; + + case 'e': + if (ValueUnion.u32 >= g_cTests) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --exclude", ValueUnion.u32); + g_aTests[ValueUnion.u32].fExcluded = true; + break; + + case 'i': + if (ValueUnion.u32 >= g_cTests) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --include", ValueUnion.u32); + g_aTests[ValueUnion.u32].fExcluded = false; + break; + + case 'm': + IoOpts.fWithMixer = true; + break; + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdSelfTest); + + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + /* For simplicity both test environments, guest and host, will have the same I/O options. + ** @todo Make this indepedent by a prefix, "--[guest|host]-<option>" -> e.g. "--guest-with-drv-audio". */ + memcpy(&g_Ctx.Guest.TstEnv.IoOpts, &IoOpts, sizeof(AUDIOTESTIOOPTS)); + memcpy(&g_Ctx.Host.TstEnv.IoOpts, &IoOpts, sizeof(AUDIOTESTIOOPTS)); + + rc = AudioTestDriverStackPerformSelftest(); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Testing driver stack failed: %Rrc\n", rc); + + /* Go with the Validation Kit audio backend if nothing else is specified. */ + if (g_Ctx.pDrvReg == NULL) + g_Ctx.pDrvReg = AudioTestFindBackendOpt("valkit"); + + /* + * In self-test mode the guest and the host side have to share the same driver stack, + * as we don't have any device emulation between the two sides. + * + * This is necessary to actually get the played/recorded audio to from/to the guest + * and host respectively. + * + * Choosing any other backend than the Validation Kit above *will* break this self-test! + */ + rc = audioTestDriverStackInitEx(&g_Ctx.DrvStack, g_Ctx.pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, g_Ctx.Host.TstEnv.IoOpts.fWithDrvAudio); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unable to init driver stack: %Rrc\n", rc); + + /* + * Start testing. + */ + RTTestBanner(g_hTest); + + int rc2 = audioTestDoSelftest(&g_Ctx); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Self test failed with rc=%Rrc", rc2); + + audioTestDriverStackDelete(&g_Ctx.DrvStack); + + /* + * Print summary and exit. + */ + return RTTestSummaryAndDestroy(g_hTest); +} + +/** + * Command table entry for 'selftest'. + */ +const VKATCMD g_CmdSelfTest = +{ + "selftest", + audioTestCmdSelftestHandler, + "Performs self-tests.", + s_aCmdSelftestOptions, + RT_ELEMENTS(s_aCmdSelftestOptions), + audioTestCmdSelftestHelp, + true /* fNeedsTransport */ +}; + diff --git a/src/VBox/ValidationKit/utils/audio/vkatCommon.cpp b/src/VBox/ValidationKit/utils/audio/vkatCommon.cpp new file mode 100644 index 00000000..12ea83be --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/vkatCommon.cpp @@ -0,0 +1,1760 @@ +/* $Id: vkatCommon.cpp $ */ +/** @file + * Validation Kit Audio Test (VKAT) - Self test code. + */ + +/* + * Copyright (C) 2021-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_TEST +#include <iprt/log.h> + +#ifdef VBOX_WITH_AUDIO_ALSA +# include "DrvHostAudioAlsaStubsMangling.h" +# include <alsa/asoundlib.h> +# include <alsa/control.h> /* For device enumeration. */ +# include <alsa/version.h> +# include "DrvHostAudioAlsaStubs.h" +#endif +#ifdef VBOX_WITH_AUDIO_OSS +# include <errno.h> +# include <fcntl.h> +# include <sys/ioctl.h> +# include <sys/mman.h> +# include <sys/soundcard.h> +# include <unistd.h> +#endif +#ifdef RT_OS_WINDOWS +# include <iprt/win/windows.h> +# include <iprt/win/audioclient.h> +# include <endpointvolume.h> /* For IAudioEndpointVolume. */ +# include <audiopolicy.h> /* For IAudioSessionManager. */ +# include <AudioSessionTypes.h> +# include <Mmdeviceapi.h> +#endif + +#include <iprt/ctype.h> +#include <iprt/dir.h> +#include <iprt/errcore.h> +#include <iprt/getopt.h> +#include <iprt/message.h> +#include <iprt/rand.h> +#include <iprt/test.h> + +#include "Audio/AudioHlp.h" +#include "Audio/AudioTest.h" +#include "Audio/AudioTestService.h" +#include "Audio/AudioTestServiceClient.h" + +#include "vkatInternal.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static int audioTestStreamInit(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream, PDMAUDIODIR enmDir, PAUDIOTESTIOOPTS pPlayOpt); +static int audioTestStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream); + + +/********************************************************************************************************************************* +* Volume handling. * +*********************************************************************************************************************************/ + +#ifdef VBOX_WITH_AUDIO_ALSA +/** + * Sets the system's master volume via ALSA, if available. + * + * @returns VBox status code. + * @param uVolPercent Volume (in percent) to set. + */ +static int audioTestSetMasterVolumeALSA(unsigned uVolPercent) +{ + int rc = audioLoadAlsaLib(); + if (RT_FAILURE(rc)) + return rc; + + int err; + snd_mixer_t *handle; + +# define ALSA_CHECK_RET(a_Exp, a_Text) \ + if (!(a_Exp)) \ + { \ + AssertLogRelMsg(a_Exp, a_Text); \ + if (handle) \ + snd_mixer_close(handle); \ + return VERR_GENERAL_FAILURE; \ + } + +# define ALSA_CHECK_ERR_RET(a_Text) \ + ALSA_CHECK_RET(err >= 0, a_Text) + + err = snd_mixer_open(&handle, 0 /* Index */); + ALSA_CHECK_ERR_RET(("ALSA: Failed to open mixer: %s\n", snd_strerror(err))); + err = snd_mixer_attach(handle, "default"); + ALSA_CHECK_ERR_RET(("ALSA: Failed to attach to default sink: %s\n", snd_strerror(err))); + err = snd_mixer_selem_register(handle, NULL, NULL); + ALSA_CHECK_ERR_RET(("ALSA: Failed to attach to default sink: %s\n", snd_strerror(err))); + err = snd_mixer_load(handle); + ALSA_CHECK_ERR_RET(("ALSA: Failed to load mixer: %s\n", snd_strerror(err))); + + snd_mixer_selem_id_t *sid = NULL; + snd_mixer_selem_id_alloca(&sid); + + snd_mixer_selem_id_set_index(sid, 0 /* Index */); + snd_mixer_selem_id_set_name(sid, "Master"); + + snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid); + ALSA_CHECK_RET(elem != NULL, ("ALSA: Failed to find mixer element: %s\n", snd_strerror(err))); + + long uVolMin, uVolMax; + snd_mixer_selem_get_playback_volume_range(elem, &uVolMin, &uVolMax); + ALSA_CHECK_ERR_RET(("ALSA: Failed to get playback volume range: %s\n", snd_strerror(err))); + + long const uVol = RT_MIN(uVolPercent, 100) * uVolMax / 100; + + err = snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, uVol); + ALSA_CHECK_ERR_RET(("ALSA: Failed to set playback volume left: %s\n", snd_strerror(err))); + err = snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, uVol); + ALSA_CHECK_ERR_RET(("ALSA: Failed to set playback volume right: %s\n", snd_strerror(err))); + + snd_mixer_close(handle); + + return VINF_SUCCESS; + +# undef ALSA_CHECK_RET +# undef ALSA_CHECK_ERR_RET +} +#endif /* VBOX_WITH_AUDIO_ALSA */ + +#ifdef VBOX_WITH_AUDIO_OSS +/** + * Sets the system's master volume via OSS, if available. + * + * @returns VBox status code. + * @param uVolPercent Volume (in percent) to set. + */ +static int audioTestSetMasterVolumeOSS(unsigned uVolPercent) +{ + int hFile = open("/dev/dsp", O_WRONLY | O_NONBLOCK, 0); + if (hFile == -1) + { + /* Try opening the mixing device instead. */ + hFile = open("/dev/mixer", O_RDONLY | O_NONBLOCK, 0); + } + + if (hFile != -1) + { + /* OSS maps 0 (muted) - 100 (max), so just use uVolPercent unmodified here. */ + uint16_t uVol = RT_MAKE_U16(uVolPercent, uVolPercent); + AssertLogRelMsgReturnStmt(ioctl(hFile, SOUND_MIXER_PCM /* SNDCTL_DSP_SETPLAYVOL */, &uVol) >= 0, + ("OSS: Failed to set DSP playback volume: %s (%d)\n", + strerror(errno), errno), close(hFile), RTErrConvertFromErrno(errno)); + return VINF_SUCCESS; + } + + return VERR_NOT_SUPPORTED; +} +#endif /* VBOX_WITH_AUDIO_OSS */ + +#ifdef RT_OS_WINDOWS +static int audioTestSetMasterVolumeWASAPI(unsigned uVolPercent) +{ + HRESULT hr; + +# define WASAPI_CHECK_HR_RET(a_Text) \ + if (FAILED(hr)) \ + { \ + AssertLogRelMsgFailed(a_Text); \ + return VERR_GENERAL_FAILURE; \ + } + + hr = CoInitialize(NULL); + WASAPI_CHECK_HR_RET(("CoInitialize() failed, hr=%Rhrc", hr)); + IMMDeviceEnumerator* pIEnumerator = NULL; + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void **)&pIEnumerator); + WASAPI_CHECK_HR_RET(("WASAPI: Unable to create IMMDeviceEnumerator, hr=%Rhrc", hr)); + + IMMDevice *pIMMDevice = NULL; + hr = pIEnumerator->GetDefaultAudioEndpoint(EDataFlow::eRender, ERole::eConsole, &pIMMDevice); + WASAPI_CHECK_HR_RET(("WASAPI: Unable to get audio endpoint, hr=%Rhrc", hr)); + pIEnumerator->Release(); + + IAudioEndpointVolume *pIAudioEndpointVolume = NULL; + hr = pIMMDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL, (void **)&pIAudioEndpointVolume); + WASAPI_CHECK_HR_RET(("WASAPI: Unable to activate audio endpoint volume, hr=%Rhrc", hr)); + pIMMDevice->Release(); + + float dbMin, dbMax, dbInc; + hr = pIAudioEndpointVolume->GetVolumeRange(&dbMin, &dbMax, &dbInc); + WASAPI_CHECK_HR_RET(("WASAPI: Unable to get volume range, hr=%Rhrc", hr)); + + float const dbSteps = (dbMax - dbMin) / dbInc; + float const dbStepsPerPercent = (dbSteps * dbInc) / 100; + float const dbVol = dbMin + (dbStepsPerPercent * (float(RT_MIN(uVolPercent, 100.0)))); + + hr = pIAudioEndpointVolume->SetMasterVolumeLevel(dbVol, NULL); + WASAPI_CHECK_HR_RET(("WASAPI: Unable to set master volume level, hr=%Rhrc", hr)); + pIAudioEndpointVolume->Release(); + + return VINF_SUCCESS; + +# undef WASAPI_CHECK_HR_RET +} +#endif /* RT_OS_WINDOWS */ + +/** + * Sets the system's master volume, if available. + * + * @returns VBox status code. VERR_NOT_SUPPORTED if not supported. + * @param uVolPercent Volume (in percent) to set. + */ +int audioTestSetMasterVolume(unsigned uVolPercent) +{ + int rc = VINF_SUCCESS; + +#ifdef VBOX_WITH_AUDIO_ALSA + rc = audioTestSetMasterVolumeALSA(uVolPercent); + if (RT_SUCCESS(rc)) + return rc; + /* else try OSS (if available) below. */ +#endif /* VBOX_WITH_AUDIO_ALSA */ + +#ifdef VBOX_WITH_AUDIO_OSS + rc = audioTestSetMasterVolumeOSS(uVolPercent); + if (RT_SUCCESS(rc)) + return rc; +#endif /* VBOX_WITH_AUDIO_OSS */ + +#ifdef RT_OS_WINDOWS + rc = audioTestSetMasterVolumeWASAPI(uVolPercent); + if (RT_SUCCESS(rc)) + return rc; +#endif + + RT_NOREF(rc, uVolPercent); + /** @todo Port other platforms. */ + return VERR_NOT_SUPPORTED; +} + + +/********************************************************************************************************************************* +* Device enumeration + handling. * +*********************************************************************************************************************************/ + +/** + * Enumerates audio devices and optionally searches for a specific device. + * + * @returns VBox status code. + * @param pDrvStack Driver stack to use for enumeration. + * @param pszDev Device name to search for. Can be NULL if the default device shall be used. + * @param ppDev Where to return the pointer of the device enumeration of \a pTstEnv when a + * specific device was found. + */ +int audioTestDevicesEnumerateAndCheck(PAUDIOTESTDRVSTACK pDrvStack, const char *pszDev, PPDMAUDIOHOSTDEV *ppDev) +{ + RTTestSubF(g_hTest, "Enumerating audio devices and checking for device '%s'", pszDev && *pszDev ? pszDev : "[Default]"); + + if (!pDrvStack->pIHostAudio->pfnGetDevices) + { + RTTestSkipped(g_hTest, "Backend does not support device enumeration, skipping"); + return VINF_NOT_SUPPORTED; + } + + Assert(pszDev == NULL || ppDev); + + if (ppDev) + *ppDev = NULL; + + int rc = pDrvStack->pIHostAudio->pfnGetDevices(pDrvStack->pIHostAudio, &pDrvStack->DevEnum); + if (RT_SUCCESS(rc)) + { + PPDMAUDIOHOSTDEV pDev; + RTListForEach(&pDrvStack->DevEnum.LstDevices, pDev, PDMAUDIOHOSTDEV, ListEntry) + { + char szFlags[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN]; + if (pDev->pszId) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Device '%s' (ID '%s'):\n", pDev->pszName, pDev->pszId); + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Device '%s':\n", pDev->pszName); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Usage = %s\n", PDMAudioDirGetName(pDev->enmUsage)); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Flags = %s\n", PDMAudioHostDevFlagsToString(szFlags, pDev->fFlags)); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Input channels = %RU8\n", pDev->cMaxInputChannels); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Output channels = %RU8\n", pDev->cMaxOutputChannels); + + if ( (pszDev && *pszDev) + && !RTStrCmp(pDev->pszName, pszDev)) + { + *ppDev = pDev; + } + } + } + else + RTTestFailed(g_hTest, "Enumerating audio devices failed with %Rrc", rc); + + if (RT_SUCCESS(rc)) + { + if ( (pszDev && *pszDev) + && *ppDev == NULL) + { + RTTestFailed(g_hTest, "Audio device '%s' not found", pszDev); + rc = VERR_NOT_FOUND; + } + } + + RTTestSubDone(g_hTest); + return rc; +} + +static int audioTestStreamInit(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream, + PDMAUDIODIR enmDir, PAUDIOTESTIOOPTS pIoOpts) +{ + int rc; + + if (enmDir == PDMAUDIODIR_IN) + rc = audioTestDriverStackStreamCreateInput(pDrvStack, &pIoOpts->Props, pIoOpts->cMsBufferSize, + pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream->pStream, &pStream->Cfg); + else if (enmDir == PDMAUDIODIR_OUT) + rc = audioTestDriverStackStreamCreateOutput(pDrvStack, &pIoOpts->Props, pIoOpts->cMsBufferSize, + pIoOpts->cMsPreBuffer, pIoOpts->cMsSchedulingHint, &pStream->pStream, &pStream->Cfg); + else + rc = VERR_NOT_SUPPORTED; + + if (RT_SUCCESS(rc)) + { + if (!pDrvStack->pIAudioConnector) + { + pStream->pBackend = &((PAUDIOTESTDRVSTACKSTREAM)pStream->pStream)->Backend; + } + else + pStream->pBackend = NULL; + + /* + * Automatically enable the mixer if the PCM properties don't match. + */ + if ( !pIoOpts->fWithMixer + && !PDMAudioPropsAreEqual(&pIoOpts->Props, &pStream->Cfg.Props)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enabling stream mixer\n"); + pIoOpts->fWithMixer = true; + } + + rc = AudioTestMixStreamInit(&pStream->Mix, pDrvStack, pStream->pStream, + pIoOpts->fWithMixer ? &pIoOpts->Props : NULL, 100 /* ms */); /** @todo Configure mixer buffer? */ + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Initializing %s stream failed with %Rrc", enmDir == PDMAUDIODIR_IN ? "input" : "output", rc); + + return rc; +} + +/** + * Destroys an audio test stream. + * + * @returns VBox status code. + * @param pDrvStack Driver stack the stream belongs to. + * @param pStream Audio stream to destroy. + */ +static int audioTestStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream) +{ + AssertPtrReturn(pStream, VERR_INVALID_POINTER); + + if (pStream->pStream) + { + /** @todo Anything else to do here, e.g. test if there are left over samples or some such? */ + + audioTestDriverStackStreamDestroy(pDrvStack, pStream->pStream); + pStream->pStream = NULL; + pStream->pBackend = NULL; + } + + AudioTestMixStreamTerm(&pStream->Mix); + + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Test Primitives * +*********************************************************************************************************************************/ + +/** + * Initializes test tone parameters (partly with random values). + + * @param pToneParms Test tone parameters to initialize. + */ +void audioTestToneParmsInit(PAUDIOTESTTONEPARMS pToneParms) +{ + RT_BZERO(pToneParms, sizeof(AUDIOTESTTONEPARMS)); + + /** + * Set default (randomized) test tone parameters if not set explicitly. + */ + pToneParms->dbFreqHz = AudioTestToneGetRandomFreq(); + pToneParms->msDuration = RTRandU32Ex(200, RT_MS_30SEC); + pToneParms->uVolumePercent = 100; /* We always go with maximum volume for now. */ + + PDMAudioPropsInit(&pToneParms->Props, + 2 /* 16-bit */, true /* fPcmSigned */, 2 /* cPcmChannels */, 44100 /* uPcmHz */); +} + +/** + * Initializes I/O options with some sane default values. + * + * @param pIoOpts I/O options to initialize. + */ +void audioTestIoOptsInitDefaults(PAUDIOTESTIOOPTS pIoOpts) +{ + RT_BZERO(pIoOpts, sizeof(AUDIOTESTIOOPTS)); + + /* Initialize the PCM properties to some sane values. */ + PDMAudioPropsInit(&pIoOpts->Props, + 2 /* 16-bit */, true /* fPcmSigned */, 2 /* cPcmChannels */, 44100 /* uPcmHz */); + + pIoOpts->cMsBufferSize = UINT32_MAX; + pIoOpts->cMsPreBuffer = UINT32_MAX; + pIoOpts->cMsSchedulingHint = UINT32_MAX; + pIoOpts->uVolumePercent = 100; /* Use maximum volume by default. */ +} + +#if 0 /* Unused */ +/** + * Returns a random scheduling hint (in ms). + */ +DECLINLINE(uint32_t) audioTestEnvGetRandomSchedulingHint(void) +{ + static const unsigned s_aSchedulingHintsMs[] = + { + 10, + 25, + 50, + 100, + 200, + 250 + }; + + return s_aSchedulingHintsMs[RTRandU32Ex(0, RT_ELEMENTS(s_aSchedulingHintsMs) - 1)]; +} +#endif + +/** + * Plays a test tone on a specific audio test stream. + * + * @returns VBox status code. + * @param pIoOpts I/O options to use. + * @param pTstEnv Test environment to use for running the test. + * Optional and can be NULL (for simple playback only). + * @param pStream Stream to use for playing the tone. + * @param pParms Tone parameters to use. + * + * @note Blocking function. + */ +int audioTestPlayTone(PAUDIOTESTIOOPTS pIoOpts, PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms) +{ + uint32_t const idxTest = (uint8_t)pParms->Hdr.idxTest; + + AUDIOTESTTONE TstTone; + AudioTestToneInit(&TstTone, &pStream->Cfg.Props, pParms->dbFreqHz); + + char const *pcszPathOut = NULL; + if (pTstEnv) + pcszPathOut = pTstEnv->Set.szPathAbs; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing test tone (tone frequency is %RU16Hz, %RU32ms, %RU8%% volume)\n", + idxTest, (uint16_t)pParms->dbFreqHz, pParms->msDuration, pParms->uVolumePercent); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Using %RU32ms stream scheduling hint\n", + idxTest, pStream->Cfg.Device.cMsSchedulingHint); + if (pcszPathOut) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Writing to '%s'\n", idxTest, pcszPathOut); + + int rc; + + /** @todo Use .WAV here? */ + AUDIOTESTOBJ Obj; + RT_ZERO(Obj); /* Shut up MSVC. */ + if (pTstEnv) + { + rc = AudioTestSetObjCreateAndRegister(&pTstEnv->Set, "guest-tone-play.pcm", &Obj); + AssertRCReturn(rc, rc); + } + + uint8_t const uVolPercent = pIoOpts->uVolumePercent; + int rc2 = audioTestSetMasterVolume(uVolPercent); + if (RT_FAILURE(rc2)) + { + if (rc2 == VERR_NOT_SUPPORTED) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Setting system's master volume is not supported on this platform, skipping\n"); + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Setting system's master volume failed with %Rrc\n", rc2); + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Set system's master volume to %RU8%%\n", uVolPercent); + + rc = AudioTestMixStreamEnable(&pStream->Mix); + if ( RT_SUCCESS(rc) + && AudioTestMixStreamIsOkay(&pStream->Mix)) + { + uint32_t cbToWriteTotal = PDMAudioPropsMilliToBytes(&pStream->Cfg.Props, pParms->msDuration); + AssertStmt(cbToWriteTotal, rc = VERR_INVALID_PARAMETER); + uint32_t cbWrittenTotal = 0; + + /* We play a pre + post beacon before + after the actual test tone. + * We always start with the pre beacon. */ + AUDIOTESTTONEBEACON Beacon; + AudioTestBeaconInit(&Beacon, (uint8_t)pParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_PRE, &pStream->Cfg.Props); + + uint32_t const cbBeacon = AudioTestBeaconGetSize(&Beacon); + if (cbBeacon) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing 2 x %RU32 bytes pre/post beacons\n", + idxTest, cbBeacon); + + if (g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing %s beacon ...\n", + idxTest, AudioTestBeaconTypeGetName(Beacon.enmType)); + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing %RU32 bytes total\n", idxTest, cbToWriteTotal); + + AudioTestObjAddMetadataStr(Obj, "test_id=%04RU32\n", pParms->Hdr.idxTest); + AudioTestObjAddMetadataStr(Obj, "beacon_type=%RU32\n", (uint32_t)AudioTestBeaconGetType(&Beacon)); + AudioTestObjAddMetadataStr(Obj, "beacon_pre_bytes=%RU32\n", cbBeacon); + AudioTestObjAddMetadataStr(Obj, "beacon_post_bytes=%RU32\n", cbBeacon); + AudioTestObjAddMetadataStr(Obj, "stream_to_write_total_bytes=%RU32\n", cbToWriteTotal); + AudioTestObjAddMetadataStr(Obj, "stream_period_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesPeriod); + AudioTestObjAddMetadataStr(Obj, "stream_buffer_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesBufferSize); + AudioTestObjAddMetadataStr(Obj, "stream_prebuf_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesPreBuffering); + /* Note: This mostly is provided by backend (e.g. PulseAudio / ALSA / ++) and + * has nothing to do with the device emulation scheduling hint. */ + AudioTestObjAddMetadataStr(Obj, "device_scheduling_hint_ms=%RU32\n", pStream->Cfg.Device.cMsSchedulingHint); + + PAUDIOTESTDRVMIXSTREAM pMix = &pStream->Mix; + + uint32_t const cbPreBuffer = PDMAudioPropsFramesToBytes(pMix->pProps, pStream->Cfg.Backend.cFramesPreBuffering); + uint64_t const nsStarted = RTTimeNanoTS(); + uint64_t nsDonePreBuffering = 0; + + uint64_t offStream = 0; + uint64_t nsTimeout = RT_MS_5MIN_64 * RT_NS_1MS; + uint64_t nsLastMsgCantWrite = 0; /* Timestamp (in ns) when the last message of an unwritable stream was shown. */ + uint64_t nsLastWrite = 0; + + AUDIOTESTSTATE enmState = AUDIOTESTSTATE_PRE; + uint8_t abBuf[_16K]; + + for (;;) + { + uint64_t const nsNow = RTTimeNanoTS(); + if (!nsLastWrite) + nsLastWrite = nsNow; + + /* Pace ourselves a little. */ + if (offStream >= cbPreBuffer) + { + if (!nsDonePreBuffering) + nsDonePreBuffering = nsNow; + uint64_t const cNsWritten = PDMAudioPropsBytesToNano64(pMix->pProps, offStream - cbPreBuffer); + uint64_t const cNsElapsed = nsNow - nsStarted; + if (cNsWritten > cNsElapsed + RT_NS_10MS) + RTThreadSleep((cNsWritten - cNsElapsed - RT_NS_10MS / 2) / RT_NS_1MS); + } + + uint32_t cbWritten = 0; + uint32_t const cbCanWrite = AudioTestMixStreamGetWritable(&pStream->Mix); + if (cbCanWrite) + { + if (g_uVerbosity >= 4) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Stream is writable with %RU64ms (%RU32 bytes)\n", + idxTest, PDMAudioPropsBytesToMilli(pMix->pProps, cbCanWrite), cbCanWrite); + + switch (enmState) + { + case AUDIOTESTSTATE_PRE: + RT_FALL_THROUGH(); + case AUDIOTESTSTATE_POST: + { + if (g_uVerbosity >= 4) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: %RU32 bytes (%RU64ms) beacon data remaining\n", + idxTest, AudioTestBeaconGetRemaining(&Beacon), + PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, AudioTestBeaconGetRemaining(&Beacon))); + + bool fGoToNextStage = false; + + if ( AudioTestBeaconGetSize(&Beacon) + && !AudioTestBeaconIsComplete(&Beacon)) + { + bool const fStarted = AudioTestBeaconGetRemaining(&Beacon) == AudioTestBeaconGetSize(&Beacon); + + uint32_t const cbBeaconRemaining = AudioTestBeaconGetRemaining(&Beacon); + AssertBreakStmt(cbBeaconRemaining, VERR_WRONG_ORDER); + + /* Limit to exactly one beacon (pre or post). */ + uint32_t const cbToWrite = RT_MIN(sizeof(abBuf), RT_MIN(cbCanWrite, cbBeaconRemaining)); + + rc = AudioTestBeaconWrite(&Beacon, abBuf, cbToWrite); + if (RT_SUCCESS(rc)) + { + rc = AudioTestMixStreamPlay(&pStream->Mix, abBuf, cbToWrite, &cbWritten); + if ( RT_SUCCESS(rc) + && pTstEnv) + { + /* Also write the beacon data to the test object. + * Note: We use cbPlayed here instead of cbToPlay to know if the data actually was + * reported as being played by the audio stack. */ + rc = AudioTestObjWrite(Obj, abBuf, cbWritten); + } + } + + if ( fStarted + && g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Writing %s beacon begin\n", + idxTest, AudioTestBeaconTypeGetName(Beacon.enmType)); + if (AudioTestBeaconIsComplete(&Beacon)) + { + if (g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Writing %s beacon end\n", + idxTest, AudioTestBeaconTypeGetName(Beacon.enmType)); + fGoToNextStage = true; + } + } + else + fGoToNextStage = true; + + if (fGoToNextStage) + { + if (enmState == AUDIOTESTSTATE_PRE) + enmState = AUDIOTESTSTATE_RUN; + else if (enmState == AUDIOTESTSTATE_POST) + enmState = AUDIOTESTSTATE_DONE; + } + break; + } + + case AUDIOTESTSTATE_RUN: + { + uint32_t cbToWrite = RT_MIN(sizeof(abBuf), cbCanWrite); + cbToWrite = RT_MIN(cbToWrite, cbToWriteTotal - cbWrittenTotal); + + if (g_uVerbosity >= 4) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Playing back %RU32 bytes\n", idxTest, cbToWrite); + + if (cbToWrite) + { + rc = AudioTestToneGenerate(&TstTone, abBuf, cbToWrite, &cbToWrite); + if (RT_SUCCESS(rc)) + { + if (pTstEnv) + { + /* Write stuff to disk before trying to play it. Helps analysis later. */ + rc = AudioTestObjWrite(Obj, abBuf, cbToWrite); + } + + if (RT_SUCCESS(rc)) + { + rc = AudioTestMixStreamPlay(&pStream->Mix, abBuf, cbToWrite, &cbWritten); + if (RT_SUCCESS(rc)) + { + AssertBreakStmt(cbWritten <= cbToWrite, rc = VERR_TOO_MUCH_DATA); + + offStream += cbWritten; + + if (cbWritten != cbToWrite) + RTTestFailed(g_hTest, "Test #%RU32: Only played %RU32/%RU32 bytes", + idxTest, cbWritten, cbToWrite); + + if (cbWritten) + nsLastWrite = nsNow; + + if (g_uVerbosity >= 4) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Played back %RU32 bytes\n", idxTest, cbWritten); + + cbWrittenTotal += cbWritten; + } + } + } + } + + if (RT_SUCCESS(rc)) + { + const bool fComplete = cbWrittenTotal >= cbToWriteTotal; + if (fComplete) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing back audio data ended\n", idxTest); + + enmState = AUDIOTESTSTATE_POST; + + /* Re-use the beacon object, but this time it's the post beacon. */ + AudioTestBeaconInit(&Beacon, (uint8_t)idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_POST, + &pStream->Cfg.Props); + } + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing back failed with %Rrc\n", idxTest, rc); + break; + } + + case AUDIOTESTSTATE_DONE: + { + /* Handled below. */ + break; + } + + default: + AssertFailed(); + break; + } + + if (RT_FAILURE(rc)) + break; + + if (enmState == AUDIOTESTSTATE_DONE) + break; + + nsLastMsgCantWrite = 0; + } + else if (AudioTestMixStreamIsOkay(&pStream->Mix)) + { + RTMSINTERVAL const msSleep = RT_MIN(RT_MAX(1, pStream->Cfg.Device.cMsSchedulingHint), 256); + + if ( g_uVerbosity >= 3 + && ( !nsLastMsgCantWrite + || (nsNow - nsLastMsgCantWrite) > RT_NS_10SEC)) /* Don't spam the output too much. */ + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Waiting %RU32ms for stream to be writable again (last write %RU64ns ago) ...\n", + idxTest, msSleep, nsNow - nsLastWrite); + nsLastMsgCantWrite = nsNow; + } + + RTThreadSleep(msSleep); + } + else + AssertFailedBreakStmt(rc = VERR_AUDIO_STREAM_NOT_READY); + + /* Fail-safe in case something screwed up while playing back. */ + uint64_t const cNsElapsed = nsNow - nsStarted; + if (cNsElapsed > nsTimeout) + { + RTTestFailed(g_hTest, "Test #%RU32: Playback took too long (running %RU64 vs. timeout %RU64), aborting\n", + idxTest, cNsElapsed, nsTimeout); + rc = VERR_TIMEOUT; + } + + if (RT_FAILURE(rc)) + break; + } /* for */ + + if (cbWrittenTotal != cbToWriteTotal) + RTTestFailed(g_hTest, "Test #%RU32: Playback ended unexpectedly (%RU32/%RU32 played)\n", + idxTest, cbWrittenTotal, cbToWriteTotal); + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Draining stream ...\n", idxTest); + rc = AudioTestMixStreamDrain(&pStream->Mix, true /*fSync*/); + } + } + else + rc = VERR_AUDIO_STREAM_NOT_READY; + + if (pTstEnv) + { + rc2 = AudioTestObjClose(Obj); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: Playing tone failed with %Rrc\n", idxTest, rc); + + return rc; +} + +/** + * Records a test tone from a specific audio test stream. + * + * @returns VBox status code. + * @param pIoOpts I/O options to use. + * @param pTstEnv Test environment to use for running the test. + * @param pStream Stream to use for recording the tone. + * @param pParms Tone parameters to use. + * + * @note Blocking function. + */ +static int audioTestRecordTone(PAUDIOTESTIOOPTS pIoOpts, PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms) +{ + uint32_t const idxTest = (uint8_t)pParms->Hdr.idxTest; + + const char *pcszPathOut = pTstEnv->Set.szPathAbs; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording test tone (tone frequency is %RU16Hz, %RU32ms)\n", + idxTest, (uint16_t)pParms->dbFreqHz, pParms->msDuration); + RTTestPrintf(g_hTest, RTTESTLVL_DEBUG, "Test #%RU32: Writing to '%s'\n", idxTest, pcszPathOut); + + /** @todo Use .WAV here? */ + AUDIOTESTOBJ Obj; + int rc = AudioTestSetObjCreateAndRegister(&pTstEnv->Set, "guest-tone-rec.pcm", &Obj); + AssertRCReturn(rc, rc); + + PAUDIOTESTDRVMIXSTREAM pMix = &pStream->Mix; + + rc = AudioTestMixStreamEnable(pMix); + if (RT_SUCCESS(rc)) + { + uint32_t cbRecTotal = 0; /* Counts everything, including silence / whatever. */ + uint32_t cbTestToRec = PDMAudioPropsMilliToBytes(&pStream->Cfg.Props, pParms->msDuration); + uint32_t cbTestRec = 0; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording %RU32 bytes total\n", idxTest, cbTestToRec); + + /* We expect a pre + post beacon before + after the actual test tone. + * We always start with the pre beacon. */ + AUDIOTESTTONEBEACON Beacon; + AudioTestBeaconInit(&Beacon, (uint8_t)pParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_PRE, &pStream->Cfg.Props); + + uint32_t const cbBeacon = AudioTestBeaconGetSize(&Beacon); + if (cbBeacon) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Expecting 2 x %RU32 bytes pre/post beacons\n", + idxTest, cbBeacon); + if (g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Waiting for %s beacon ...\n", + idxTest, AudioTestBeaconTypeGetName(Beacon.enmType)); + } + + AudioTestObjAddMetadataStr(Obj, "test_id=%04RU32\n", pParms->Hdr.idxTest); + AudioTestObjAddMetadataStr(Obj, "beacon_type=%RU32\n", (uint32_t)AudioTestBeaconGetType(&Beacon)); + AudioTestObjAddMetadataStr(Obj, "beacon_pre_bytes=%RU32\n", cbBeacon); + AudioTestObjAddMetadataStr(Obj, "beacon_post_bytes=%RU32\n", cbBeacon); + AudioTestObjAddMetadataStr(Obj, "stream_to_record_bytes=%RU32\n", cbTestToRec); + AudioTestObjAddMetadataStr(Obj, "stream_buffer_size_ms=%RU32\n", pIoOpts->cMsBufferSize); + AudioTestObjAddMetadataStr(Obj, "stream_prebuf_size_ms=%RU32\n", pIoOpts->cMsPreBuffer); + /* Note: This mostly is provided by backend (e.g. PulseAudio / ALSA / ++) and + * has nothing to do with the device emulation scheduling hint. */ + AudioTestObjAddMetadataStr(Obj, "device_scheduling_hint_ms=%RU32\n", pIoOpts->cMsSchedulingHint); + + uint8_t abSamples[16384]; + uint32_t const cbSamplesAligned = PDMAudioPropsFloorBytesToFrame(pMix->pProps, sizeof(abSamples)); + + uint64_t const nsStarted = RTTimeNanoTS(); + + uint64_t nsTimeout = RT_MS_5MIN_64 * RT_NS_1MS; + uint64_t nsLastMsgCantRead = 0; /* Timestamp (in ns) when the last message of an unreadable stream was shown. */ + + AUDIOTESTSTATE enmState = AUDIOTESTSTATE_PRE; + + while (!g_fTerminate) + { + uint64_t const nsNow = RTTimeNanoTS(); + + /* + * Anything we can read? + */ + uint32_t const cbCanRead = AudioTestMixStreamGetReadable(pMix); + if (cbCanRead) + { + if (g_uVerbosity >= 3) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Stream is readable with %RU64ms (%RU32 bytes)\n", + idxTest, PDMAudioPropsBytesToMilli(pMix->pProps, cbCanRead), cbCanRead); + + uint32_t const cbToRead = RT_MIN(cbCanRead, cbSamplesAligned); + uint32_t cbRecorded = 0; + rc = AudioTestMixStreamCapture(pMix, abSamples, cbToRead, &cbRecorded); + if (RT_SUCCESS(rc)) + { + /* Flag indicating whether the whole block we're going to play is silence or not. */ + bool const fIsAllSilence = PDMAudioPropsIsBufferSilence(&pStream->pStream->Cfg.Props, abSamples, cbRecorded); + + cbRecTotal += cbRecorded; /* Do a bit of accounting. */ + + switch (enmState) + { + case AUDIOTESTSTATE_PRE: + RT_FALL_THROUGH(); + case AUDIOTESTSTATE_POST: + { + bool fGoToNextStage = false; + + if ( AudioTestBeaconGetSize(&Beacon) + && !AudioTestBeaconIsComplete(&Beacon)) + { + bool const fStarted = AudioTestBeaconGetRemaining(&Beacon) == AudioTestBeaconGetSize(&Beacon); + + size_t uOff; + rc = AudioTestBeaconAddConsecutive(&Beacon, abSamples, cbRecorded, &uOff); + if (RT_SUCCESS(rc)) + { + /* + * When being in the AUDIOTESTSTATE_PRE state, we might get more audio data + * than we need for the pre-beacon to complete. In other words, that "more data" + * needs to be counted to the actual recorded test tone data then. + */ + if (enmState == AUDIOTESTSTATE_PRE) + cbTestRec += cbRecorded - (uint32_t)uOff; + } + + if ( fStarted + && g_uVerbosity >= 3) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Detection of %s beacon started (%RU32ms recorded so far)\n", + idxTest, AudioTestBeaconTypeGetName(Beacon.enmType), + PDMAudioPropsBytesToMilli(&pStream->pStream->Cfg.Props, cbRecTotal)); + + if (AudioTestBeaconIsComplete(&Beacon)) + { + if (g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Detected %s beacon\n", + idxTest, AudioTestBeaconTypeGetName(Beacon.enmType)); + fGoToNextStage = true; + } + } + else + fGoToNextStage = true; + + if (fGoToNextStage) + { + if (enmState == AUDIOTESTSTATE_PRE) + enmState = AUDIOTESTSTATE_RUN; + else if (enmState == AUDIOTESTSTATE_POST) + enmState = AUDIOTESTSTATE_DONE; + } + break; + } + + case AUDIOTESTSTATE_RUN: + { + /* Whether we count all silence as recorded data or not. + * Currently we don't, as otherwise consequtively played tones will be cut off in the end. */ + if (!fIsAllSilence) + { + uint32_t const cbToAddMax = cbTestToRec - cbTestRec; + + /* Don't read more than we're told to. + * After the actual test tone data there might come a post beacon which also + * needs to be handled in the AUDIOTESTSTATE_POST state then. */ + if (cbRecorded > cbToAddMax) + cbRecorded = cbToAddMax; + + cbTestRec += cbRecorded; + } + + if (cbTestToRec - cbTestRec == 0) /* Done recording the test tone? */ + { + enmState = AUDIOTESTSTATE_POST; + + if (g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording tone data done", idxTest); + + if (AudioTestBeaconGetSize(&Beacon)) + { + /* Re-use the beacon object, but this time it's the post beacon. */ + AudioTestBeaconInit(&Beacon, (uint8_t)pParms->Hdr.idxTest, AUDIOTESTTONEBEACONTYPE_PLAY_POST, + &pStream->Cfg.Props); + if (g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Waiting for %s beacon ...", + idxTest, AudioTestBeaconTypeGetName(Beacon.enmType)); + } + } + break; + } + + case AUDIOTESTSTATE_DONE: + { + /* Nothing to do here. */ + break; + } + + default: + AssertFailed(); + break; + } + } + + if (cbRecorded) + { + /* Always write (record) everything, no matter if the current audio contains complete silence or not. + * Might be also become handy later if we want to have a look at start/stop timings and so on. */ + rc = AudioTestObjWrite(Obj, abSamples, cbRecorded); + AssertRCBreak(rc); + } + + if (enmState == AUDIOTESTSTATE_DONE) /* Bail out when in state "done". */ + break; + } + else if (AudioTestMixStreamIsOkay(pMix)) + { + RTMSINTERVAL const msSleep = RT_MIN(RT_MAX(1, pStream->Cfg.Device.cMsSchedulingHint), 256); + + if ( g_uVerbosity >= 3 + && ( !nsLastMsgCantRead + || (nsNow - nsLastMsgCantRead) > RT_NS_10SEC)) /* Don't spam the output too much. */ + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Waiting %RU32ms for stream to be readable again ...\n", + idxTest, msSleep); + nsLastMsgCantRead = nsNow; + } + + RTThreadSleep(msSleep); + } + + /* Fail-safe in case something screwed up while playing back. */ + uint64_t const cNsElapsed = nsNow - nsStarted; + if (cNsElapsed > nsTimeout) + { + RTTestFailed(g_hTest, "Test #%RU32: Recording took too long (running %RU64 vs. timeout %RU64), aborting\n", + idxTest, cNsElapsed, nsTimeout); + rc = VERR_TIMEOUT; + } + + if (RT_FAILURE(rc)) + break; + } + + if (g_uVerbosity >= 2) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recorded %RU32 bytes total\n", idxTest, cbRecTotal); + if (cbTestRec != cbTestToRec) + { + RTTestFailed(g_hTest, "Test #%RU32: Recording ended unexpectedly (%RU32/%RU32 recorded)\n", + idxTest, cbTestRec, cbTestToRec); + rc = VERR_WRONG_ORDER; /** @todo Find a better rc. */ + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: Recording failed (state is '%s')\n", idxTest, AudioTestStateToStr(enmState)); + + int rc2 = AudioTestMixStreamDisable(pMix); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + int rc2 = AudioTestObjClose(Obj); + if (RT_SUCCESS(rc)) + rc = rc2; + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: Recording tone done failed with %Rrc\n", idxTest, rc); + + return rc; +} + + +/********************************************************************************************************************************* +* ATS Callback Implementations * +*********************************************************************************************************************************/ + +/** @copydoc ATSCALLBACKS::pfnHowdy + * + * @note Runs as part of the guest ATS. + */ +static DECLCALLBACK(int) audioTestGstAtsHowdyCallback(void const *pvUser) +{ + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + + AssertReturn(pCtx->cClients <= UINT8_MAX - 1, VERR_BUFFER_OVERFLOW); + + pCtx->cClients++; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "New client connected, now %RU8 total\n", pCtx->cClients); + + return VINF_SUCCESS; +} + +/** @copydoc ATSCALLBACKS::pfnBye + * + * @note Runs as part of the guest ATS. + */ +static DECLCALLBACK(int) audioTestGstAtsByeCallback(void const *pvUser) +{ + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + + AssertReturn(pCtx->cClients, VERR_WRONG_ORDER); + pCtx->cClients--; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Client wants to disconnect, %RU8 remaining\n", pCtx->cClients); + + if (0 == pCtx->cClients) /* All clients disconnected? Tear things down. */ + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Last client disconnected, terminating server ...\n"); + ASMAtomicWriteBool(&g_fTerminate, true); + } + + return VINF_SUCCESS; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetBegin + * + * @note Runs as part of the guest ATS. + */ +static DECLCALLBACK(int) audioTestGstAtsTestSetBeginCallback(void const *pvUser, const char *pszTag) +{ + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + PAUDIOTESTENV pTstEnv = pCtx->pTstEnv; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for beginning test set '%s' in '%s'\n", pszTag, pTstEnv->szPathTemp); + + return AudioTestSetCreate(&pTstEnv->Set, pTstEnv->szPathTemp, pszTag); +} + +/** @copydoc ATSCALLBACKS::pfnTestSetEnd + * + * @note Runs as part of the guest ATS. + */ +static DECLCALLBACK(int) audioTestGstAtsTestSetEndCallback(void const *pvUser, const char *pszTag) +{ + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + PAUDIOTESTENV pTstEnv = pCtx->pTstEnv; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for ending test set '%s'\n", pszTag); + + /* Pack up everything to be ready for transmission. */ + return audioTestEnvPrologue(pTstEnv, true /* fPack */, pCtx->szTestSetArchive, sizeof(pCtx->szTestSetArchive)); +} + +/** @copydoc ATSCALLBACKS::pfnTonePlay + * + * @note Runs as part of the guest ATS. + */ +static DECLCALLBACK(int) audioTestGstAtsTonePlayCallback(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms) +{ + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + PAUDIOTESTENV pTstEnv = pCtx->pTstEnv; + PAUDIOTESTIOOPTS pIoOpts = &pTstEnv->IoOpts; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for playing test tone #%RU32 (%RU16Hz, %RU32ms) ...\n", + pToneParms->Hdr.idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration); + + char szTimeCreated[RTTIME_STR_LEN]; + RTTimeToString(&pToneParms->Hdr.tsCreated, szTimeCreated, sizeof(szTimeCreated)); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test created (caller UTC): %s\n", szTimeCreated); + + const PAUDIOTESTSTREAM pTstStream = &pTstEnv->aStreams[0]; /** @todo Make this dynamic. */ + + int rc = audioTestStreamInit(pTstEnv->pDrvStack, pTstStream, PDMAUDIODIR_OUT, pIoOpts); + if (RT_SUCCESS(rc)) + { + AUDIOTESTPARMS TstParms; + RT_ZERO(TstParms); + TstParms.enmType = AUDIOTESTTYPE_TESTTONE_PLAY; + TstParms.enmDir = PDMAUDIODIR_OUT; + TstParms.TestTone = *pToneParms; + + PAUDIOTESTENTRY pTst; + rc = AudioTestSetTestBegin(&pTstEnv->Set, "Playing test tone", &TstParms, &pTst); + if (RT_SUCCESS(rc)) + { + rc = audioTestPlayTone(&pTstEnv->IoOpts, pTstEnv, pTstStream, pToneParms); + if (RT_SUCCESS(rc)) + { + AudioTestSetTestDone(pTst); + } + else + AudioTestSetTestFailed(pTst, rc, "Playing tone failed"); + } + + int rc2 = audioTestStreamDestroy(pTstEnv->pDrvStack, pTstStream); + if (RT_SUCCESS(rc)) + rc = rc2; + } + else + RTTestFailed(g_hTest, "Error creating output stream, rc=%Rrc\n", rc); + + return rc; +} + +/** @copydoc ATSCALLBACKS::pfnToneRecord */ +static DECLCALLBACK(int) audioTestGstAtsToneRecordCallback(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms) +{ + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + PAUDIOTESTENV pTstEnv = pCtx->pTstEnv; + PAUDIOTESTIOOPTS pIoOpts = &pTstEnv->IoOpts; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Got request for recording test tone #%RU32 (%RU32ms) ...\n", + pToneParms->Hdr.idxTest, pToneParms->msDuration); + + char szTimeCreated[RTTIME_STR_LEN]; + RTTimeToString(&pToneParms->Hdr.tsCreated, szTimeCreated, sizeof(szTimeCreated)); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test created (caller UTC): %s\n", szTimeCreated); + + const PAUDIOTESTSTREAM pTstStream = &pTstEnv->aStreams[0]; /** @todo Make this dynamic. */ + + int rc = audioTestStreamInit(pTstEnv->pDrvStack, pTstStream, PDMAUDIODIR_IN, pIoOpts); + if (RT_SUCCESS(rc)) + { + AUDIOTESTPARMS TstParms; + RT_ZERO(TstParms); + TstParms.enmType = AUDIOTESTTYPE_TESTTONE_RECORD; + TstParms.enmDir = PDMAUDIODIR_IN; + TstParms.TestTone = *pToneParms; + + PAUDIOTESTENTRY pTst; + rc = AudioTestSetTestBegin(&pTstEnv->Set, "Recording test tone from host", &TstParms, &pTst); + if (RT_SUCCESS(rc)) + { + rc = audioTestRecordTone(pIoOpts, pTstEnv, pTstStream, pToneParms); + if (RT_SUCCESS(rc)) + { + AudioTestSetTestDone(pTst); + } + else + AudioTestSetTestFailed(pTst, rc, "Recording tone failed"); + } + + int rc2 = audioTestStreamDestroy(pTstEnv->pDrvStack, pTstStream); + if (RT_SUCCESS(rc)) + rc = rc2; + } + else + RTTestFailed(g_hTest, "Error creating input stream, rc=%Rrc\n", rc); + + return rc; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetSendBegin */ +static DECLCALLBACK(int) audioTestGstAtsTestSetSendBeginCallback(void const *pvUser, const char *pszTag) +{ + RT_NOREF(pszTag); + + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + + if (!RTFileExists(pCtx->szTestSetArchive)) /* Has the archive successfully been created yet? */ + return VERR_WRONG_ORDER; + + int rc = RTFileOpen(&pCtx->hTestSetArchive, pCtx->szTestSetArchive, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE); + if (RT_SUCCESS(rc)) + { + uint64_t uSize; + rc = RTFileQuerySize(pCtx->hTestSetArchive, &uSize); + if (RT_SUCCESS(rc)) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Sending test set '%s' (%zu bytes)\n", pCtx->szTestSetArchive, uSize); + } + + return rc; +} + +/** @copydoc ATSCALLBACKS::pfnTestSetSendRead */ +static DECLCALLBACK(int) audioTestGstAtsTestSetSendReadCallback(void const *pvUser, + const char *pszTag, void *pvBuf, size_t cbBuf, size_t *pcbRead) +{ + RT_NOREF(pszTag); + + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + + return RTFileRead(pCtx->hTestSetArchive, pvBuf, cbBuf, pcbRead); +} + +/** @copydoc ATSCALLBACKS::pfnTestSetSendEnd */ +static DECLCALLBACK(int) audioTestGstAtsTestSetSendEndCallback(void const *pvUser, const char *pszTag) +{ + RT_NOREF(pszTag); + + PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser; + + int rc = RTFileClose(pCtx->hTestSetArchive); + if (RT_SUCCESS(rc)) + { + pCtx->hTestSetArchive = NIL_RTFILE; + } + + return rc; +} + + +/********************************************************************************************************************************* +* Implementation of audio test environment handling * +*********************************************************************************************************************************/ + +/** + * Connects an ATS client via TCP/IP to a peer. + * + * @returns VBox status code. + * @param pTstEnv Test environment to use. + * @param pClient Client to connect. + * @param pszWhat Hint of what to connect to where. + * @param pTcpOpts Pointer to TCP options to use. + */ +int audioTestEnvConnectViaTcp(PAUDIOTESTENV pTstEnv, PATSCLIENT pClient, const char *pszWhat, PAUDIOTESTENVTCPOPTS pTcpOpts) +{ + RT_NOREF(pTstEnv); + + RTGETOPTUNION Val; + RT_ZERO(Val); + + Val.u32 = pTcpOpts->enmConnMode; + int rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONN_MODE, &Val); + AssertRCReturn(rc, rc); + + if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH + || pTcpOpts->enmConnMode == ATSCONNMODE_SERVER) + { + Assert(pTcpOpts->uBindPort); /* Always set by the caller. */ + Val.u16 = pTcpOpts->uBindPort; + rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_BIND_PORT, &Val); + AssertRCReturn(rc, rc); + + if (pTcpOpts->szBindAddr[0]) + { + Val.psz = pTcpOpts->szBindAddr; + rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_BIND_ADDRESS, &Val); + AssertRCReturn(rc, rc); + } + else + { + RTTestFailed(g_hTest, "No bind address specified!\n"); + return VERR_INVALID_PARAMETER; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Connecting %s by listening as server at %s:%RU32 ...\n", + pszWhat, pTcpOpts->szBindAddr, pTcpOpts->uBindPort); + } + + + if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH + || pTcpOpts->enmConnMode == ATSCONNMODE_CLIENT) + { + Assert(pTcpOpts->uConnectPort); /* Always set by the caller. */ + Val.u16 = pTcpOpts->uConnectPort; + rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONNECT_PORT, &Val); + AssertRCReturn(rc, rc); + + if (pTcpOpts->szConnectAddr[0]) + { + Val.psz = pTcpOpts->szConnectAddr; + rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONNECT_ADDRESS, &Val); + AssertRCReturn(rc, rc); + } + else + { + RTTestFailed(g_hTest, "No connect address specified!\n"); + return VERR_INVALID_PARAMETER; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Connecting %s by connecting as client to %s:%RU32 ...\n", + pszWhat, pTcpOpts->szConnectAddr, pTcpOpts->uConnectPort); + } + + rc = AudioTestSvcClientConnect(pClient); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "Connecting %s failed with %Rrc\n", pszWhat, rc); + return rc; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Successfully connected %s\n", pszWhat); + return rc; +} + +/** + * Configures and starts an ATS TCP/IP server. + * + * @returns VBox status code. + * @param pSrv ATS server instance to configure and start. + * @param pCallbacks ATS callback table to use. + * @param pszDesc Hint of server type which is being started. + * @param pTcpOpts TCP options to use. + */ +int audioTestEnvConfigureAndStartTcpServer(PATSSERVER pSrv, PCATSCALLBACKS pCallbacks, const char *pszDesc, + PAUDIOTESTENVTCPOPTS pTcpOpts) +{ + RTGETOPTUNION Val; + RT_ZERO(Val); + + int rc = AudioTestSvcInit(pSrv, pCallbacks); + if (RT_FAILURE(rc)) + return rc; + + Val.u32 = pTcpOpts->enmConnMode; + rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONN_MODE, &Val); + AssertRCReturn(rc, rc); + + if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH + || pTcpOpts->enmConnMode == ATSCONNMODE_SERVER) + { + Assert(pTcpOpts->uBindPort); /* Always set by the caller. */ + Val.u16 = pTcpOpts->uBindPort; + rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_BIND_PORT, &Val); + AssertRCReturn(rc, rc); + + if (pTcpOpts->szBindAddr[0]) + { + Val.psz = pTcpOpts->szBindAddr; + rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_BIND_ADDRESS, &Val); + AssertRCReturn(rc, rc); + } + else + { + RTTestFailed(g_hTest, "No bind address specified!\n"); + return VERR_INVALID_PARAMETER; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Starting server for %s at %s:%RU32 ...\n", + pszDesc, pTcpOpts->szBindAddr, pTcpOpts->uBindPort); + } + + + if ( pTcpOpts->enmConnMode == ATSCONNMODE_BOTH + || pTcpOpts->enmConnMode == ATSCONNMODE_CLIENT) + { + Assert(pTcpOpts->uConnectPort); /* Always set by the caller. */ + Val.u16 = pTcpOpts->uConnectPort; + rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONNECT_PORT, &Val); + AssertRCReturn(rc, rc); + + if (pTcpOpts->szConnectAddr[0]) + { + Val.psz = pTcpOpts->szConnectAddr; + rc = AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONNECT_ADDRESS, &Val); + AssertRCReturn(rc, rc); + } + else + { + RTTestFailed(g_hTest, "No connect address specified!\n"); + return VERR_INVALID_PARAMETER; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Starting server for %s by connecting as client to %s:%RU32 ...\n", + pszDesc, pTcpOpts->szConnectAddr, pTcpOpts->uConnectPort); + } + + if (RT_SUCCESS(rc)) + { + rc = AudioTestSvcStart(pSrv); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Starting server for %s failed with %Rrc\n", pszDesc, rc); + } + + return rc; +} + +/** + * Initializes an audio test environment. + * + * @param pTstEnv Audio test environment to initialize. + */ +void audioTestEnvInit(PAUDIOTESTENV pTstEnv) +{ + RT_BZERO(pTstEnv, sizeof(AUDIOTESTENV)); + + audioTestIoOptsInitDefaults(&pTstEnv->IoOpts); + audioTestToneParmsInit(&pTstEnv->ToneParms); +} + +/** + * Creates an audio test environment. + * + * @returns VBox status code. + * @param pTstEnv Audio test environment to create. + * @param pDrvStack Driver stack to use. + */ +int audioTestEnvCreate(PAUDIOTESTENV pTstEnv, PAUDIOTESTDRVSTACK pDrvStack) +{ + AssertReturn(PDMAudioPropsAreValid(&pTstEnv->IoOpts.Props), VERR_WRONG_ORDER); + + int rc = VINF_SUCCESS; + + pTstEnv->pDrvStack = pDrvStack; + + /* + * Set sane defaults if not already set. + */ + if (!RTStrNLen(pTstEnv->szTag, sizeof(pTstEnv->szTag))) + { + rc = AudioTestGenTag(pTstEnv->szTag, sizeof(pTstEnv->szTag)); + AssertRCReturn(rc, rc); + } + + if (!RTStrNLen(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp))) + { + rc = AudioTestPathGetTemp(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp)); + AssertRCReturn(rc, rc); + } + + if (!RTStrNLen(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut))) + { + rc = RTPathJoin(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut), pTstEnv->szPathTemp, "vkat-temp"); + AssertRCReturn(rc, rc); + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Initializing environment for mode '%s'\n", pTstEnv->enmMode == AUDIOTESTMODE_HOST ? "host" : "guest"); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using tag '%s'\n", pTstEnv->szTag); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Output directory is '%s'\n", pTstEnv->szPathOut); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Temp directory is '%s'\n", pTstEnv->szPathTemp); + + char szPathTemp[RTPATH_MAX]; + if ( !strlen(pTstEnv->szPathTemp) + || !strlen(pTstEnv->szPathOut)) + rc = RTPathTemp(szPathTemp, sizeof(szPathTemp)); + + if ( RT_SUCCESS(rc) + && !strlen(pTstEnv->szPathTemp)) + rc = RTPathJoin(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp), szPathTemp, "vkat-temp"); + + if (RT_SUCCESS(rc)) + { + rc = RTDirCreate(pTstEnv->szPathTemp, RTFS_UNIX_IRWXU, 0 /* fFlags */); + if (rc == VERR_ALREADY_EXISTS) + rc = VINF_SUCCESS; + } + + if ( RT_SUCCESS(rc) + && !strlen(pTstEnv->szPathOut)) + rc = RTPathJoin(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut), szPathTemp, "vkat"); + + if (RT_SUCCESS(rc)) + { + rc = RTDirCreate(pTstEnv->szPathOut, RTFS_UNIX_IRWXU, 0 /* fFlags */); + if (rc == VERR_ALREADY_EXISTS) + rc = VINF_SUCCESS; + } + + if (RT_FAILURE(rc)) + return rc; + + /** + * For NAT'ed VMs we use (default): + * - client mode (uConnectAddr / uConnectPort) on the guest. + * - server mode (uBindAddr / uBindPort) on the host. + */ + if ( !pTstEnv->TcpOpts.szConnectAddr[0] + && !pTstEnv->TcpOpts.szBindAddr[0]) + RTStrCopy(pTstEnv->TcpOpts.szBindAddr, sizeof(pTstEnv->TcpOpts.szBindAddr), "0.0.0.0"); + + /* + * Determine connection mode based on set variables. + */ + if ( pTstEnv->TcpOpts.szBindAddr[0] + && pTstEnv->TcpOpts.szConnectAddr[0]) + { + pTstEnv->TcpOpts.enmConnMode = ATSCONNMODE_BOTH; + } + else if (pTstEnv->TcpOpts.szBindAddr[0]) + pTstEnv->TcpOpts.enmConnMode = ATSCONNMODE_SERVER; + else /* "Reversed mode", i.e. used for NATed VMs. */ + pTstEnv->TcpOpts.enmConnMode = ATSCONNMODE_CLIENT; + + /* Set a back reference to the test environment for the callback context. */ + pTstEnv->CallbackCtx.pTstEnv = pTstEnv; + + ATSCALLBACKS Callbacks; + RT_ZERO(Callbacks); + Callbacks.pvUser = &pTstEnv->CallbackCtx; + + if (pTstEnv->enmMode == AUDIOTESTMODE_GUEST) + { + Callbacks.pfnHowdy = audioTestGstAtsHowdyCallback; + Callbacks.pfnBye = audioTestGstAtsByeCallback; + Callbacks.pfnTestSetBegin = audioTestGstAtsTestSetBeginCallback; + Callbacks.pfnTestSetEnd = audioTestGstAtsTestSetEndCallback; + Callbacks.pfnTonePlay = audioTestGstAtsTonePlayCallback; + Callbacks.pfnToneRecord = audioTestGstAtsToneRecordCallback; + Callbacks.pfnTestSetSendBegin = audioTestGstAtsTestSetSendBeginCallback; + Callbacks.pfnTestSetSendRead = audioTestGstAtsTestSetSendReadCallback; + Callbacks.pfnTestSetSendEnd = audioTestGstAtsTestSetSendEndCallback; + + if (!pTstEnv->TcpOpts.uBindPort) + pTstEnv->TcpOpts.uBindPort = ATS_TCP_DEF_BIND_PORT_GUEST; + + if (!pTstEnv->TcpOpts.uConnectPort) + pTstEnv->TcpOpts.uConnectPort = ATS_TCP_DEF_CONNECT_PORT_GUEST; + + pTstEnv->pSrv = (PATSSERVER)RTMemAlloc(sizeof(ATSSERVER)); + AssertPtrReturn(pTstEnv->pSrv, VERR_NO_MEMORY); + + /* + * Start the ATS (Audio Test Service) on the guest side. + * That service then will perform playback and recording operations on the guest, triggered from the host. + * + * When running this in self-test mode, that service also can be run on the host if nothing else is specified. + * Note that we have to bind to "0.0.0.0" by default so that the host can connect to it. + */ + rc = audioTestEnvConfigureAndStartTcpServer(pTstEnv->pSrv, &Callbacks, "guest", &pTstEnv->TcpOpts); + } + else /* Host mode */ + { + if (!pTstEnv->TcpOpts.uBindPort) + pTstEnv->TcpOpts.uBindPort = ATS_TCP_DEF_BIND_PORT_HOST; + + if (!pTstEnv->TcpOpts.uConnectPort) + pTstEnv->TcpOpts.uConnectPort = ATS_TCP_DEF_CONNECT_PORT_HOST_PORT_FWD; + + /** + * Note: Don't set pTstEnv->TcpOpts.szTcpConnectAddr by default here, as this specifies what connection mode + * (client / server / both) we use on the host. + */ + + /* We need to start a server on the host so that VMs configured with NAT networking + * can connect to it as well. */ + rc = AudioTestSvcClientCreate(&pTstEnv->u.Host.AtsClGuest); + if (RT_SUCCESS(rc)) + rc = audioTestEnvConnectViaTcp(pTstEnv, &pTstEnv->u.Host.AtsClGuest, + "host -> guest", &pTstEnv->TcpOpts); + if (RT_SUCCESS(rc)) + { + AUDIOTESTENVTCPOPTS ValKitTcpOpts; + RT_ZERO(ValKitTcpOpts); + + /* We only connect as client to the Validation Kit audio driver ATS. */ + ValKitTcpOpts.enmConnMode = ATSCONNMODE_CLIENT; + + /* For now we ASSUME that the Validation Kit audio driver ATS runs on the same host as VKAT (this binary) runs on. */ + ValKitTcpOpts.uConnectPort = ATS_TCP_DEF_CONNECT_PORT_VALKIT; /** @todo Make this dynamic. */ + RTStrCopy(ValKitTcpOpts.szConnectAddr, sizeof(ValKitTcpOpts.szConnectAddr), ATS_TCP_DEF_CONNECT_HOST_ADDR_STR); /** @todo Ditto. */ + + rc = AudioTestSvcClientCreate(&pTstEnv->u.Host.AtsClValKit); + if (RT_SUCCESS(rc)) + { + rc = audioTestEnvConnectViaTcp(pTstEnv, &pTstEnv->u.Host.AtsClValKit, + "host -> valkit", &ValKitTcpOpts); + if (RT_FAILURE(rc)) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Unable to connect to the Validation Kit audio driver!\n" + "There could be multiple reasons:\n\n" + " - Wrong host being used\n" + " - VirtualBox host version is too old\n" + " - Audio debug mode is not enabled\n" + " - Support for Validation Kit audio driver is not included\n" + " - Firewall / network configuration problem\n"); + } + } + } + + return rc; +} + +/** + * Destroys an audio test environment. + * + * @param pTstEnv Audio test environment to destroy. + */ +void audioTestEnvDestroy(PAUDIOTESTENV pTstEnv) +{ + if (!pTstEnv) + return; + + /* When in host mode, we need to destroy our ATS clients in order to also let + * the ATS server(s) know we're going to quit. */ + if (pTstEnv->enmMode == AUDIOTESTMODE_HOST) + { + AudioTestSvcClientDestroy(&pTstEnv->u.Host.AtsClValKit); + AudioTestSvcClientDestroy(&pTstEnv->u.Host.AtsClGuest); + } + + if (pTstEnv->pSrv) + { + int rc2 = AudioTestSvcDestroy(pTstEnv->pSrv); + AssertRC(rc2); + + RTMemFree(pTstEnv->pSrv); + pTstEnv->pSrv = NULL; + } + + for (unsigned i = 0; i < RT_ELEMENTS(pTstEnv->aStreams); i++) + { + int rc2 = audioTestStreamDestroy(pTstEnv->pDrvStack, &pTstEnv->aStreams[i]); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Stream destruction for stream #%u failed with %Rrc\n", i, rc2); + } + + /* Try cleaning up a bit. */ + RTDirRemove(pTstEnv->szPathTemp); + RTDirRemove(pTstEnv->szPathOut); + + pTstEnv->pDrvStack = NULL; +} + +/** + * Closes, packs up and destroys a test environment. + * + * @returns VBox status code. + * @param pTstEnv Test environment to handle. + * @param fPack Whether to pack the test set up before destroying / wiping it. + * @param pszPackFile Where to store the packed test set file on success. Can be NULL if \a fPack is \c false. + * @param cbPackFile Size (in bytes) of \a pszPackFile. Can be 0 if \a fPack is \c false. + */ +int audioTestEnvPrologue(PAUDIOTESTENV pTstEnv, bool fPack, char *pszPackFile, size_t cbPackFile) +{ + /* Close the test set first. */ + AudioTestSetClose(&pTstEnv->Set); + + int rc = VINF_SUCCESS; + + if (fPack) + { + /* Before destroying the test environment, pack up the test set so + * that it's ready for transmission. */ + rc = AudioTestSetPack(&pTstEnv->Set, pTstEnv->szPathOut, pszPackFile, cbPackFile); + if (RT_SUCCESS(rc)) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set packed up to '%s'\n", pszPackFile); + } + + if (!g_fDrvAudioDebug) /* Don't wipe stuff when debugging. Can be useful for introspecting data. */ + /* ignore rc */ AudioTestSetWipe(&pTstEnv->Set); + + AudioTestSetDestroy(&pTstEnv->Set); + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test set prologue failed with %Rrc\n", rc); + + return rc; +} + +/** + * Initializes an audio test parameters set. + * + * @param pTstParms Test parameters set to initialize. + */ +void audioTestParmsInit(PAUDIOTESTPARMS pTstParms) +{ + RT_ZERO(*pTstParms); +} + +/** + * Destroys an audio test parameters set. + * + * @param pTstParms Test parameters set to destroy. + */ +void audioTestParmsDestroy(PAUDIOTESTPARMS pTstParms) +{ + if (!pTstParms) + return; + + return; +} + diff --git a/src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp b/src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp new file mode 100644 index 00000000..89dbb4ae --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/vkatDriverStack.cpp @@ -0,0 +1,1621 @@ +/* $Id: vkatDriverStack.cpp $ */ +/** @file + * Validation Kit Audio Test (VKAT) - Driver stack code. + */ + +/* + * Copyright (C) 2021-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_TEST +#include <iprt/log.h> + +#include <iprt/errcore.h> +#include <iprt/message.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/uuid.h> +#include <iprt/test.h> + + +/** + * Internal driver instance data + * @note This must be put here as it's needed before pdmdrv.h is included. + */ +typedef struct PDMDRVINSINT +{ + /** The stack the drive belongs to. */ + struct AUDIOTESTDRVSTACK *pStack; +} PDMDRVINSINT; +#define PDMDRVINSINT_DECLARED + +#include "vkatInternal.h" +#include "VBoxDD.h" + + + +/********************************************************************************************************************************* +* Fake PDM Driver Handling. * +*********************************************************************************************************************************/ + +/** @name Driver Fakes/Stubs + * + * @{ */ + +VMMR3DECL(PCFGMNODE) audioTestDrvHlp_CFGMR3GetChild(PCFGMNODE pNode, const char *pszPath) +{ + RT_NOREF(pNode, pszPath); + return NULL; +} + + +VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryString(PCFGMNODE pNode, const char *pszName, char *pszString, size_t cchString) +{ + if (pNode != NULL) + { + PCPDMDRVREG pDrvReg = (PCPDMDRVREG)pNode; + if (g_uVerbosity > 2) + RTPrintf("debug: CFGMR3QueryString([%s], %s, %p, %#x)\n", pDrvReg->szName, pszName, pszString, cchString); + + if ( ( strcmp(pDrvReg->szName, "PulseAudio") == 0 + || strcmp(pDrvReg->szName, "HostAudioWas") == 0) + && strcmp(pszName, "VmName") == 0) + return RTStrCopy(pszString, cchString, "vkat"); + + if ( strcmp(pDrvReg->szName, "HostAudioWas") == 0 + && strcmp(pszName, "VmUuid") == 0) + return RTStrCopy(pszString, cchString, "794c9192-d045-4f28-91ed-46253ac9998e"); + } + else if (g_uVerbosity > 2) + RTPrintf("debug: CFGMR3QueryString(%p, %s, %p, %#x)\n", pNode, pszName, pszString, cchString); + + return VERR_CFGM_VALUE_NOT_FOUND; +} + + +VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryStringAlloc(PCFGMNODE pNode, const char *pszName, char **ppszString) +{ + char szStr[128]; + int rc = audioTestDrvHlp_CFGMR3QueryString(pNode, pszName, szStr, sizeof(szStr)); + if (RT_SUCCESS(rc)) + *ppszString = RTStrDup(szStr); + + return rc; +} + + +VMMR3DECL(void) audioTestDrvHlp_MMR3HeapFree(PPDMDRVINS pDrvIns, void *pv) +{ + RT_NOREF(pDrvIns); + + /* counterpart to CFGMR3QueryStringAlloc */ + RTStrFree((char *)pv); +} + + +VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryStringDef(PCFGMNODE pNode, const char *pszName, char *pszString, size_t cchString, const char *pszDef) +{ + PCPDMDRVREG pDrvReg = (PCPDMDRVREG)pNode; + if (RT_VALID_PTR(pDrvReg)) + { + const char *pszRet = pszDef; + if ( g_pszDrvAudioDebug + && strcmp(pDrvReg->szName, "AUDIO") == 0 + && strcmp(pszName, "DebugPathOut") == 0) + pszRet = g_pszDrvAudioDebug; + + int rc = RTStrCopy(pszString, cchString, pszRet); + + if (g_uVerbosity > 2) + RTPrintf("debug: CFGMR3QueryStringDef([%s], %s, %p, %#x, %s) -> '%s' + %Rrc\n", + pDrvReg->szName, pszName, pszString, cchString, pszDef, pszRet, rc); + return rc; + } + + if (g_uVerbosity > 2) + RTPrintf("debug: CFGMR3QueryStringDef(%p, %s, %p, %#x, %s)\n", pNode, pszName, pszString, cchString, pszDef); + return RTStrCopy(pszString, cchString, pszDef); +} + + +VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryBoolDef(PCFGMNODE pNode, const char *pszName, bool *pf, bool fDef) +{ + PCPDMDRVREG pDrvReg = (PCPDMDRVREG)pNode; + if (RT_VALID_PTR(pDrvReg)) + { + *pf = fDef; + if ( strcmp(pDrvReg->szName, "AUDIO") == 0 + && strcmp(pszName, "DebugEnabled") == 0) + *pf = g_fDrvAudioDebug; + + if (g_uVerbosity > 2) + RTPrintf("debug: CFGMR3QueryBoolDef([%s], %s, %p, %RTbool) -> %RTbool\n", pDrvReg->szName, pszName, pf, fDef, *pf); + return VINF_SUCCESS; + } + *pf = fDef; + return VINF_SUCCESS; +} + + +VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryU8(PCFGMNODE pNode, const char *pszName, uint8_t *pu8) +{ + RT_NOREF(pNode, pszName, pu8); + return VERR_CFGM_VALUE_NOT_FOUND; +} + + +VMMR3DECL(int) audioTestDrvHlp_CFGMR3QueryU32(PCFGMNODE pNode, const char *pszName, uint32_t *pu32) +{ + RT_NOREF(pNode, pszName, pu32); + return VERR_CFGM_VALUE_NOT_FOUND; +} + + +VMMR3DECL(int) audioTestDrvHlp_CFGMR3ValidateConfig(PCFGMNODE pNode, const char *pszNode, + const char *pszValidValues, const char *pszValidNodes, + const char *pszWho, uint32_t uInstance) +{ + RT_NOREF(pNode, pszNode, pszValidValues, pszValidNodes, pszWho, uInstance); + return VINF_SUCCESS; +} + +/** @} */ + +/** @name Driver Helper Fakes + * @{ */ + +static DECLCALLBACK(int) audioTestDrvHlp_Attach(PPDMDRVINS pDrvIns, uint32_t fFlags, PPDMIBASE *ppBaseInterface) +{ + /* DrvAudio must be allowed to attach the backend driver (paranoid + backend drivers may call us to check that nothing is attached). */ + if (strcmp(pDrvIns->pReg->szName, "AUDIO") == 0) + { + PAUDIOTESTDRVSTACK pDrvStack = pDrvIns->Internal.s.pStack; + AssertReturn(pDrvStack->pDrvBackendIns == NULL, VERR_PDM_DRIVER_ALREADY_ATTACHED); + + if (g_uVerbosity > 1) + RTMsgInfo("Attaching backend '%s' to DrvAudio...\n", pDrvStack->pDrvReg->szName); + int rc = audioTestDrvConstruct(pDrvStack, pDrvStack->pDrvReg, pDrvIns, &pDrvStack->pDrvBackendIns); + if (RT_SUCCESS(rc)) + { + if (ppBaseInterface) + *ppBaseInterface = &pDrvStack->pDrvBackendIns->IBase; + } + else + RTMsgError("Failed to attach backend: %Rrc", rc); + return rc; + } + RT_NOREF(fFlags); + return VERR_PDM_NO_ATTACHED_DRIVER; +} + + +static DECLCALLBACK(void) audioTestDrvHlp_STAMRegister(PPDMDRVINS pDrvIns, void *pvSample, STAMTYPE enmType, const char *pszName, + STAMUNIT enmUnit, const char *pszDesc) +{ + RT_NOREF(pDrvIns, pvSample, enmType, pszName, enmUnit, pszDesc); +} + + +static DECLCALLBACK(void) audioTestDrvHlp_STAMRegisterF(PPDMDRVINS pDrvIns, void *pvSample, STAMTYPE enmType, + STAMVISIBILITY enmVisibility, STAMUNIT enmUnit, const char *pszDesc, + const char *pszName, ...) +{ + RT_NOREF(pDrvIns, pvSample, enmType, enmVisibility, enmUnit, pszDesc, pszName); +} + + +static DECLCALLBACK(void) audioTestDrvHlp_STAMRegisterV(PPDMDRVINS pDrvIns, void *pvSample, STAMTYPE enmType, + STAMVISIBILITY enmVisibility, STAMUNIT enmUnit, const char *pszDesc, + const char *pszName, va_list args) +{ + RT_NOREF(pDrvIns, pvSample, enmType, enmVisibility, enmUnit, pszDesc, pszName, args); +} + + +static DECLCALLBACK(int) audioTestDrvHlp_STAMDeregister(PPDMDRVINS pDrvIns, void *pvSample) +{ + RT_NOREF(pDrvIns, pvSample); + return VINF_SUCCESS; +} + + +static DECLCALLBACK(int) audioTestDrvHlp_STAMDeregisterByPrefix(PPDMDRVINS pDrvIns, const char *pszPrefix) +{ + RT_NOREF(pDrvIns, pszPrefix); + return VINF_SUCCESS; +} + +/** + * Get the driver helpers. + */ +static const PDMDRVHLPR3 *audioTestFakeGetDrvHlp(void) +{ + /* + * Note! No initializer for s_DrvHlp (also why it's not a file global). + * We do not want to have to update this code every time PDMDRVHLPR3 + * grows new entries or are otherwise modified. Only when the + * entries used by the audio driver changes do we want to change + * our code. + */ + static PDMDRVHLPR3 s_DrvHlp; + if (s_DrvHlp.u32Version != PDM_DRVHLPR3_VERSION) + { + s_DrvHlp.u32Version = PDM_DRVHLPR3_VERSION; + s_DrvHlp.u32TheEnd = PDM_DRVHLPR3_VERSION; + s_DrvHlp.pfnAttach = audioTestDrvHlp_Attach; + s_DrvHlp.pfnSTAMRegister = audioTestDrvHlp_STAMRegister; + s_DrvHlp.pfnSTAMRegisterF = audioTestDrvHlp_STAMRegisterF; + s_DrvHlp.pfnSTAMRegisterV = audioTestDrvHlp_STAMRegisterV; + s_DrvHlp.pfnSTAMDeregister = audioTestDrvHlp_STAMDeregister; + s_DrvHlp.pfnSTAMDeregisterByPrefix = audioTestDrvHlp_STAMDeregisterByPrefix; + s_DrvHlp.pfnCFGMGetChild = audioTestDrvHlp_CFGMR3GetChild; + s_DrvHlp.pfnCFGMQueryString = audioTestDrvHlp_CFGMR3QueryString; + s_DrvHlp.pfnCFGMQueryStringAlloc = audioTestDrvHlp_CFGMR3QueryStringAlloc; + s_DrvHlp.pfnMMHeapFree = audioTestDrvHlp_MMR3HeapFree; + s_DrvHlp.pfnCFGMQueryStringDef = audioTestDrvHlp_CFGMR3QueryStringDef; + s_DrvHlp.pfnCFGMQueryBoolDef = audioTestDrvHlp_CFGMR3QueryBoolDef; + s_DrvHlp.pfnCFGMQueryU8 = audioTestDrvHlp_CFGMR3QueryU8; + s_DrvHlp.pfnCFGMQueryU32 = audioTestDrvHlp_CFGMR3QueryU32; + s_DrvHlp.pfnCFGMValidateConfig = audioTestDrvHlp_CFGMR3ValidateConfig; + } + return &s_DrvHlp; +} + +/** @} */ + + +/** + * Implementation of PDMIBASE::pfnQueryInterface for a fake device above + * DrvAudio. + */ +static DECLCALLBACK(void *) audioTestFakeDeviceIBaseQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, pInterface); + RTMsgWarning("audioTestFakeDeviceIBaseQueryInterface: Unknown interface: %s\n", pszIID); + return NULL; +} + +/** IBase interface for a fake device above DrvAudio. */ +static PDMIBASE g_AudioTestFakeDeviceIBase = { audioTestFakeDeviceIBaseQueryInterface }; + + +static DECLCALLBACK(int) audioTestIHostAudioPort_DoOnWorkerThread(PPDMIHOSTAUDIOPORT pInterface, PPDMAUDIOBACKENDSTREAM pStream, + uintptr_t uUser, void *pvUser) +{ + RT_NOREF(pInterface, pStream, uUser, pvUser); + RTMsgWarning("audioTestIHostAudioPort_DoOnWorkerThread was called\n"); + return VERR_NOT_IMPLEMENTED; +} + + +DECLCALLBACK(void) audioTestIHostAudioPort_NotifyDeviceChanged(PPDMIHOSTAUDIOPORT pInterface, PDMAUDIODIR enmDir, void *pvUser) +{ + RT_NOREF(pInterface, enmDir, pvUser); + RTMsgWarning("audioTestIHostAudioPort_NotifyDeviceChanged was called\n"); +} + + +static DECLCALLBACK(void) audioTestIHostAudioPort_StreamNotifyPreparingDeviceSwitch(PPDMIHOSTAUDIOPORT pInterface, + PPDMAUDIOBACKENDSTREAM pStream) +{ + RT_NOREF(pInterface, pStream); + RTMsgWarning("audioTestIHostAudioPort_StreamNotifyPreparingDeviceSwitch was called\n"); +} + + +static DECLCALLBACK(void) audioTestIHostAudioPort_StreamNotifyDeviceChanged(PPDMIHOSTAUDIOPORT pInterface, + PPDMAUDIOBACKENDSTREAM pStream, bool fReInit) +{ + RT_NOREF(pInterface, pStream, fReInit); + RTMsgWarning("audioTestIHostAudioPort_StreamNotifyDeviceChanged was called\n"); +} + + +static DECLCALLBACK(void) audioTestIHostAudioPort_NotifyDevicesChanged(PPDMIHOSTAUDIOPORT pInterface) +{ + RT_NOREF(pInterface); + RTMsgWarning("audioTestIHostAudioPort_NotifyDevicesChanged was called\n"); +} + + +static PDMIHOSTAUDIOPORT g_AudioTestIHostAudioPort = +{ + audioTestIHostAudioPort_DoOnWorkerThread, + audioTestIHostAudioPort_NotifyDeviceChanged, + audioTestIHostAudioPort_StreamNotifyPreparingDeviceSwitch, + audioTestIHostAudioPort_StreamNotifyDeviceChanged, + audioTestIHostAudioPort_NotifyDevicesChanged, +}; + + +/** + * Implementation of PDMIBASE::pfnQueryInterface for a fake DrvAudio above a + * backend. + */ +static DECLCALLBACK(void *) audioTestFakeDrvAudioIBaseQueryInterface(PPDMIBASE pInterface, const char *pszIID) +{ + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIBASE, pInterface); + PDMIBASE_RETURN_INTERFACE(pszIID, PDMIHOSTAUDIOPORT, &g_AudioTestIHostAudioPort); + RTMsgWarning("audioTestFakeDrvAudioIBaseQueryInterface: Unknown interface: %s\n", pszIID); + return NULL; +} + + +/** IBase interface for a fake DrvAudio above a lonesome backend. */ +static PDMIBASE g_AudioTestFakeDrvAudioIBase = { audioTestFakeDrvAudioIBaseQueryInterface }; + + + +/** + * Constructs a PDM audio driver instance. + * + * @returns VBox status code. + * @param pDrvStack The stack this is associated with. + * @param pDrvReg PDM driver registration record to use for construction. + * @param pParentDrvIns The parent driver (if any). + * @param ppDrvIns Where to return the driver instance structure. + */ +int audioTestDrvConstruct(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, PPDMDRVINS pParentDrvIns, + PPPDMDRVINS ppDrvIns) +{ + /* The destruct function must have valid data to work with. */ + *ppDrvIns = NULL; + + /* + * Check registration structure validation (doesn't need to be too + * thorough, PDM check it in detail on every VM startup). + */ + AssertPtrReturn(pDrvReg, VERR_INVALID_POINTER); + RTMsgInfo("Initializing backend '%s' ...\n", pDrvReg->szName); + AssertPtrReturn(pDrvReg->pfnConstruct, VERR_INVALID_PARAMETER); + + /* + * Create the instance data structure. + */ + PPDMDRVINS pDrvIns = (PPDMDRVINS)RTMemAllocZVar(RT_UOFFSETOF_DYN(PDMDRVINS, achInstanceData[pDrvReg->cbInstance])); + RTTEST_CHECK_RET(g_hTest, pDrvIns, VERR_NO_MEMORY); + + pDrvIns->u32Version = PDM_DRVINS_VERSION; + pDrvIns->iInstance = 0; + pDrvIns->pHlpR3 = audioTestFakeGetDrvHlp(); + pDrvIns->pvInstanceDataR3 = &pDrvIns->achInstanceData[0]; + pDrvIns->pReg = pDrvReg; + pDrvIns->pCfg = (PCFGMNODE)pDrvReg; + pDrvIns->Internal.s.pStack = pDrvStack; + pDrvIns->pUpBase = NULL; + pDrvIns->pDownBase = NULL; + if (pParentDrvIns) + { + Assert(pParentDrvIns->pDownBase == NULL); + pParentDrvIns->pDownBase = &pDrvIns->IBase; + pDrvIns->pUpBase = &pParentDrvIns->IBase; + } + else if (strcmp(pDrvReg->szName, "AUDIO") == 0) + pDrvIns->pUpBase = &g_AudioTestFakeDeviceIBase; + else + pDrvIns->pUpBase = &g_AudioTestFakeDrvAudioIBase; + + /* + * Invoke the constructor. + */ + int rc = pDrvReg->pfnConstruct(pDrvIns, pDrvIns->pCfg, 0 /*fFlags*/); + if (RT_SUCCESS(rc)) + { + *ppDrvIns = pDrvIns; + return VINF_SUCCESS; + } + + if (pDrvReg->pfnDestruct) + pDrvReg->pfnDestruct(pDrvIns); + RTMemFree(pDrvIns); + return rc; +} + + +/** + * Destructs a PDM audio driver instance. + * + * @param pDrvIns Driver instance to destruct. + */ +static void audioTestDrvDestruct(PPDMDRVINS pDrvIns) +{ + if (pDrvIns) + { + Assert(pDrvIns->u32Version == PDM_DRVINS_VERSION); + + if (pDrvIns->pReg->pfnDestruct) + pDrvIns->pReg->pfnDestruct(pDrvIns); + + pDrvIns->u32Version = 0; + pDrvIns->pReg = NULL; + RTMemFree(pDrvIns); + } +} + + +/** + * Sends the PDM driver a power off notification. + * + * @param pDrvIns Driver instance to notify. + */ +static void audioTestDrvNotifyPowerOff(PPDMDRVINS pDrvIns) +{ + if (pDrvIns) + { + Assert(pDrvIns->u32Version == PDM_DRVINS_VERSION); + if (pDrvIns->pReg->pfnPowerOff) + pDrvIns->pReg->pfnPowerOff(pDrvIns); + } +} + + +/** + * Deletes a driver stack. + * + * This will power off and destroy the drivers. + */ +void audioTestDriverStackDelete(PAUDIOTESTDRVSTACK pDrvStack) +{ + /* + * Do power off notifications (top to bottom). + */ + audioTestDrvNotifyPowerOff(pDrvStack->pDrvAudioIns); + audioTestDrvNotifyPowerOff(pDrvStack->pDrvBackendIns); + + /* + * Drivers are destroyed from bottom to top (closest to the device). + */ + audioTestDrvDestruct(pDrvStack->pDrvBackendIns); + pDrvStack->pDrvBackendIns = NULL; + pDrvStack->pIHostAudio = NULL; + + audioTestDrvDestruct(pDrvStack->pDrvAudioIns); + pDrvStack->pDrvAudioIns = NULL; + pDrvStack->pIAudioConnector = NULL; + + PDMAudioHostEnumDelete(&pDrvStack->DevEnum); +} + + +/** + * Initializes a driver stack, extended version. + * + * @returns VBox status code. + * @param pDrvStack The driver stack to initialize. + * @param pDrvReg The backend driver to use. + * @param fEnabledIn Whether input is enabled or not on creation time. + * @param fEnabledOut Whether output is enabled or not on creation time. + * @param fWithDrvAudio Whether to include DrvAudio in the stack or not. + */ +int audioTestDriverStackInitEx(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio) +{ + int rc; + + RT_ZERO(*pDrvStack); + pDrvStack->pDrvReg = pDrvReg; + + PDMAudioHostEnumInit(&pDrvStack->DevEnum); + + if (!fWithDrvAudio) + rc = audioTestDrvConstruct(pDrvStack, pDrvReg, NULL /*pParentDrvIns*/, &pDrvStack->pDrvBackendIns); + else + { + rc = audioTestDrvConstruct(pDrvStack, &g_DrvAUDIO, NULL /*pParentDrvIns*/, &pDrvStack->pDrvAudioIns); + if (RT_SUCCESS(rc)) + { + Assert(pDrvStack->pDrvAudioIns); + PPDMIBASE const pIBase = &pDrvStack->pDrvAudioIns->IBase; + pDrvStack->pIAudioConnector = (PPDMIAUDIOCONNECTOR)pIBase->pfnQueryInterface(pIBase, PDMIAUDIOCONNECTOR_IID); + if (pDrvStack->pIAudioConnector) + { + /* Both input and output is disabled by default. */ + if (fEnabledIn) + rc = pDrvStack->pIAudioConnector->pfnEnable(pDrvStack->pIAudioConnector, PDMAUDIODIR_IN, true); + + if (RT_SUCCESS(rc)) + { + if (fEnabledOut) + rc = pDrvStack->pIAudioConnector->pfnEnable(pDrvStack->pIAudioConnector, PDMAUDIODIR_OUT, true); + } + + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "Failed to enabled input and output: %Rrc", rc); + audioTestDriverStackDelete(pDrvStack); + } + } + else + { + RTTestFailed(g_hTest, "Failed to query PDMIAUDIOCONNECTOR"); + audioTestDriverStackDelete(pDrvStack); + rc = VERR_PDM_MISSING_INTERFACE; + } + } + } + + /* + * Get the IHostAudio interface and check that the host driver is working. + */ + if (RT_SUCCESS(rc)) + { + PPDMIBASE const pIBase = &pDrvStack->pDrvBackendIns->IBase; + pDrvStack->pIHostAudio = (PPDMIHOSTAUDIO)pIBase->pfnQueryInterface(pIBase, PDMIHOSTAUDIO_IID); + if (pDrvStack->pIHostAudio) + { + PDMAUDIOBACKENDSTS enmStatus = pDrvStack->pIHostAudio->pfnGetStatus(pDrvStack->pIHostAudio, PDMAUDIODIR_OUT); + if (enmStatus == PDMAUDIOBACKENDSTS_RUNNING) + return VINF_SUCCESS; + + RTTestFailed(g_hTest, "Expected backend status RUNNING, got %d instead", enmStatus); + } + else + RTTestFailed(g_hTest, "Failed to query PDMIHOSTAUDIO for '%s'", pDrvReg->szName); + audioTestDriverStackDelete(pDrvStack); + } + + return rc; +} + + +/** + * Initializes a driver stack. + * + * @returns VBox status code. + * @param pDrvStack The driver stack to initialize. + * @param pDrvReg The backend driver to use. + * @param fEnabledIn Whether input is enabled or not on creation time. + * @param fEnabledOut Whether output is enabled or not on creation time. + * @param fWithDrvAudio Whether to include DrvAudio in the stack or not. + */ +int audioTestDriverStackInit(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fWithDrvAudio) +{ + return audioTestDriverStackInitEx(pDrvStack, pDrvReg, true /* fEnabledIn */, true /* fEnabledOut */, fWithDrvAudio); +} + +/** + * Initializes a driver stack by probing all backends in the order of appearance + * in the backends description table. + * + * @returns VBox status code. + * @param pDrvStack The driver stack to initialize. + * @param pDrvReg The backend driver to use. + * @param fEnabledIn Whether input is enabled or not on creation time. + * @param fEnabledOut Whether output is enabled or not on creation time. + * @param fWithDrvAudio Whether to include DrvAudio in the stack or not. + */ +int audioTestDriverStackProbe(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio) +{ + int rc = VERR_IPE_UNINITIALIZED_STATUS; /* Shut up MSVC. */ + + for (size_t i = 0; i < g_cBackends; i++) + { + pDrvReg = g_aBackends[i].pDrvReg; + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing for backend '%s' ...\n", g_aBackends[i].pszName); + + rc = audioTestDriverStackInitEx(pDrvStack, pDrvReg, fEnabledIn, fEnabledOut, fWithDrvAudio); /** @todo Make in/out configurable, too. */ + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing backend '%s' successful\n", g_aBackends[i].pszName); + return rc; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing backend '%s' failed with %Rrc, trying next one\n", + g_aBackends[i].pszName, rc); + continue; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Probing all backends failed\n"); + return rc; +} + +/** + * Wrapper around PDMIHOSTAUDIO::pfnSetDevice. + */ +int audioTestDriverStackSetDevice(PAUDIOTESTDRVSTACK pDrvStack, PDMAUDIODIR enmDir, const char *pszDevId) +{ + int rc; + if ( pDrvStack->pIHostAudio + && pDrvStack->pIHostAudio->pfnSetDevice) + rc = pDrvStack->pIHostAudio->pfnSetDevice(pDrvStack->pIHostAudio, enmDir, pszDevId); + else if (!pszDevId || *pszDevId) + rc = VINF_SUCCESS; + else + rc = VERR_INVALID_FUNCTION; + return rc; +} + + +/** + * Common stream creation code. + * + * @returns VBox status code. + * @param pDrvStack The audio driver stack to create it via. + * @param pCfgReq The requested config. + * @param ppStream Where to return the stream pointer on success. + * @param pCfgAcq Where to return the actual (well, not necessarily when + * using DrvAudio, but probably the same) stream config on + * success (not used as input). + */ +static int audioTestDriverStackStreamCreate(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOSTREAMCFG pCfgReq, + PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + char szTmp[PDMAUDIOSTRMCFGTOSTRING_MAX + 16]; + int rc; + *ppStream = NULL; + + if (pDrvStack->pIAudioConnector) + { + /* + * DrvAudio does most of the work here. + */ + rc = pDrvStack->pIAudioConnector->pfnStreamCreate(pDrvStack->pIAudioConnector, 0 /*fFlags*/, pCfgReq, ppStream); + if (RT_SUCCESS(rc)) + { + *pCfgAcq = (*ppStream)->Cfg; + RTMsgInfo("Created backend stream: %s\n", PDMAudioStrmCfgToString(pCfgReq, szTmp, sizeof(szTmp))); + return rc; + } + /* else: Don't set RTTestFailed(...) here, as test boxes (servers) don't have any audio hardware. + * Caller has check the rc then. */ + } + else + { + /* + * Get the config so we can see how big the PDMAUDIOBACKENDSTREAM + * structure actually is for this backend. + */ + PDMAUDIOBACKENDCFG BackendCfg; + rc = pDrvStack->pIHostAudio->pfnGetConfig(pDrvStack->pIHostAudio, &BackendCfg); + if (RT_SUCCESS(rc)) + { + if (BackendCfg.cbStream >= sizeof(PDMAUDIOBACKENDSTREAM)) + { + /* + * Allocate and initialize the stream. + */ + uint32_t const cbStream = sizeof(AUDIOTESTDRVSTACKSTREAM) - sizeof(PDMAUDIOBACKENDSTREAM) + BackendCfg.cbStream; + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)RTMemAllocZVar(cbStream); + if (pStreamAt) + { + pStreamAt->Core.uMagic = PDMAUDIOSTREAM_MAGIC; + pStreamAt->Core.Cfg = *pCfgReq; + pStreamAt->Core.cbBackend = cbStream; + + pStreamAt->Backend.uMagic = PDMAUDIOBACKENDSTREAM_MAGIC; + pStreamAt->Backend.pStream = &pStreamAt->Core; + + /* + * Call the backend to create the stream. + */ + rc = pDrvStack->pIHostAudio->pfnStreamCreate(pDrvStack->pIHostAudio, &pStreamAt->Backend, + pCfgReq, &pStreamAt->Core.Cfg); + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 1) + RTMsgInfo("Created backend stream: %s\n", + PDMAudioStrmCfgToString(&pStreamAt->Core.Cfg, szTmp, sizeof(szTmp))); + + /* Return if stream is ready: */ + if (rc == VINF_SUCCESS) + { + *ppStream = &pStreamAt->Core; + *pCfgAcq = pStreamAt->Core.Cfg; + return VINF_SUCCESS; + } + if (rc == VINF_AUDIO_STREAM_ASYNC_INIT_NEEDED) + { + /* + * Do async init right here and now. + */ + rc = pDrvStack->pIHostAudio->pfnStreamInitAsync(pDrvStack->pIHostAudio, &pStreamAt->Backend, + false /*fDestroyed*/); + if (RT_SUCCESS(rc)) + { + *ppStream = &pStreamAt->Core; + *pCfgAcq = pStreamAt->Core.Cfg; + return VINF_SUCCESS; + } + + RTTestFailed(g_hTest, "pfnStreamInitAsync failed: %Rrc\n", rc); + } + else + { + RTTestFailed(g_hTest, "pfnStreamCreate returned unexpected info status: %Rrc", rc); + rc = VERR_IPE_UNEXPECTED_INFO_STATUS; + } + pDrvStack->pIHostAudio->pfnStreamDestroy(pDrvStack->pIHostAudio, &pStreamAt->Backend, true /*fImmediate*/); + } + /* else: Don't set RTTestFailed(...) here, as test boxes (servers) don't have any audio hardware. + * Caller has check the rc then. */ + } + else + { + RTTestFailed(g_hTest, "Out of memory!\n"); + rc = VERR_NO_MEMORY; + } + RTMemFree(pStreamAt); + } + else + { + RTTestFailed(g_hTest, "cbStream=%#x is too small, min %#zx!\n", BackendCfg.cbStream, sizeof(PDMAUDIOBACKENDSTREAM)); + rc = VERR_OUT_OF_RANGE; + } + } + else + RTTestFailed(g_hTest, "pfnGetConfig failed: %Rrc\n", rc); + } + return rc; +} + + +/** + * Creates an output stream. + * + * @returns VBox status code. + * @param pDrvStack The audio driver stack to create it via. + * @param pProps The audio properties to use. + * @param cMsBufferSize The buffer size in milliseconds. + * @param cMsPreBuffer The pre-buffering amount in milliseconds. + * @param cMsSchedulingHint The scheduling hint in milliseconds. + * @param ppStream Where to return the stream pointer on success. + * @param pCfgAcq Where to return the actual (well, not + * necessarily when using DrvAudio, but probably + * the same) stream config on success (not used as + * input). + */ +int audioTestDriverStackStreamCreateOutput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps, + uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint, + PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + /* + * Calculate the stream config. + */ + PDMAUDIOSTREAMCFG CfgReq; + int rc = PDMAudioStrmCfgInitWithProps(&CfgReq, pProps); + AssertRC(rc); + CfgReq.enmDir = PDMAUDIODIR_OUT; + CfgReq.enmPath = PDMAUDIOPATH_OUT_FRONT; + CfgReq.Device.cMsSchedulingHint = cMsSchedulingHint == UINT32_MAX || cMsSchedulingHint == 0 + ? 10 : cMsSchedulingHint; + if (pDrvStack->pIAudioConnector && (cMsBufferSize == UINT32_MAX || cMsBufferSize == 0)) + CfgReq.Backend.cFramesBufferSize = 0; /* DrvAudio picks the default */ + else + CfgReq.Backend.cFramesBufferSize = PDMAudioPropsMilliToFrames(pProps, + cMsBufferSize == UINT32_MAX || cMsBufferSize == 0 + ? 300 : cMsBufferSize); + if (cMsPreBuffer == UINT32_MAX) + CfgReq.Backend.cFramesPreBuffering = pDrvStack->pIAudioConnector ? UINT32_MAX /*DrvAudo picks the default */ + : CfgReq.Backend.cFramesBufferSize * 2 / 3; + else + CfgReq.Backend.cFramesPreBuffering = PDMAudioPropsMilliToFrames(pProps, cMsPreBuffer); + if ( CfgReq.Backend.cFramesPreBuffering >= CfgReq.Backend.cFramesBufferSize + 16 + && !pDrvStack->pIAudioConnector /*DrvAudio deals with it*/ ) + { + RTMsgWarning("Cannot pre-buffer %#x frames with only %#x frames of buffer!", + CfgReq.Backend.cFramesPreBuffering, CfgReq.Backend.cFramesBufferSize); + CfgReq.Backend.cFramesPreBuffering = CfgReq.Backend.cFramesBufferSize > 16 + ? CfgReq.Backend.cFramesBufferSize - 16 : 0; + } + + static uint32_t s_idxStream = 0; + uint32_t const idxStream = s_idxStream++; + RTStrPrintf(CfgReq.szName, sizeof(CfgReq.szName), "out-%u", idxStream); + + /* + * Call common code to do the actual work. + */ + return audioTestDriverStackStreamCreate(pDrvStack, &CfgReq, ppStream, pCfgAcq); +} + + +/** + * Creates an input stream. + * + * @returns VBox status code. + * @param pDrvStack The audio driver stack to create it via. + * @param pProps The audio properties to use. + * @param cMsBufferSize The buffer size in milliseconds. + * @param cMsPreBuffer The pre-buffering amount in milliseconds. + * @param cMsSchedulingHint The scheduling hint in milliseconds. + * @param ppStream Where to return the stream pointer on success. + * @param pCfgAcq Where to return the actual (well, not + * necessarily when using DrvAudio, but probably + * the same) stream config on success (not used as + * input). + */ +int audioTestDriverStackStreamCreateInput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps, + uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint, + PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq) +{ + /* + * Calculate the stream config. + */ + PDMAUDIOSTREAMCFG CfgReq; + int rc = PDMAudioStrmCfgInitWithProps(&CfgReq, pProps); + AssertRC(rc); + CfgReq.enmDir = PDMAUDIODIR_IN; + CfgReq.enmPath = PDMAUDIOPATH_IN_LINE; + CfgReq.Device.cMsSchedulingHint = cMsSchedulingHint == UINT32_MAX || cMsSchedulingHint == 0 + ? 10 : cMsSchedulingHint; + if (pDrvStack->pIAudioConnector && (cMsBufferSize == UINT32_MAX || cMsBufferSize == 0)) + CfgReq.Backend.cFramesBufferSize = 0; /* DrvAudio picks the default */ + else + CfgReq.Backend.cFramesBufferSize = PDMAudioPropsMilliToFrames(pProps, + cMsBufferSize == UINT32_MAX || cMsBufferSize == 0 + ? 300 : cMsBufferSize); + if (cMsPreBuffer == UINT32_MAX) + CfgReq.Backend.cFramesPreBuffering = pDrvStack->pIAudioConnector ? UINT32_MAX /*DrvAudio picks the default */ + : CfgReq.Backend.cFramesBufferSize / 2; + else + CfgReq.Backend.cFramesPreBuffering = PDMAudioPropsMilliToFrames(pProps, cMsPreBuffer); + if ( CfgReq.Backend.cFramesPreBuffering >= CfgReq.Backend.cFramesBufferSize + 16 /** @todo way to little */ + && !pDrvStack->pIAudioConnector /*DrvAudio deals with it*/ ) + { + RTMsgWarning("Cannot pre-buffer %#x frames with only %#x frames of buffer!", + CfgReq.Backend.cFramesPreBuffering, CfgReq.Backend.cFramesBufferSize); + CfgReq.Backend.cFramesPreBuffering = CfgReq.Backend.cFramesBufferSize > 16 + ? CfgReq.Backend.cFramesBufferSize - 16 : 0; + } + + static uint32_t s_idxStream = 0; + uint32_t const idxStream = s_idxStream++; + RTStrPrintf(CfgReq.szName, sizeof(CfgReq.szName), "in-%u", idxStream); + + /* + * Call common code to do the actual work. + */ + return audioTestDriverStackStreamCreate(pDrvStack, &CfgReq, ppStream, pCfgAcq); +} + + +/** + * Destroys a stream. + * + * @param pDrvStack Driver stack the stream to destroy is assigned to. + * @param pStream Stream to destroy. Pointer will be NULL (invalid) after successful return. + */ +void audioTestDriverStackStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream) +{ + if (!pStream) + return; + + if (pDrvStack->pIAudioConnector) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Destroying stream '%s' (IAudioConnector) ...\n", pStream->Cfg.szName); + int rc = pDrvStack->pIAudioConnector->pfnStreamDestroy(pDrvStack->pIAudioConnector, pStream, true /*fImmediate*/); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "pfnStreamDestroy failed: %Rrc", rc); + } + else + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Destroying stream '%s' (IHostAudio) ...\n", pStream->Cfg.szName); + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + int rc = pDrvStack->pIHostAudio->pfnStreamDestroy(pDrvStack->pIHostAudio, &pStreamAt->Backend, true /*fImmediate*/); + if (RT_SUCCESS(rc)) + { + pStreamAt->Core.uMagic = ~PDMAUDIOSTREAM_MAGIC; + pStreamAt->Backend.uMagic = ~PDMAUDIOBACKENDSTREAM_MAGIC; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Destroying stream '%s' done\n", pStream->Cfg.szName); + + RTMemFree(pStreamAt); + + pStreamAt = NULL; + pStream = NULL; + } + else + RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamDestroy failed: %Rrc", rc); + } +} + + +/** + * Enables a stream. + */ +int audioTestDriverStackStreamEnable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream) +{ + int rc; + if (pDrvStack->pIAudioConnector) + { + rc = pDrvStack->pIAudioConnector->pfnStreamControl(pDrvStack->pIAudioConnector, pStream, PDMAUDIOSTREAMCMD_ENABLE); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "pfnStreamControl/ENABLE failed: %Rrc", rc); + } + else + { + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + rc = pDrvStack->pIHostAudio->pfnStreamEnable(pDrvStack->pIHostAudio, &pStreamAt->Backend); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamEnable failed: %Rrc", rc); + } + return rc; +} + + +/** + * Disables a stream. + */ +int AudioTestDriverStackStreamDisable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream) +{ + int rc; + if (pDrvStack->pIAudioConnector) + { + rc = pDrvStack->pIAudioConnector->pfnStreamControl(pDrvStack->pIAudioConnector, pStream, PDMAUDIOSTREAMCMD_DISABLE); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "pfnStreamControl/DISABLE failed: %Rrc", rc); + } + else + { + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + rc = pDrvStack->pIHostAudio->pfnStreamDisable(pDrvStack->pIHostAudio, &pStreamAt->Backend); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamDisable failed: %Rrc", rc); + } + return rc; +} + + +/** + * Drains an output stream. + */ +int audioTestDriverStackStreamDrain(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, bool fSync) +{ + int rc; + if (pDrvStack->pIAudioConnector) + { + /* + * Issue the drain request. + */ + rc = pDrvStack->pIAudioConnector->pfnStreamControl(pDrvStack->pIAudioConnector, pStream, PDMAUDIOSTREAMCMD_DRAIN); + if (RT_SUCCESS(rc) && fSync) + { + /* + * This is a synchronous drain, so wait for the driver to change state to inactive. + */ + PDMAUDIOSTREAMSTATE enmState; + while ( (enmState = pDrvStack->pIAudioConnector->pfnStreamGetState(pDrvStack->pIAudioConnector, pStream)) + >= PDMAUDIOSTREAMSTATE_ENABLED) + { + RTThreadSleep(2); + rc = pDrvStack->pIAudioConnector->pfnStreamIterate(pDrvStack->pIAudioConnector, pStream); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "pfnStreamIterate/DRAIN failed: %Rrc", rc); + break; + } + } + if (enmState != PDMAUDIOSTREAMSTATE_INACTIVE) + { + RTTestFailed(g_hTest, "Stream state not INACTIVE after draining: %s", PDMAudioStreamStateGetName(enmState)); + rc = VERR_AUDIO_STREAM_NOT_READY; + } + } + else if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "pfnStreamControl/ENABLE failed: %Rrc", rc); + } + else + { + /* + * Issue the drain request. + */ + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + rc = pDrvStack->pIHostAudio->pfnStreamDrain(pDrvStack->pIHostAudio, &pStreamAt->Backend); + if (RT_SUCCESS(rc) && fSync) + { + RTMSINTERVAL const msTimeout = RT_MS_5MIN; /* 5 minutes should be really enough for draining our stuff. */ + uint64_t const tsStart = RTTimeMilliTS(); + + /* + * This is a synchronous drain, so wait for the driver to change state to inactive. + */ + PDMHOSTAUDIOSTREAMSTATE enmHostState; + while ( (enmHostState = pDrvStack->pIHostAudio->pfnStreamGetState(pDrvStack->pIHostAudio, &pStreamAt->Backend)) + == PDMHOSTAUDIOSTREAMSTATE_DRAINING) + { + RTThreadSleep(2); + uint32_t cbWritten = UINT32_MAX; + rc = pDrvStack->pIHostAudio->pfnStreamPlay(pDrvStack->pIHostAudio, &pStreamAt->Backend, + NULL /*pvBuf*/, 0 /*cbBuf*/, &cbWritten); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "pfnStreamPlay/DRAIN failed: %Rrc", rc); + break; + } + if (cbWritten != 0) + { + RTTestFailed(g_hTest, "pfnStreamPlay/DRAIN did not set cbWritten to zero: %#x", cbWritten); + rc = VERR_MISSING; + break; + } + + /* Fail-safe for audio stacks and/or implementations which mess up draining. + * + * Note: On some testboxes draining never seems to finish and thus is getting aborted, no clue why. + * The test result in the end still could be correct, although the actual draining problem + * needs to be investigated further. + * + * So don't make this (and the stream state check below) an error for now and just warn about it. + * + ** @todo Investigate draining issues on testboxes. + */ + if (RTTimeMilliTS() - tsStart > msTimeout) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Warning: Draining stream took too long (timeout is %RU32ms), giving up", msTimeout); + break; + } + } + if (enmHostState != PDMHOSTAUDIOSTREAMSTATE_OKAY) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Warning: Stream state not OKAY after draining: %s", PDMHostAudioStreamStateGetName(enmHostState)); + } + else if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamControl/ENABLE failed: %Rrc", rc); + } + return rc; +} + + +/** + * Checks if the stream is okay. + * @returns true if okay, false if not. + */ +bool audioTestDriverStackStreamIsOkay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream) +{ + /* + * Get the stream status and check if it means is okay or not. + */ + bool fRc = false; + if (pDrvStack->pIAudioConnector) + { + PDMAUDIOSTREAMSTATE enmState = pDrvStack->pIAudioConnector->pfnStreamGetState(pDrvStack->pIAudioConnector, pStream); + switch (enmState) + { + case PDMAUDIOSTREAMSTATE_NOT_WORKING: + case PDMAUDIOSTREAMSTATE_NEED_REINIT: + break; + case PDMAUDIOSTREAMSTATE_INACTIVE: + case PDMAUDIOSTREAMSTATE_ENABLED: + case PDMAUDIOSTREAMSTATE_ENABLED_READABLE: + case PDMAUDIOSTREAMSTATE_ENABLED_WRITABLE: + fRc = true; + break; + /* no default */ + case PDMAUDIOSTREAMSTATE_INVALID: + case PDMAUDIOSTREAMSTATE_END: + case PDMAUDIOSTREAMSTATE_32BIT_HACK: + break; + } + } + else + { + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + PDMHOSTAUDIOSTREAMSTATE enmHostState = pDrvStack->pIHostAudio->pfnStreamGetState(pDrvStack->pIHostAudio, + &pStreamAt->Backend); + switch (enmHostState) + { + case PDMHOSTAUDIOSTREAMSTATE_INITIALIZING: + case PDMHOSTAUDIOSTREAMSTATE_NOT_WORKING: + break; + case PDMHOSTAUDIOSTREAMSTATE_OKAY: + case PDMHOSTAUDIOSTREAMSTATE_DRAINING: + case PDMHOSTAUDIOSTREAMSTATE_INACTIVE: + fRc = true; + break; + /* no default */ + case PDMHOSTAUDIOSTREAMSTATE_INVALID: + case PDMHOSTAUDIOSTREAMSTATE_END: + case PDMHOSTAUDIOSTREAMSTATE_32BIT_HACK: + break; + } + } + return fRc; +} + + +/** + * Gets the number of bytes it's currently possible to write to the stream. + */ +uint32_t audioTestDriverStackStreamGetWritable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream) +{ + uint32_t cbWritable; + if (pDrvStack->pIAudioConnector) + cbWritable = pDrvStack->pIAudioConnector->pfnStreamGetWritable(pDrvStack->pIAudioConnector, pStream); + else + { + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + cbWritable = pDrvStack->pIHostAudio->pfnStreamGetWritable(pDrvStack->pIHostAudio, &pStreamAt->Backend); + } + return cbWritable; +} + + +/** + * Tries to play the @a cbBuf bytes of samples in @a pvBuf. + */ +int audioTestDriverStackStreamPlay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, + void const *pvBuf, uint32_t cbBuf, uint32_t *pcbPlayed) +{ + int rc; + if (pDrvStack->pIAudioConnector) + { + rc = pDrvStack->pIAudioConnector->pfnStreamPlay(pDrvStack->pIAudioConnector, pStream, pvBuf, cbBuf, pcbPlayed); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "pfnStreamPlay(,,,%#x,) failed: %Rrc", cbBuf, rc); + } + else + { + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + rc = pDrvStack->pIHostAudio->pfnStreamPlay(pDrvStack->pIHostAudio, &pStreamAt->Backend, pvBuf, cbBuf, pcbPlayed); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamPlay(,,,%#x,) failed: %Rrc", cbBuf, rc); + } + return rc; +} + + +/** + * Gets the number of bytes it's currently possible to write to the stream. + */ +uint32_t audioTestDriverStackStreamGetReadable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream) +{ + uint32_t cbReadable; + if (pDrvStack->pIAudioConnector) + cbReadable = pDrvStack->pIAudioConnector->pfnStreamGetReadable(pDrvStack->pIAudioConnector, pStream); + else + { + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + cbReadable = pDrvStack->pIHostAudio->pfnStreamGetReadable(pDrvStack->pIHostAudio, &pStreamAt->Backend); + } + return cbReadable; +} + + +/** + * Tries to capture @a cbBuf bytes of samples in @a pvBuf. + */ +int audioTestDriverStackStreamCapture(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured) +{ + int rc; + if (pDrvStack->pIAudioConnector) + { + rc = pDrvStack->pIAudioConnector->pfnStreamCapture(pDrvStack->pIAudioConnector, pStream, pvBuf, cbBuf, pcbCaptured); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "pfnStreamCapture(,,,%#x,) failed: %Rrc", cbBuf, rc); + } + else + { + PAUDIOTESTDRVSTACKSTREAM pStreamAt = (PAUDIOTESTDRVSTACKSTREAM)pStream; + rc = pDrvStack->pIHostAudio->pfnStreamCapture(pDrvStack->pIHostAudio, &pStreamAt->Backend, pvBuf, cbBuf, pcbCaptured); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "PDMIHOSTAUDIO::pfnStreamCapture(,,,%#x,) failed: %Rrc", cbBuf, rc); + } + return rc; +} + + +/********************************************************************************************************************************* +* Mixed streams * +*********************************************************************************************************************************/ + +/** + * Initializing mixing for a stream. + * + * This can be used as a do-nothing wrapper for the stack. + * + * @returns VBox status code. + * @param pMix The mixing state. + * @param pStream The stream to mix to/from. + * @param pProps The mixer properties. Pass NULL for no mixing, just + * wrap the driver stack functionality. + * @param cMsBuffer The buffer size. + */ +int AudioTestMixStreamInit(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, + PCPDMAUDIOPCMPROPS pProps, uint32_t cMsBuffer) +{ + RT_ZERO(*pMix); + + AssertReturn(pDrvStack, VERR_INVALID_PARAMETER); + AssertReturn(pStream, VERR_INVALID_PARAMETER); + + pMix->pDrvStack = pDrvStack; + pMix->pStream = pStream; + if (!pProps) + { + pMix->pProps = &pStream->Cfg.Props; + return VINF_SUCCESS; + } + + /* + * Okay, we're doing mixing so we need to set up the mixer buffer + * and associated states. + */ + pMix->fDoMixing = true; + int rc = AudioMixBufInit(&pMix->MixBuf, "mixer", pProps, PDMAudioPropsMilliToFrames(pProps, cMsBuffer)); + if (RT_SUCCESS(rc)) + { + pMix->pProps = &pMix->MixBuf.Props; + + if (pStream->Cfg.enmDir == PDMAUDIODIR_IN) + { + rc = AudioMixBufInitPeekState(&pMix->MixBuf, &pMix->PeekState, &pMix->MixBuf.Props); + if (RT_SUCCESS(rc)) + { + rc = AudioMixBufInitWriteState(&pMix->MixBuf, &pMix->WriteState, &pStream->Cfg.Props); + if (RT_SUCCESS(rc)) + return rc; + } + } + else if (pStream->Cfg.enmDir == PDMAUDIODIR_OUT) + { + rc = AudioMixBufInitWriteState(&pMix->MixBuf, &pMix->WriteState, &pMix->MixBuf.Props); + if (RT_SUCCESS(rc)) + { + rc = AudioMixBufInitPeekState(&pMix->MixBuf, &pMix->PeekState, &pStream->Cfg.Props); + if (RT_SUCCESS(rc)) + return rc; + } + } + else + { + RTTestFailed(g_hTest, "Bogus stream direction!"); + rc = VERR_INVALID_STATE; + } + } + else + RTTestFailed(g_hTest, "AudioMixBufInit failed: %Rrc", rc); + RT_ZERO(*pMix); + return rc; +} + + +/** + * Terminate mixing (leaves the stream untouched). + * + * @param pMix The mixing state. + */ +void AudioTestMixStreamTerm(PAUDIOTESTDRVMIXSTREAM pMix) +{ + if (pMix->fDoMixing) + { + AudioMixBufTerm(&pMix->MixBuf); + pMix->pStream = NULL; + } + RT_ZERO(*pMix); +} + + +/** + * Worker that transports data between the mixer buffer and the drivers. + * + * @returns VBox status code. + * @param pMix The mixer stream setup to do transfers for. + */ +static int audioTestMixStreamTransfer(PAUDIOTESTDRVMIXSTREAM pMix) +{ + uint8_t abBuf[16384]; + if (pMix->pStream->Cfg.enmDir == PDMAUDIODIR_IN) + { + /* + * Try fill up the mixer buffer as much as possible. + * + * Slight fun part is that we have to calculate conversion + * ratio and be rather pessimistic about it. + */ + uint32_t const cbBuf = PDMAudioPropsFloorBytesToFrame(&pMix->pStream->Cfg.Props, sizeof(abBuf)); + for (;;) + { + /* + * Figure out how much we can move in this iteration. + */ + uint32_t cDstFrames = AudioMixBufFree(&pMix->MixBuf); + if (!cDstFrames) + break; + + uint32_t cbReadable = audioTestDriverStackStreamGetReadable(pMix->pDrvStack, pMix->pStream); + if (!cbReadable) + break; + + uint32_t cbToRead; + if (PDMAudioPropsHz(&pMix->pStream->Cfg.Props) == PDMAudioPropsHz(&pMix->MixBuf.Props)) + cbToRead = PDMAudioPropsFramesToBytes(&pMix->pStream->Cfg.Props, cDstFrames); + else + cbToRead = PDMAudioPropsFramesToBytes(&pMix->pStream->Cfg.Props, + (uint64_t)cDstFrames * PDMAudioPropsHz(&pMix->pStream->Cfg.Props) + / PDMAudioPropsHz(&pMix->MixBuf.Props)); + cbToRead = RT_MIN(cbToRead, RT_MIN(cbReadable, cbBuf)); + if (!cbToRead) + break; + + /* + * Get the data. + */ + uint32_t cbCaptured = 0; + int rc = audioTestDriverStackStreamCapture(pMix->pDrvStack, pMix->pStream, abBuf, cbToRead, &cbCaptured); + if (RT_FAILURE(rc)) + return rc; + Assert(cbCaptured == cbToRead); + AssertBreak(cbCaptured > 0); + + /* + * Feed it to the mixer. + */ + uint32_t cDstFramesWritten = 0; + if ((abBuf[0] >> 4) & 1) /* some cheap random */ + AudioMixBufWrite(&pMix->MixBuf, &pMix->WriteState, abBuf, cbCaptured, + 0 /*offDstFrame*/, cDstFrames, &cDstFramesWritten); + else + { + AudioMixBufSilence(&pMix->MixBuf, &pMix->WriteState, 0 /*offFrame*/, cDstFrames); + AudioMixBufBlend(&pMix->MixBuf, &pMix->WriteState, abBuf, cbCaptured, + 0 /*offDstFrame*/, cDstFrames, &cDstFramesWritten); + } + AudioMixBufCommit(&pMix->MixBuf, cDstFramesWritten); + } + } + else + { + /* + * The goal here is to empty the mixer buffer by transfering all + * the data to the drivers. + */ + uint32_t const cbBuf = PDMAudioPropsFloorBytesToFrame(&pMix->MixBuf.Props, sizeof(abBuf)); + for (;;) + { + uint32_t cFrames = AudioMixBufUsed(&pMix->MixBuf); + if (!cFrames) + break; + + uint32_t cbWritable = audioTestDriverStackStreamGetWritable(pMix->pDrvStack, pMix->pStream); + if (!cbWritable) + break; + + uint32_t cSrcFramesPeeked; + uint32_t cbDstPeeked; + AudioMixBufPeek(&pMix->MixBuf, 0 /*offSrcFrame*/, cFrames, &cSrcFramesPeeked, + &pMix->PeekState, abBuf, RT_MIN(cbBuf, cbWritable), &cbDstPeeked); + AudioMixBufAdvance(&pMix->MixBuf, cSrcFramesPeeked); + + if (!cbDstPeeked) + break; + + uint32_t offBuf = 0; + while (offBuf < cbDstPeeked) + { + uint32_t cbPlayed = 0; + int rc = audioTestDriverStackStreamPlay(pMix->pDrvStack, pMix->pStream, + &abBuf[offBuf], cbDstPeeked - offBuf, &cbPlayed); + if (RT_FAILURE(rc)) + return rc; + if (!cbPlayed) + RTThreadSleep(1); + offBuf += cbPlayed; + } + } + } + return VINF_SUCCESS; +} + + +/** + * Same as audioTestDriverStackStreamEnable. + */ +int AudioTestMixStreamEnable(PAUDIOTESTDRVMIXSTREAM pMix) +{ + return audioTestDriverStackStreamEnable(pMix->pDrvStack, pMix->pStream); +} + + +/** + * Same as audioTestDriverStackStreamDrain. + */ +int AudioTestMixStreamDrain(PAUDIOTESTDRVMIXSTREAM pMix, bool fSync) +{ + /* + * If we're mixing, we must first make sure the buffer is empty. + */ + if (pMix->fDoMixing) + { + audioTestMixStreamTransfer(pMix); + while (AudioMixBufUsed(&pMix->MixBuf) > 0) + { + RTThreadSleep(1); + audioTestMixStreamTransfer(pMix); + } + } + + /* + * Then we do the regular work. + */ + return audioTestDriverStackStreamDrain(pMix->pDrvStack, pMix->pStream, fSync); +} + +/** + * Same as audioTestDriverStackStreamDisable. + */ +int AudioTestMixStreamDisable(PAUDIOTESTDRVMIXSTREAM pMix) +{ + return AudioTestDriverStackStreamDisable(pMix->pDrvStack, pMix->pStream); +} + + +/** + * Same as audioTestDriverStackStreamIsOkay. + */ +bool AudioTestMixStreamIsOkay(PAUDIOTESTDRVMIXSTREAM pMix) +{ + return audioTestDriverStackStreamIsOkay(pMix->pDrvStack, pMix->pStream); +} + + +/** + * Same as audioTestDriverStackStreamGetWritable + */ +uint32_t AudioTestMixStreamGetWritable(PAUDIOTESTDRVMIXSTREAM pMix) +{ + if (!pMix->fDoMixing) + return audioTestDriverStackStreamGetWritable(pMix->pDrvStack, pMix->pStream); + uint32_t cbRet = AudioMixBufFreeBytes(&pMix->MixBuf); + if (!cbRet) + { + audioTestMixStreamTransfer(pMix); + cbRet = AudioMixBufFreeBytes(&pMix->MixBuf); + } + return cbRet; +} + + + + +/** + * Same as audioTestDriverStackStreamPlay. + */ +int AudioTestMixStreamPlay(PAUDIOTESTDRVMIXSTREAM pMix, void const *pvBuf, uint32_t cbBuf, uint32_t *pcbPlayed) +{ + if (!pMix->fDoMixing) + return audioTestDriverStackStreamPlay(pMix->pDrvStack, pMix->pStream, pvBuf, cbBuf, pcbPlayed); + + *pcbPlayed = 0; + + int rc = audioTestMixStreamTransfer(pMix); + if (RT_FAILURE(rc)) + return rc; + + uint32_t const cbFrame = PDMAudioPropsFrameSize(&pMix->MixBuf.Props); + while (cbBuf >= cbFrame) + { + uint32_t const cFrames = AudioMixBufFree(&pMix->MixBuf); + if (!cFrames) + break; + uint32_t cbToWrite = PDMAudioPropsFramesToBytes(&pMix->MixBuf.Props, cFrames); + cbToWrite = RT_MIN(cbToWrite, cbBuf); + cbToWrite = PDMAudioPropsFloorBytesToFrame(&pMix->MixBuf.Props, cbToWrite); + + uint32_t cFramesWritten = 0; + AudioMixBufWrite(&pMix->MixBuf, &pMix->WriteState, pvBuf, cbToWrite, 0 /*offDstFrame*/, cFrames, &cFramesWritten); + Assert(cFramesWritten == PDMAudioPropsBytesToFrames(&pMix->MixBuf.Props, cbToWrite)); + AudioMixBufCommit(&pMix->MixBuf, cFramesWritten); + + *pcbPlayed += cbToWrite; + cbBuf -= cbToWrite; + pvBuf = (uint8_t const *)pvBuf + cbToWrite; + + rc = audioTestMixStreamTransfer(pMix); + if (RT_FAILURE(rc)) + return *pcbPlayed ? VINF_SUCCESS : rc; + } + + return VINF_SUCCESS; +} + + +/** + * Same as audioTestDriverStackStreamGetReadable + */ +uint32_t AudioTestMixStreamGetReadable(PAUDIOTESTDRVMIXSTREAM pMix) +{ + if (!pMix->fDoMixing) + return audioTestDriverStackStreamGetReadable(pMix->pDrvStack, pMix->pStream); + + audioTestMixStreamTransfer(pMix); + uint32_t cbRet = AudioMixBufUsedBytes(&pMix->MixBuf); + return cbRet; +} + + + + +/** + * Same as audioTestDriverStackStreamCapture. + */ +int AudioTestMixStreamCapture(PAUDIOTESTDRVMIXSTREAM pMix, void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured) +{ + if (!pMix->fDoMixing) + return audioTestDriverStackStreamCapture(pMix->pDrvStack, pMix->pStream, pvBuf, cbBuf, pcbCaptured); + + *pcbCaptured = 0; + + int rc = audioTestMixStreamTransfer(pMix); + if (RT_FAILURE(rc)) + return rc; + + uint32_t const cbFrame = PDMAudioPropsFrameSize(&pMix->MixBuf.Props); + while (cbBuf >= cbFrame) + { + uint32_t const cFrames = AudioMixBufUsed(&pMix->MixBuf); + if (!cFrames) + break; + uint32_t cbToRead = PDMAudioPropsFramesToBytes(&pMix->MixBuf.Props, cFrames); + cbToRead = RT_MIN(cbToRead, cbBuf); + cbToRead = PDMAudioPropsFloorBytesToFrame(&pMix->MixBuf.Props, cbToRead); + + uint32_t cFramesPeeked = 0; + uint32_t cbPeeked = 0; + AudioMixBufPeek(&pMix->MixBuf, 0 /*offSrcFrame*/, cFrames, &cFramesPeeked, &pMix->PeekState, pvBuf, cbToRead, &cbPeeked); + Assert(cFramesPeeked == PDMAudioPropsBytesToFrames(&pMix->MixBuf.Props, cbPeeked)); + AudioMixBufAdvance(&pMix->MixBuf, cFramesPeeked); + + *pcbCaptured += cbToRead; + cbBuf -= cbToRead; + pvBuf = (uint8_t *)pvBuf + cbToRead; + + rc = audioTestMixStreamTransfer(pMix); + if (RT_FAILURE(rc)) + return *pcbCaptured ? VINF_SUCCESS : rc; + } + + return VINF_SUCCESS; +} + +/** + * Sets the volume of a mixing stream. + * + * @param pMix Mixing stream to set volume for. + * @param uVolumePercent Volume to set (in percent, 0-100). + */ +void AudioTestMixStreamSetVolume(PAUDIOTESTDRVMIXSTREAM pMix, uint8_t uVolumePercent) +{ + AssertReturnVoid(pMix->fDoMixing); + + uint8_t const uVol = (PDMAUDIO_VOLUME_MAX / 100) * uVolumePercent; + + PDMAUDIOVOLUME Vol; + RT_ZERO(Vol); + for (size_t i = 0; i < RT_ELEMENTS(Vol.auChannels); i++) + Vol.auChannels[i] = uVol; + AudioMixBufSetVolume(&pMix->MixBuf, &Vol); +} + diff --git a/src/VBox/ValidationKit/utils/audio/vkatInternal.h b/src/VBox/ValidationKit/utils/audio/vkatInternal.h new file mode 100644 index 00000000..aed2ff6c --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/vkatInternal.h @@ -0,0 +1,547 @@ +/* $Id: vkatInternal.h $ */ +/** @file + * VKAT - Internal header file for common definitions + structs. + */ + +/* + * Copyright (C) 2021-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_audio_vkatInternal_h +#define VBOX_INCLUDED_SRC_audio_vkatInternal_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/getopt.h> + +#include <VBox/vmm/pdmdrv.h> +#include <VBox/vmm/pdmaudioinline.h> +#include <VBox/vmm/pdmaudiohostenuminline.h> + +#include "Audio/AudioMixBuffer.h" +#include "Audio/AudioTest.h" +#include "Audio/AudioTestService.h" +#include "Audio/AudioTestServiceClient.h" + +#include "VBoxDD.h" + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Audio driver stack. + * + * This can be just be backend driver alone or DrvAudio with a backend. + * @todo add automatic resampling via mixer so we can test more of the audio + * stack used by the device emulations. + */ +typedef struct AUDIOTESTDRVSTACK +{ + /** The device registration record for the backend. */ + PCPDMDRVREG pDrvReg; + /** The backend driver instance. */ + PPDMDRVINS pDrvBackendIns; + /** The backend's audio interface. */ + PPDMIHOSTAUDIO pIHostAudio; + + /** The DrvAudio instance. */ + PPDMDRVINS pDrvAudioIns; + /** This is NULL if we don't use DrvAudio. */ + PPDMIAUDIOCONNECTOR pIAudioConnector; + + /** The current (last) audio device enumeration to use. */ + PDMAUDIOHOSTENUM DevEnum; +} AUDIOTESTDRVSTACK; +/** Pointer to an audio driver stack. */ +typedef AUDIOTESTDRVSTACK *PAUDIOTESTDRVSTACK; + +/** + * Backend-only stream structure. + */ +typedef struct AUDIOTESTDRVSTACKSTREAM +{ + /** The public stream data. */ + PDMAUDIOSTREAM Core; + /** The backend data (variable size). */ + PDMAUDIOBACKENDSTREAM Backend; +} AUDIOTESTDRVSTACKSTREAM; +/** Pointer to a backend-only stream structure. */ +typedef AUDIOTESTDRVSTACKSTREAM *PAUDIOTESTDRVSTACKSTREAM; + +/** + * Mixer setup for a stream. + */ +typedef struct AUDIOTESTDRVMIXSTREAM +{ + /** Pointer to the driver stack. */ + PAUDIOTESTDRVSTACK pDrvStack; + /** Pointer to the stream. */ + PPDMAUDIOSTREAM pStream; + /** Properties to use. */ + PCPDMAUDIOPCMPROPS pProps; + /** Set if we're mixing or just passing thru to the driver stack. */ + bool fDoMixing; + /** Mixer buffer. */ + AUDIOMIXBUF MixBuf; + /** Write state. */ + AUDIOMIXBUFWRITESTATE WriteState; + /** Peek state. */ + AUDIOMIXBUFPEEKSTATE PeekState; +} AUDIOTESTDRVMIXSTREAM; +/** Pointer to mixer setup for a stream. */ +typedef AUDIOTESTDRVMIXSTREAM *PAUDIOTESTDRVMIXSTREAM; + +/** + * Enumeration specifying the current audio test mode. + */ +typedef enum AUDIOTESTMODE +{ + /** Unknown mode. */ + AUDIOTESTMODE_UNKNOWN = 0, + /** VKAT is running on the guest side. */ + AUDIOTESTMODE_GUEST, + /** VKAT is running on the host side. */ + AUDIOTESTMODE_HOST +} AUDIOTESTMODE; + +struct AUDIOTESTENV; +/** Pointer a audio test environment. */ +typedef AUDIOTESTENV *PAUDIOTESTENV; + +struct AUDIOTESTDESC; +/** Pointer a audio test descriptor. */ +typedef AUDIOTESTDESC *PAUDIOTESTDESC; + +/** + * Callback to set up the test parameters for a specific test. + * + * @returns IPRT status code. + * @retval VINF_SUCCESS if setting the parameters up succeeded. Any other error code + * otherwise indicating the kind of error. + * @param pszTest Test name. + * @param pTstParmsAcq The audio test parameters to set up. + */ +typedef DECLCALLBACKTYPE(int, FNAUDIOTESTSETUP,(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx)); +/** Pointer to an audio test setup callback. */ +typedef FNAUDIOTESTSETUP *PFNAUDIOTESTSETUP; + +typedef DECLCALLBACKTYPE(int, FNAUDIOTESTEXEC,(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms)); +/** Pointer to an audio test exec callback. */ +typedef FNAUDIOTESTEXEC *PFNAUDIOTESTEXEC; + +typedef DECLCALLBACKTYPE(int, FNAUDIOTESTDESTROY,(PAUDIOTESTENV pTstEnv, void *pvCtx)); +/** Pointer to an audio test destroy callback. */ +typedef FNAUDIOTESTDESTROY *PFNAUDIOTESTDESTROY; + +/** + * Structure for keeping an audio test audio stream. + */ +typedef struct AUDIOTESTSTREAM +{ + /** The PDM stream. */ + PPDMAUDIOSTREAM pStream; + /** The backend stream. */ + PPDMAUDIOBACKENDSTREAM pBackend; + /** The stream config. */ + PDMAUDIOSTREAMCFG Cfg; + /** Associated mixing stream. Optional. */ + AUDIOTESTDRVMIXSTREAM Mix; +} AUDIOTESTSTREAM; +/** Pointer to audio test stream. */ +typedef AUDIOTESTSTREAM *PAUDIOTESTSTREAM; + +/** Maximum audio streams a test environment can handle. */ +#define AUDIOTESTENV_MAX_STREAMS 8 + +/** + * Structure for keeping TCP/IP-specific options. + */ +typedef struct AUDIOTESTENVTCPOPTS +{ + /** Connection mode(s) to use. */ + ATSCONNMODE enmConnMode; + /** Bind address (server mode). When empty, "0.0.0.0" (any host) will be used. */ + char szBindAddr[128]; + /** Bind port (server mode). */ + uint16_t uBindPort; + /** Connection address (client mode). */ + char szConnectAddr[128]; + /** Connection port (client mode). */ + uint16_t uConnectPort; +} AUDIOTESTENVTCPOPTS; +/** Pointer to audio test TCP options. */ +typedef AUDIOTESTENVTCPOPTS *PAUDIOTESTENVTCPOPTS; + +/** + * Structure holding additional I/O options. + */ +typedef struct AUDIOTESTIOOPTS +{ + /** Whether to use the audio connector or not. */ + bool fWithDrvAudio; + /** Whether to use a mixing buffer or not. */ + bool fWithMixer; + /** Buffer size (in ms). */ + uint32_t cMsBufferSize; + /** Pre-buffering size (in ms). */ + uint32_t cMsPreBuffer; + /** Scheduling (in ms). */ + uint32_t cMsSchedulingHint; + /** Audio vlume to use (in percent). */ + uint8_t uVolumePercent; + /** PCM audio properties to use. */ + PDMAUDIOPCMPROPS Props; +} AUDIOTESTIOOPTS; +/** Pointer to additional playback options. */ +typedef AUDIOTESTIOOPTS *PAUDIOTESTIOOPTS; + +/** + * Structure for keeping a user context for the test service callbacks. + */ +typedef struct ATSCALLBACKCTX +{ + /** The test environment bound to this context. */ + PAUDIOTESTENV pTstEnv; + /** Absolute path to the packed up test set archive. + * Keep it simple for now and only support one (open) archive at a time. */ + char szTestSetArchive[RTPATH_MAX]; + /** File handle to the (opened) test set archive for reading. */ + RTFILE hTestSetArchive; + /** Number of currently connected clients. */ + uint8_t cClients; +} ATSCALLBACKCTX; +typedef ATSCALLBACKCTX *PATSCALLBACKCTX; + +/** + * Audio test environment parameters. + * + * This is global to all tests defined. + */ +typedef struct AUDIOTESTENV +{ + /** Audio testing mode. */ + AUDIOTESTMODE enmMode; + /** Whether self test mode is active or not. */ + bool fSelftest; + /** Whether skip the actual verification or not. */ + bool fSkipVerify; + /** Name of the audio device to use. + * If empty the default audio device will be used. */ + char szDev[128]; + /** Zero-based index of current test (will be increased for every run test). */ + uint32_t idxTest; + /** Number of iterations for *all* tests specified. + * When set to 0 (default), a random value (see specific test) will be chosen. */ + uint32_t cIterations; + /** I/O options to use. */ + AUDIOTESTIOOPTS IoOpts; + /** Test tone parameters to use. */ + AUDIOTESTTONEPARMS ToneParms; + /** Output path for storing the test environment's final test files. */ + char szTag[AUDIOTEST_TAG_MAX]; + /** Output path for storing the test environment's final test files. */ + char szPathOut[RTPATH_MAX]; + /** Temporary path for this test environment. */ + char szPathTemp[RTPATH_MAX]; + /** Pointer to audio test driver stack to use. */ + PAUDIOTESTDRVSTACK pDrvStack; + /** Audio stream. */ + AUDIOTESTSTREAM aStreams[AUDIOTESTENV_MAX_STREAMS]; + /** The audio test set to use. */ + AUDIOTESTSET Set; + /** TCP options to use for ATS. */ + AUDIOTESTENVTCPOPTS TcpOpts; + /** ATS server instance to use. + * NULL if not in use. */ + PATSSERVER pSrv; + /** ATS callback context to use. */ + ATSCALLBACKCTX CallbackCtx; + union + { + struct + { + /** Client connected to the ATS on the guest side. */ + ATSCLIENT AtsClGuest; + /** Path to the guest's test set downloaded to the host. */ + char szPathTestSetGuest[RTPATH_MAX]; + /** Client connected to the Validation Kit audio driver ATS. */ + ATSCLIENT AtsClValKit; + /** Path to the Validation Kit audio driver's test set downloaded to the host. */ + char szPathTestSetValKit[RTPATH_MAX]; + } Host; + } u; +} AUDIOTESTENV; + +/** + * Audio test descriptor. + */ +typedef struct AUDIOTESTDESC +{ + /** (Sort of) Descriptive test name. */ + const char *pszName; + /** Flag whether the test is excluded. */ + bool fExcluded; + /** The setup callback. */ + PFNAUDIOTESTSETUP pfnSetup; + /** The exec callback. */ + PFNAUDIOTESTEXEC pfnExec; + /** The destruction callback. */ + PFNAUDIOTESTDESTROY pfnDestroy; +} AUDIOTESTDESC; + +/** + * Backend description. + */ +typedef struct AUDIOTESTBACKENDDESC +{ + /** The driver registration structure. */ + PCPDMDRVREG pDrvReg; + /** The backend name. + * Aliases are implemented by having multiple entries for the same backend. */ + const char *pszName; +} AUDIOTESTBACKENDDESC; + +/** + * VKAT command table entry. + */ +typedef struct VKATCMD +{ + /** The command name. */ + const char *pszCommand; + /** The command handler. */ + DECLCALLBACKMEMBER(RTEXITCODE, pfnHandler,(PRTGETOPTSTATE pGetState)); + + /** Command description. */ + const char *pszDesc; + /** Options array. */ + PCRTGETOPTDEF paOptions; + /** Number of options in the option array. */ + size_t cOptions; + /** Gets help for an option. */ + DECLCALLBACKMEMBER(const char *, pfnOptionHelp,(PCRTGETOPTDEF pOpt)); + /** Flag indicating if the command needs the ATS transport layer. + * Needed for command line parsing. */ + bool fNeedsTransport; +} VKATCMD; +/** Pointer to a const VKAT command entry. */ +typedef VKATCMD const *PCVKATCMD; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Terminate ASAP if set. Set on Ctrl-C. */ +extern bool volatile g_fTerminate; +/** The release logger. */ +extern PRTLOGGER g_pRelLogger; + +/** The test handle. */ +extern RTTEST g_hTest; +/** The current verbosity level. */ +extern unsigned g_uVerbosity; +/** DrvAudio: Enable debug (or not). */ +extern bool g_fDrvAudioDebug; +/** DrvAudio: The debug output path. */ +extern const char *g_pszDrvAudioDebug; + +extern const VKATCMD g_CmdTest; +extern const VKATCMD g_CmdVerify; +extern const VKATCMD g_CmdBackends; +extern const VKATCMD g_CmdEnum; +extern const VKATCMD g_CmdPlay; +extern const VKATCMD g_CmdRec; +extern const VKATCMD g_CmdSelfTest; + + +extern AUDIOTESTDESC g_aTests[]; +extern unsigned g_cTests; + +extern AUDIOTESTBACKENDDESC const g_aBackends[]; +extern unsigned g_cBackends; + + +/********************************************************************************************************************************* +* Prototypes * +*********************************************************************************************************************************/ + +/** @name Command line handlers + * @{ */ +RTEXITCODE audioTestUsage(PRTSTREAM pStrm, PCVKATCMD pOnlyCmd); +RTEXITCODE audioTestVersion(void); +void audioTestShowLogo(PRTSTREAM pStream); +/** @} */ + +/** @name Driver stack + * @{ */ +int AudioTestDriverStackPerformSelftest(void); + +void audioTestDriverStackDelete(PAUDIOTESTDRVSTACK pDrvStack); +int audioTestDriverStackInitEx(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio); +int audioTestDriverStackInit(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fWithDrvAudio); +int audioTestDriverStackProbe(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, bool fEnabledIn, bool fEnabledOut, bool fWithDrvAudio); +int audioTestDriverStackSetDevice(PAUDIOTESTDRVSTACK pDrvStack, PDMAUDIODIR enmDir, const char *pszDevId); +/** @} */ + +/** @name Driver + * @{ */ +int audioTestDrvConstruct(PAUDIOTESTDRVSTACK pDrvStack, PCPDMDRVREG pDrvReg, PPDMDRVINS pParentDrvIns, PPPDMDRVINS ppDrvIns); +/** @} */ + +/** @name Driver stack stream + * @{ */ +int audioTestDriverStackStreamCreateInput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps, + uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint, + PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq); +int audioTestDriverStackStreamCreateOutput(PAUDIOTESTDRVSTACK pDrvStack, PCPDMAUDIOPCMPROPS pProps, + uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint, + PPDMAUDIOSTREAM *ppStream, PPDMAUDIOSTREAMCFG pCfgAcq); +void audioTestDriverStackStreamDestroy(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream); +int audioTestDriverStackStreamDrain(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, bool fSync); +int audioTestDriverStackStreamEnable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream); +int AudioTestDriverStackStreamDisable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream); +bool audioTestDriverStackStreamIsOkay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream); +uint32_t audioTestDriverStackStreamGetWritable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream); +int audioTestDriverStackStreamPlay(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, void const *pvBuf, + uint32_t cbBuf, uint32_t *pcbPlayed); +uint32_t audioTestDriverStackStreamGetReadable(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream); +int audioTestDriverStackStreamCapture(PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, + void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured); +/** @} */ + +/** @name Backend handling + * @{ */ +PCPDMDRVREG AudioTestGetDefaultBackend(void); +PCPDMDRVREG AudioTestFindBackendOpt(const char *pszBackend); +/** @} */ + +/** @name Mixing stream + * @{ */ +int AudioTestMixStreamInit(PAUDIOTESTDRVMIXSTREAM pMix, PAUDIOTESTDRVSTACK pDrvStack, PPDMAUDIOSTREAM pStream, + PCPDMAUDIOPCMPROPS pProps, uint32_t cMsBuffer); +void AudioTestMixStreamTerm(PAUDIOTESTDRVMIXSTREAM pMix); +int AudioTestMixStreamEnable(PAUDIOTESTDRVMIXSTREAM pMix); +int AudioTestMixStreamDrain(PAUDIOTESTDRVMIXSTREAM pMix, bool fSync); +int AudioTestMixStreamDisable(PAUDIOTESTDRVMIXSTREAM pMix); +bool AudioTestMixStreamIsOkay(PAUDIOTESTDRVMIXSTREAM pMix); +uint32_t AudioTestMixStreamGetWritable(PAUDIOTESTDRVMIXSTREAM pMix); +int AudioTestMixStreamPlay(PAUDIOTESTDRVMIXSTREAM pMix, void const *pvBuf, uint32_t cbBuf, uint32_t *pcbPlayed); +uint32_t AudioTestMixStreamGetReadable(PAUDIOTESTDRVMIXSTREAM pMix); +int AudioTestMixStreamCapture(PAUDIOTESTDRVMIXSTREAM pMix, void *pvBuf, uint32_t cbBuf, uint32_t *pcbCaptured); +void AudioTestMixStreamSetVolume(PAUDIOTESTDRVMIXSTREAM pMix, uint8_t uVolumePercent); +/** @} */ + +/** @name Device handling + * @{ */ +int audioTestDeviceOpen(PPDMAUDIOHOSTDEV pDev); +int audioTestDeviceClose(PPDMAUDIOHOSTDEV pDev); + +int audioTestDevicesEnumerateAndCheck(PAUDIOTESTDRVSTACK pDrvStack, const char *pszDev, PPDMAUDIOHOSTDEV *ppDev); +/** @} */ + +/** @name ATS routines + * @{ */ +int audioTestEnvConnectToValKitAts(PAUDIOTESTENV pTstEnv, + const char *pszHostTcpAddr, uint32_t uHostTcpPort); +/** @} */ + +/** @name Test environment handling + * @{ */ +void audioTestEnvInit(PAUDIOTESTENV pTstEnv); +int audioTestEnvCreate(PAUDIOTESTENV pTstEnv, PAUDIOTESTDRVSTACK pDrvStack); +void audioTestEnvDestroy(PAUDIOTESTENV pTstEnv); +int audioTestEnvPrologue(PAUDIOTESTENV pTstEnv, bool fPack, char *pszPackFile, size_t cbPackFile); + +void audioTestParmsInit(PAUDIOTESTPARMS pTstParms); +void audioTestParmsDestroy(PAUDIOTESTPARMS pTstParms); +/** @} */ + +int audioTestWorker(PAUDIOTESTENV pTstEnv); + +/** @todo Test tone handling */ +int audioTestPlayTone(PAUDIOTESTIOOPTS pIoOpts, PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms); +void audioTestToneParmsInit(PAUDIOTESTTONEPARMS pToneParms); +/** @} */ + +void audioTestIoOptsInitDefaults(PAUDIOTESTIOOPTS pIoOpts); + + +/********************************************************************************************************************************* +* Common command line stuff * +*********************************************************************************************************************************/ + +/** + * Common long options values. + */ +enum +{ + AUDIO_TEST_OPT_CMN_DAEMONIZE = 256, + AUDIO_TEST_OPT_CMN_DAEMONIZED, + AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_ENABLE, + AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_PATH +}; + +/** For use in the option switch to handle common options. */ +#define AUDIO_TEST_COMMON_OPTION_CASES(a_ValueUnion, a_pCmd) \ + case 'q': \ + g_uVerbosity = 0; \ + if (g_pRelLogger) \ + RTLogGroupSettings(g_pRelLogger, "all=0 all.e"); \ + break; \ + \ + case 'v': \ + /* No-op here, has been handled by main() already. */ /** @todo r-bird: -q works, so -v must too! */ \ + break; \ + \ + case 'V': \ + return audioTestVersion(); \ + \ + case 'h': \ + return audioTestUsage(g_pStdOut, a_pCmd); \ + \ + case AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_ENABLE: \ + g_fDrvAudioDebug = true; \ + break; \ + \ + case AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_PATH: \ + g_pszDrvAudioDebug = (a_ValueUnion).psz; \ + break; \ + case AUDIO_TEST_OPT_CMN_DAEMONIZE: \ + break; \ + case AUDIO_TEST_OPT_CMN_DAEMONIZED: \ + break; + +#endif /* !VBOX_INCLUDED_SRC_audio_vkatInternal_h */ + diff --git a/src/VBox/ValidationKit/utils/clipboard/ClipUtil.cpp b/src/VBox/ValidationKit/utils/clipboard/ClipUtil.cpp new file mode 100644 index 00000000..4c1e10c5 --- /dev/null +++ b/src/VBox/ValidationKit/utils/clipboard/ClipUtil.cpp @@ -0,0 +1,1793 @@ +/* $Id: ClipUtil.cpp $ */ +/** @file + * ClipUtil - Clipboard Utility + */ + +/* + * Copyright (C) 2021-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#ifdef RT_OS_OS2 +# define INCL_BASE +# define INCL_PM +# define INCL_ERRORS +# include <os2.h> +# undef RT_MAX +#endif + +#include <iprt/assert.h> +#include <iprt/errcore.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/process.h> +#include <iprt/string.h> +#include <iprt/stream.h> +#include <iprt/utf16.h> +#include <iprt/zero.h> + +#ifdef RT_OS_DARWIN +/** @todo */ +#elif defined(RT_OS_WINDOWS) +# include <iprt/nt/nt-and-windows.h> +#elif !defined(RT_OS_OS2) +# include <X11/Xlib.h> +# include <X11/Xatom.h> +#endif + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) || defined(RT_OS_DARWIN) +# undef MULTI_TARGET_CLIPBOARD +# undef CU_X11 +#else +# define MULTI_TARGET_CLIPBOARD +# define CU_X11 +#endif + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * Clipboard format descriptor. + */ +typedef struct CLIPUTILFORMAT +{ + /** Format name. */ + const char *pszName; + +#if defined(RT_OS_WINDOWS) + /** Windows integer format (CF_XXXX). */ + UINT fFormat; + /** Windows string format name. */ + const WCHAR *pwszFormat; + +#elif defined(RT_OS_OS2) + /** OS/2 integer format. */ + ULONG fFormat; + /** OS/2 string format name. */ + const char *pszFormat; + +#elif defined(RT_OS_DARWIN) + /** Native format (flavor). */ + CFStringRef *hStrFormat; +#else + /** The X11 atom for the format. */ + Atom uAtom; + /** The X11 atom name if uAtom must be termined dynamically. */ + const char *pszAtomName; + /** @todo X11 */ +#endif + + /** Description. */ + const char *pszDesc; + /** CLIPUTILFORMAT_F_XXX. */ + uint32_t fFlags; +} CLIPUTILFORMAT; +/** Pointer to a clipobard format descriptor. */ +typedef CLIPUTILFORMAT const *PCCLIPUTILFORMAT; + +/** Convert to/from UTF-8. */ +#define CLIPUTILFORMAT_F_CONVERT_UTF8 RT_BIT_32(0) +/** Ad hoc entry. */ +#define CLIPUTILFORMAT_F_AD_HOC RT_BIT_32(1) + + +#ifdef MULTI_TARGET_CLIPBOARD +/** + * Clipboard target descriptor. + */ +typedef struct CLIPUTILTARGET +{ + /** Target name. */ + const char *pszName; + /** The X11 atom for the target. */ + Atom uAtom; + /** The X11 atom name if uAtom must be termined dynamically. */ + const char *pszAtomName; + /** Description. */ + const char *pszDesc; +} CLIPUTILTARGET; +/** Pointer to clipboard target descriptor. */ +typedef CLIPUTILTARGET const *PCCLIPUTILTARGET; +#endif /* MULTI_TARGET_CLIPBOARD */ + + +#ifdef RT_OS_OS2 +/** Header for Odin32 specific clipboard entries. + * (Used to get the correct size of the data.) + */ +typedef struct _Odin32ClipboardHeader +{ + /** Magic (CLIPHEADER_MAGIC) */ + char achMagic[8]; + /** Size of the following data. + * (The interpretation depends on the type.) */ + unsigned cbData; + /** Odin32 format number. */ + unsigned uFormat; +} CLIPHEADER, *PCLIPHEADER; + +#define CLIPHEADER_MAGIC "Odin\1\0\1" +#endif + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Command line parameters */ +static const RTGETOPTDEF g_aCmdOptions[] = +{ + { "--list", 'l', RTGETOPT_REQ_NOTHING }, + { "--get", 'g', RTGETOPT_REQ_STRING }, + { "--get-file", 'G', RTGETOPT_REQ_STRING }, + { "--put", 'p', RTGETOPT_REQ_STRING }, + { "--put-file", 'P', RTGETOPT_REQ_STRING }, + { "--check", 'c', RTGETOPT_REQ_STRING }, + { "--check-file", 'C', RTGETOPT_REQ_STRING }, + { "--check-not", 'n', RTGETOPT_REQ_STRING }, + { "--zap", 'z', RTGETOPT_REQ_NOTHING }, +#ifdef MULTI_TARGET_CLIPBOARD + { "--target", 't', RTGETOPT_REQ_STRING }, +#endif +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) + { "--close", 'k', RTGETOPT_REQ_NOTHING }, +#endif + { "--wait", 'w', RTGETOPT_REQ_UINT32 }, + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + { "--version", 'V', RTGETOPT_REQ_NOTHING }, + { "--help", 'h', RTGETOPT_REQ_NOTHING }, /* for Usage() */ +}; + +/** Format descriptors. */ +static CLIPUTILFORMAT g_aFormats[] = +{ +#if defined(RT_OS_WINDOWS) + { "text/ansi", CF_TEXT, NULL, "ANSI text", 0 }, + { "text/utf-16", CF_UNICODETEXT, NULL, "UTF-16 text", 0 }, + { "text/utf-8", CF_UNICODETEXT, NULL, "UTF-8 text", CLIPUTILFORMAT_F_CONVERT_UTF8 }, + /* https://docs.microsoft.com/en-us/windows/desktop/dataxchg/html-clipboard-format */ + { "text/html", 0, L"HTML Format", "HTML text", 0 }, + { "bitmap", CF_DIB, NULL, "Bitmap (DIB)", 0 }, + { "bitmap/v5", CF_DIBV5, NULL, "Bitmap version 5 (DIBv5)", 0 }, +#elif defined(RT_OS_OS2) + { "text/ascii", CF_TEXT, NULL, "ASCII text", 0 }, + { "text/utf-8", CF_TEXT, NULL, "UTF-8 text", CLIPUTILFORMAT_F_CONVERT_UTF8 }, + { "text/utf-16", 0, "Odin32 UnicodeText", "UTF-16 text", 0}, +#elif defined(RT_OS_DARWIN) + { "text/utf-8", kUTTypeUTF8PlainText, "UTF-8 text", 0 }, + { "text/utf-16", kUTTypeUTF16PlainText, "UTF-16 text", 0 }, +#else + /** @todo X11 */ + { "text/utf-8", None, "UTF8_STRING", "UTF-8 text", 0 }, +#endif +}; + +#ifdef MULTI_TARGET_CLIPBOARD +/** Target descriptors. */ +static CLIPUTILTARGET g_aTargets[] = +{ + { "clipboard", 0, "CLIPBOARD", "XA_CLIPBOARD: The clipboard (default)" }, + { "primary", XA_PRIMARY, NULL, "XA_PRIMARY: Primary selected text (middle mouse button)" }, + { "secondary", XA_SECONDARY, NULL, "XA_SECONDARY: Secondary selected text (with ctrl)" }, +}; + +/** The current clipboard target. */ +static CLIPUTILTARGET *g_pTarget = &g_aTargets[0]; +#endif /* MULTI_TARGET_CLIPBOARD */ + +/** The -v/-q state. */ +static unsigned g_uVerbosity = 1; + +#ifdef RT_OS_DARWIN + +#elif defined(RT_OS_OS2) +/** Anchorblock handle. */ +static HAB g_hOs2Ab = NULLHANDLE; +/** The message queue handle. */ +static HMQ g_hOs2MsgQueue = NULLHANDLE; +/** Windows that becomes clipboard owner when setting data. */ +static HWND g_hOs2Wnd = NULLHANDLE; +/** Set if we've opened the clipboard. */ +static bool g_fOs2OpenedClipboard = false; +/** Set if we're the clipboard owner. */ +static bool g_fOs2ClipboardOwner = false; +/** Set when we receive a WM_TIMER message during DoWait(). */ +static bool volatile g_fOs2TimerTicked = false; + +#elif defined(RT_OS_WINDOWS) +/** Set if we've opened the clipboard. */ +static bool g_fWinOpenedClipboard = false; +/** Set when we receive a WM_TIMER message during DoWait(). */ +static bool volatile g_fWinTimerTicked = false; +/** Window that becomes clipboard owner when setting data. */ +static HWND g_hWinWnd = NULL; + +#else +/** Number of errors (incremented by error handle callback). */ +static uint32_t volatile g_cX11Errors; +/** The X11 display. */ +static Display *g_pX11Display = NULL; +/** The X11 dummy window. */ +static Window g_hX11Window = 0; +/** TARGETS */ +static Atom g_uX11AtomTargets; +/** MULTIPLE */ +static Atom g_uX11AtomMultiple; + +#endif + + +/** + * Gets a format descriptor, complaining if invalid format. + * + * @returns Pointer to the descriptor if found, NULL + msg if not. + * @param pszFormat The format to get. + */ +static PCCLIPUTILFORMAT GetFormatDesc(const char *pszFormat) +{ + for (size_t i = 0; i < RT_ELEMENTS(g_aFormats); i++) + if (strcmp(pszFormat, g_aFormats[i].pszName) == 0) + { +#if defined(RT_OS_DARWIN) + /** @todo */ + +#elif defined(RT_OS_OS2) + if (g_aFormats[i].pszFormat && g_aFormats[i].fFormat == 0) + { + g_aFormats[i].fFormat = WinAddAtom(WinQuerySystemAtomTable(), g_aFormats[i].pszFormat); + if (g_aFormats[i].fFormat == 0) + RTMsgError("WinAddAtom(,%s) failed: %#x", g_aFormats[i].pszFormat, WinGetLastError(g_hOs2Ab)); + } + +#elif defined(RT_OS_WINDOWS) + if (g_aFormats[i].pwszFormat && g_aFormats[i].fFormat == 0) + { + g_aFormats[i].fFormat = RegisterClipboardFormatW(g_aFormats[i].pwszFormat); + if (g_aFormats[i].fFormat == 0) + RTMsgError("RegisterClipboardFormatW(%ls) failed: %u (%#x)", + g_aFormats[i].pwszFormat, GetLastError(), GetLastError()); + } +#elif defined(CU_X11) + if (g_aFormats[i].pszAtomName && g_aFormats[i].uAtom == 0) + g_aFormats[i].uAtom = XInternAtom(g_pX11Display, g_aFormats[i].pszAtomName, False); +#endif + return &g_aFormats[i]; + } + + /* + * Try register the format. + */ + static CLIPUTILFORMAT AdHoc; + AdHoc.pszName = pszFormat; + AdHoc.pszDesc = pszFormat; + AdHoc.fFlags = CLIPUTILFORMAT_F_AD_HOC; +#ifdef RT_OS_DARWIN +/** @todo */ + +#elif defined(RT_OS_OS2) + AdHoc.pszFormat = pszFormat; + AdHoc.fFormat = WinAddAtom(WinQuerySystemAtomTable(), pszFormat); + if (AdHoc.fFormat == 0) + { + RTMsgError("Invalid format '%s' (%#x)", pszFormat, WinGetLastError(g_hOs2Ab)); + return NULL; + } + +#elif defined(RT_OS_WINDOWS) + AdHoc.pwszFormat = NULL; + AdHoc.fFormat = RegisterClipboardFormatA(pszFormat); + if (AdHoc.fFormat == 0) + { + RTMsgError("RegisterClipboardFormatA(%s) failed: %u (%#x)", pszFormat, GetLastError(), GetLastError()); + return NULL; + } + +#else + AdHoc.pszAtomName = pszFormat; + AdHoc.uAtom = XInternAtom(g_pX11Display, pszFormat, False); + if (AdHoc.uAtom == None) + { + RTMsgError("Invalid format '%s' or out of memory for X11 atoms", pszFormat); + return NULL; + } + +#endif + return &AdHoc; +} + + +#ifdef RT_OS_DARWIN + +/** @todo */ + + +#elif defined(RT_OS_OS2) + +/** + * The window procedure for the object window. + * + * @returns Message result. + * + * @param hwnd The window handle. + * @param msg The message. + * @param mp1 Message parameter 1. + * @param mp2 Message parameter 2. + */ +static MRESULT EXPENTRY CuOs2WinProc(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2) +{ + if (g_uVerbosity > 2) + RTMsgInfo("CuOs2WinProc: hwnd=%#lx msg=%#lx mp1=%#lx mp2=%#lx\n", hwnd, msg, mp1, mp2); + + switch (msg) + { + case WM_CREATE: + return NULL; /* FALSE(/NULL) == Continue*/ + case WM_DESTROY: + break; + + /* + * Clipboard viewer message - the content has been changed. + * This is sent *after* releasing the clipboard sem + * and during the WinSetClipbrdViewer call. + */ + case WM_DRAWCLIPBOARD: + break; + + /* + * Clipboard owner message - the content was replaced. + * This is sent by someone with an open clipboard, so don't try open it now. + */ + case WM_DESTROYCLIPBOARD: + break; + + /* + * Clipboard owner message - somebody is requesting us to render a format. + * This is called by someone which owns the clipboard, but that's fine. + */ + case WM_RENDERFMT: + break; + + /* + * Clipboard owner message - we're about to quit and should render all formats. + */ + case WM_RENDERALLFMTS: + break; + + /* + * Clipboard owner messages dealing with owner drawn content. + * We shouldn't be seeing any of these. + */ + case WM_PAINTCLIPBOARD: + case WM_SIZECLIPBOARD: + case WM_HSCROLLCLIPBOARD: + case WM_VSCROLLCLIPBOARD: + AssertMsgFailed(("msg=%lx (%ld)\n", msg, msg)); + break; + + /* + * We shouldn't be seeing any other messages according to the docs. + * But for whatever reason, PM sends us a WM_ADJUSTWINDOWPOS message + * during WinCreateWindow. So, ignore that and assert on anything else. + */ + default: + AssertMsgFailed(("msg=%lx (%ld)\n", msg, msg)); + case WM_ADJUSTWINDOWPOS: + break; + + /* + * We use this window fielding WM_TIMER during DoWait. + */ + case WM_TIMER: + if (SHORT1FROMMP(mp1) == 1) + g_fOs2TimerTicked = true; + break; + } + return NULL; +} + + +/** + * Initialize the OS/2 bits. + */ +static RTEXITCODE CuOs2Init(void) +{ + g_hOs2Ab = WinInitialize(0); + if (g_hOs2Ab == NULLHANDLE) + return RTMsgErrorExitFailure("WinInitialize failed!"); + + g_hOs2MsgQueue = WinCreateMsgQueue(g_hOs2Ab, 10); + if (g_hOs2MsgQueue == NULLHANDLE) + return RTMsgErrorExitFailure("WinCreateMsgQueue failed: %#x", WinGetLastError(g_hOs2Ab)); + + static char s_szClass[] = "VBox-ClipUtilClipboardClass"; + if (!WinRegisterClass(g_hOs2Wnd, (PCSZ)s_szClass, CuOs2WinProc, 0, 0)) + return RTMsgErrorExitFailure("WinRegisterClass failed: %#x", WinGetLastError(g_hOs2Ab)); + + g_hOs2Wnd = WinCreateWindow(HWND_OBJECT, /* hwndParent */ + (PCSZ)s_szClass, /* pszClass */ + (PCSZ)"VirtualBox Clipboard Utility", /* pszName */ + 0, /* flStyle */ + 0, 0, 0, 0, /* x, y, cx, cy */ + NULLHANDLE, /* hwndOwner */ + HWND_BOTTOM, /* hwndInsertBehind */ + 42, /* id */ + NULL, /* pCtlData */ + NULL); /* pPresParams */ + if (g_hOs2Wnd == NULLHANDLE) + return RTMsgErrorExitFailure("WinCreateWindow failed: %#x", WinGetLastError(g_hOs2Ab)); + + return RTEXITCODE_SUCCESS; +} + + +/** + * Terminates the OS/2 bits. + */ +static RTEXITCODE CuOs2Term(void) +{ + if (g_fOs2OpenedClipboard) + { + if (!WinCloseClipbrd(g_hOs2Ab)) + return RTMsgErrorExitFailure("WinCloseClipbrd failed: %#x", WinGetLastError(g_hOs2Ab)); + g_fOs2OpenedClipboard = false; + } + + WinDestroyWindow(g_hOs2Wnd); + g_hOs2Wnd = NULLHANDLE; + + WinDestroyMsgQueue(g_hOs2MsgQueue); + g_hOs2MsgQueue = NULLHANDLE; + + WinTerminate(g_hOs2Ab); + g_hOs2Ab = NULLHANDLE; + + return RTEXITCODE_SUCCESS; +} + + +/** + * Opens the OS/2 clipboard. + */ +static RTEXITCODE CuOs2OpenClipboardIfNecessary(void) +{ + if (g_fOs2OpenedClipboard) + return RTEXITCODE_SUCCESS; + if (WinOpenClipbrd(g_hOs2Ab)) + { + if (g_uVerbosity > 0) + RTMsgInfo("Opened the clipboard\n"); + g_fOs2OpenedClipboard = true; + return RTEXITCODE_SUCCESS; + } + return RTMsgErrorExitFailure("WinOpenClipbrd failed: %#x", WinGetLastError(g_hOs2Ab)); +} + + +#elif defined(RT_OS_WINDOWS) + +/** + * Window procedure for the clipboard owner window on Windows. + */ +static LRESULT CALLBACK CuWinWndProc(HWND hWnd, UINT idMsg, WPARAM wParam, LPARAM lParam) RT_NOTHROW_DEF +{ + if (g_uVerbosity > 2) + RTMsgInfo("CuWinWndProc: hWnd=%p idMsg=%#05x wParam=%#zx lParam=%#zx\n", hWnd, idMsg, wParam, lParam); + + switch (idMsg) + { + case WM_TIMER: + if (wParam == 1) + g_fWinTimerTicked = true; + break; + } + return DefWindowProc(hWnd, idMsg, wParam, lParam); +} + + +/** + * Initialize the Windows bits. + */ +static RTEXITCODE CuWinInit(void) +{ + /* Register the window class: */ + static wchar_t s_wszClass[] = L"VBox-ClipUtilClipboardClass"; + WNDCLASSW WndCls = {0}; + WndCls.style = CS_NOCLOSE; + WndCls.lpfnWndProc = CuWinWndProc; + WndCls.cbClsExtra = 0; + WndCls.cbWndExtra = 0; + WndCls.hInstance = (HINSTANCE)GetModuleHandle(NULL); + WndCls.hIcon = NULL; + WndCls.hCursor = NULL; + WndCls.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1); + WndCls.lpszMenuName = NULL; + WndCls.lpszClassName = s_wszClass; + + ATOM uAtomWndClass = RegisterClassW(&WndCls); + if (!uAtomWndClass) + return RTMsgErrorExitFailure("RegisterClassW failed: %u (%#x)", GetLastError(), GetLastError()); + + /* Create the clipboard owner window: */ + g_hWinWnd = CreateWindowExW(WS_EX_TRANSPARENT, /* fExStyle */ + s_wszClass, /* pwszClass */ + L"VirtualBox Clipboard Utility", /* pwszName */ + 0, /* fStyle */ + 0, 0, 0, 0, /* x, y, cx, cy */ + HWND_MESSAGE, /* hWndParent */ + NULL, /* hMenu */ + (HINSTANCE)GetModuleHandle(NULL), /* hinstance */ + NULL); /* pParam */ + if (g_hWinWnd == NULL) + return RTMsgErrorExitFailure("CreateWindowExW failed: %u (%#x)", GetLastError(), GetLastError()); + + return RTEXITCODE_SUCCESS; +} + +/** + * Terminates the Windows bits. + */ +static RTEXITCODE CuWinTerm(void) +{ + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + if (g_fWinOpenedClipboard) + { + if (CloseClipboard()) + g_fWinOpenedClipboard = false; + else + rcExit = RTMsgErrorExitFailure("CloseClipboard failed: %u (%#x)", GetLastError(), GetLastError()); + } + + if (g_hWinWnd != NULL) + { + if (!DestroyWindow(g_hWinWnd)) + rcExit = RTMsgErrorExitFailure("DestroyWindow failed: %u (%#x)", GetLastError(), GetLastError()); + g_hWinWnd = NULL; + } + + return rcExit; +} + + +/** + * Opens the window clipboard. + */ +static RTEXITCODE WinOpenClipboardIfNecessary(void) +{ + if (g_fWinOpenedClipboard) + return RTEXITCODE_SUCCESS; + if (OpenClipboard(g_hWinWnd)) + { + if (g_uVerbosity > 0) + RTMsgInfo("Opened the clipboard\n"); + g_fWinOpenedClipboard = true; + return RTEXITCODE_SUCCESS; + } + return RTMsgErrorExitFailure("OpenClipboard failed: %u (%#x)", GetLastError(), GetLastError()); +} + + +#else /* X11: */ + +/** + * Error handler callback. + */ +static int CuX11ErrorCallback(Display *pX11Display, XErrorEvent *pErrEvt) +{ + g_cX11Errors++; + char szErr[2048]; + XGetErrorText(pX11Display, pErrEvt->error_code, szErr, sizeof(szErr)); + RTMsgError("An X Window protocol error occurred: %s\n" + " Request code: %u\n" + " Minor code: %u\n" + " Serial number of the failed request: %u\n", + szErr, pErrEvt->request_code, pErrEvt->minor_code, pErrEvt->serial); + return 0; +} + + +/** + * Initialize the X11 bits. + */ +static RTEXITCODE CuX11Init(void) +{ + /* + * Open the X11 display and create a little dummy window. + */ + XSetErrorHandler(CuX11ErrorCallback); + g_pX11Display = XOpenDisplay(NULL); + if (!g_pX11Display) + return RTMsgErrorExitFailure("XOpenDisplay failed"); + + int const iDefaultScreen = DefaultScreen(g_pX11Display); + g_hX11Window = XCreateSimpleWindow(g_pX11Display, + RootWindow(g_pX11Display, iDefaultScreen), + 0 /*x*/, 0 /*y*/, + 1 /*cx*/, 1 /*cy*/, + 0 /*cPxlBorder*/, + BlackPixel(g_pX11Display, iDefaultScreen) /*Border*/, + WhitePixel(g_pX11Display, iDefaultScreen) /*Background*/); + + /* + * Resolve any unknown atom values we might need later. + */ + for (size_t i = 0; i < RT_ELEMENTS(g_aTargets); i++) + if (g_aTargets[i].pszAtomName) + { + g_aTargets[i].uAtom = XInternAtom(g_pX11Display, g_aTargets[i].pszAtomName, False); + if (g_uVerbosity > 2) + RTPrintf("target %s atom=%#x\n", g_aTargets[i].pszName, g_aTargets[i].uAtom); + } + + g_uX11AtomTargets = XInternAtom(g_pX11Display, "TARGETS", False); + g_uX11AtomMultiple = XInternAtom(g_pX11Display, "MULTIPLE", False); + + return RTEXITCODE_SUCCESS; +} + +#endif /* X11 */ + + +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) +/** + * Closes the clipboard if open. + */ +static RTEXITCODE CuCloseClipboard(void) +{ +# if defined(RT_OS_OS2) + if (g_fOs2OpenedClipboard) + { + if (!WinCloseClipbrd(g_hOs2Ab)) + return RTMsgErrorExitFailure("WinCloseClipbrd failed: %#x", WinGetLastError(g_hOs2Ab)); + g_fOs2OpenedClipboard = false; + if (g_uVerbosity > 0) + RTMsgInfo("Closed the clipboard.\n"); + } +# else + if (g_fWinOpenedClipboard) + { + if (!CloseClipboard()) + return RTMsgErrorExitFailure("CloseClipboard failed: %u (%#x)", GetLastError(), GetLastError()); + g_fWinOpenedClipboard = false; + if (g_uVerbosity > 0) + RTMsgInfo("Closed the clipboard.\n"); + } +# endif + else if (g_uVerbosity > 0) + RTMsgInfo("No need to close clipboard, not opened.\n"); + + return RTEXITCODE_SUCCESS; +} +#endif /* RT_OS_OS2 || RT_OS_WINDOWS */ + + +/** + * Lists the clipboard format. + */ +static RTEXITCODE ListClipboardContent(void) +{ +#if defined(RT_OS_OS2) + RTEXITCODE rcExit = CuOs2OpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + HATOMTBL const hAtomTbl = WinQuerySystemAtomTable(); + uint32_t idx = 0; + ULONG fFormat = 0; + while ((fFormat = WinEnumClipbrdFmts(g_hOs2Ab)) != 0) + { + char szName[256] = {0}; + ULONG cchRet = WinQueryAtomName(hAtomTbl, fFormat, szName, sizeof(szName)); + if (cchRet != 0) + RTPrintf("#%02u: %#06x - %s\n", idx, fFormat, szName); + else + { + const char *pszName = NULL; + switch (fFormat) + { + case CF_TEXT: pszName = "CF_TEXT"; break; + case CF_BITMAP: pszName = "CF_BITMAP"; break; + case CF_DSPTEXT: pszName = "CF_DSPTEXT"; break; + case CF_DSPBITMAP: pszName = "CF_DSPBITMAP"; break; + case CF_METAFILE: pszName = "CF_METAFILE"; break; + case CF_DSPMETAFILE: pszName = "CF_DSPMETAFILE"; break; + case CF_PALETTE: pszName = "CF_PALETTE"; break; + default: + break; + } + if (pszName) + RTPrintf("#%02u: %#06x - %s\n", idx, fFormat, pszName); + else + RTPrintf("#%02u: %#06x\n", idx, fFormat); + } + + idx++; + } + } + + return rcExit; + +#elif defined(RT_OS_WINDOWS) + RTEXITCODE rcExit = WinOpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + SetLastError(0); + uint32_t idx = 0; + UINT fFormat = 0; + while ((fFormat = EnumClipboardFormats(fFormat)) != 0) + { + WCHAR wszName[256]; + int cchName = GetClipboardFormatNameW(fFormat, wszName, RT_ELEMENTS(wszName)); + if (cchName > 0) + RTPrintf("#%02u: %#06x - %ls\n", idx, fFormat, wszName); + else + { + const char *pszName = NULL; + switch (fFormat) + { + case CF_TEXT: pszName = "CF_TEXT"; break; + case CF_BITMAP: pszName = "CF_BITMAP"; break; + case CF_METAFILEPICT: pszName = "CF_METAFILEPICT"; break; + case CF_SYLK: pszName = "CF_SYLK"; break; + case CF_DIF: pszName = "CF_DIF"; break; + case CF_TIFF: pszName = "CF_TIFF"; break; + case CF_OEMTEXT: pszName = "CF_OEMTEXT"; break; + case CF_DIB: pszName = "CF_DIB"; break; + case CF_PALETTE: pszName = "CF_PALETTE"; break; + case CF_PENDATA: pszName = "CF_PENDATA"; break; + case CF_RIFF: pszName = "CF_RIFF"; break; + case CF_WAVE: pszName = "CF_WAVE"; break; + case CF_UNICODETEXT: pszName = "CF_UNICODETEXT"; break; + case CF_ENHMETAFILE: pszName = "CF_ENHMETAFILE"; break; + case CF_HDROP: pszName = "CF_HDROP"; break; + case CF_LOCALE: pszName = "CF_LOCALE"; break; + case CF_DIBV5: pszName = "CF_DIBV5"; break; + default: + break; + } + if (pszName) + RTPrintf("#%02u: %#06x - %s\n", idx, fFormat, pszName); + else + RTPrintf("#%02u: %#06x\n", idx, fFormat); + } + + idx++; + } + if (idx == 0) + RTPrintf("Empty\n"); + } + return rcExit; + +#elif defined(CU_X11) + /* Request the TARGETS property: */ + Atom uAtomDst = g_uX11AtomTargets; + int rc = XConvertSelection(g_pX11Display, g_pTarget->uAtom, g_uX11AtomTargets, uAtomDst, g_hX11Window, CurrentTime); + if (g_uVerbosity > 1) + RTPrintf("XConvertSelection -> %d\n", rc); + + /* Wait for the reply: */ + for (;;) + { + XEvent Evt = {0}; + rc = XNextEvent(g_pX11Display, &Evt); + if (Evt.type == SelectionNotify) + { + if (g_uVerbosity > 1) + RTPrintf("XNextEvent -> %d; type=SelectionNotify\n", rc); + if (Evt.xselection.selection == g_pTarget->uAtom) + { + if (Evt.xselection.property == None) + return RTMsgErrorExitFailure("XConvertSelection(,%s,TARGETS,) failed", g_pTarget->pszName); + + /* Get the TARGETS property data: */ + Atom uAtomRetType = 0; + int iActualFmt = 0; + unsigned long cbLeftToRead = 0; + unsigned long cItems = 0; + unsigned char *pbData = NULL; + rc = XGetWindowProperty(g_pX11Display, g_hX11Window, uAtomDst, + 0 /*offset*/, sizeof(Atom) * 4096 /* should be enough */, True /*fDelete*/, XA_ATOM, + &uAtomRetType, &iActualFmt, &cItems, &cbLeftToRead, &pbData); + if (g_uVerbosity > 1) + RTPrintf("XConvertSelection -> %d; uAtomRetType=%u iActualFmt=%d cItems=%lu cbLeftToRead=%lu pbData=%p\n", + rc, uAtomRetType, iActualFmt, cItems, cbLeftToRead, pbData); + if (pbData && cItems > 0) + { + /* Display the TARGETS: */ + Atom const *paTargets = (Atom const *)pbData; + for (unsigned long i = 0; i < cItems; i++) + { + const char *pszName = XGetAtomName(g_pX11Display, paTargets[i]); + if (pszName) + RTPrintf("#%02u: %#06x - %s\n", i, paTargets[i], pszName); + else + RTPrintf("#%02u: %#06x\n", i, paTargets[i]); + } + } + else + RTMsgInfo("Empty"); + if (pbData) + XFree(pbData); + return RTEXITCODE_SUCCESS; + } + } + else if (g_uVerbosity > 1) + RTPrintf("XNextEvent -> %d; type=%d\n", rc, Evt.type); + } + +#else + return RTMsgErrorExitFailure("ListClipboardContent is not implemented"); +#endif +} + + +/** + * Reads the given clipboard format and stores it in on the heap. + * + * @returns Success indicator. + * @param pFmtDesc The format to get. + * @param ppvData Where to return the pointer to the data. Free using + * RTMemFree when done. + * @param pcbData Where to return the amount of data returned. + */ +static RTEXITCODE ReadClipboardData(PCCLIPUTILFORMAT pFmtDesc, void **ppvData, size_t *pcbData) +{ + *ppvData = NULL; + *pcbData = 0; + +#if defined(RT_OS_OS2) + RTEXITCODE rcExit = CuOs2OpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + ULONG fFmtInfo = 0; + if (WinQueryClipbrdFmtInfo(g_hOs2Ab, pFmtDesc->fFormat, &fFmtInfo)) + { + ULONG uData = WinQueryClipbrdData(g_hOs2Ab, pFmtDesc->fFormat); + if (fFmtInfo & CFI_POINTER) + { + PCLIPHEADER pOdinHdr = (PCLIPHEADER)uData; + if (pFmtDesc->fFormat == CF_TEXT) + { + if (pFmtDesc->fFlags & CLIPUTILFORMAT_F_CONVERT_UTF8) + { + char *pszUtf8 = NULL; + int rc = RTStrCurrentCPToUtf8(&pszUtf8, (const char *)uData); + if (RT_SUCCESS(rc)) + { + *pcbData = strlen(pszUtf8) + 1; + *ppvData = RTMemDup(pszUtf8, *pcbData); + RTStrFree(pszUtf8); + } + else + return RTMsgErrorExitFailure("RTStrCurrentCPToUtf8 failed: %Rrc", rc); + } + else + { + *pcbData = strlen((const char *)uData) + 1; + *ppvData = RTMemDup((const char *)uData, *pcbData); + } + } + else if ( strcmp(pFmtDesc->pszFormat, "Odin32 UnicodeText") == 0 + && memcmp(pOdinHdr->achMagic, CLIPHEADER_MAGIC, sizeof(pOdinHdr->achMagic)) == 0) + { + *pcbData = pOdinHdr->cbData; + *ppvData = RTMemDup(pOdinHdr + 1, pOdinHdr->cbData); + } + else + { + /* We could use DosQueryMem here to figure out the size of the allocation... */ + *pcbData = PAGE_SIZE - (uData & PAGE_OFFSET_MASK); + *ppvData = RTMemDup((void const *)uData, *pcbData); + } + } + else + { + *pcbData = sizeof(uData); + *ppvData = RTMemDup(&uData, sizeof(uData)); + } + if (!*ppvData) + rcExit = RTMsgErrorExitFailure("Out of memory allocating %#zx bytes.", *pcbData); + } + else + rcExit = RTMsgErrorExitFailure("WinQueryClipbrdFmtInfo(,%s,) failed: %#x\n", + pFmtDesc->pszName, WinGetLastError(g_hOs2Ab)); + } + return rcExit; + +#elif defined(RT_OS_WINDOWS) + RTEXITCODE rcExit = WinOpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + HANDLE hData = GetClipboardData(pFmtDesc->fFormat); + if (hData != NULL) + { + SIZE_T const cbData = GlobalSize(hData); + PVOID const pvData = GlobalLock(hData); + if (pvData != NULL) + { + *pcbData = cbData; + if (cbData != 0) + { + if (pFmtDesc->fFlags & CLIPUTILFORMAT_F_CONVERT_UTF8) + { + char *pszUtf8 = NULL; + size_t cchUtf8 = 0; + int rc = RTUtf16ToUtf8Ex((PCRTUTF16)pvData, cbData / sizeof(RTUTF16), &pszUtf8, 0, &cchUtf8); + if (RT_SUCCESS(rc)) + { + *pcbData = cchUtf8 + 1; + *ppvData = RTMemDup(pszUtf8, cchUtf8 + 1); + RTStrFree(pszUtf8); + if (!*ppvData) + rcExit = RTMsgErrorExitFailure("Out of memory allocating %#zx bytes.", cbData); + } + else + rcExit = RTMsgErrorExitFailure("RTUtf16ToUtf8Ex failed: %Rrc", rc); + } + else + { + *ppvData = RTMemDup(pvData, cbData); + if (!*ppvData) + rcExit = RTMsgErrorExitFailure("Out of memory allocating %#zx bytes.", cbData); + } + } + GlobalUnlock(hData); + } + else + rcExit = RTMsgErrorExitFailure("GetClipboardData(%s) failed: %u (%#x)\n", + pFmtDesc->pszName, GetLastError(), GetLastError()); + } + else + rcExit = RTMsgErrorExitFailure("GetClipboardData(%s) failed: %u (%#x)\n", + pFmtDesc->pszName, GetLastError(), GetLastError()); + } + return rcExit; + +#elif defined(CU_X11) + + /* Request the data: */ + Atom const uAtomDst = pFmtDesc->uAtom; + int rc = XConvertSelection(g_pX11Display, g_pTarget->uAtom, pFmtDesc->uAtom, uAtomDst, g_hX11Window, CurrentTime); + if (g_uVerbosity > 1) + RTPrintf("XConvertSelection -> %d\n", rc); + + /* Wait for the reply: */ + for (;;) + { + XEvent Evt = {0}; + rc = XNextEvent(g_pX11Display, &Evt); + if (Evt.type == SelectionNotify) + { + if (g_uVerbosity > 1) + RTPrintf("XNextEvent -> %d; type=SelectionNotify\n", rc); + if (Evt.xselection.selection == g_pTarget->uAtom) + { + if (Evt.xselection.property == None) + return RTMsgErrorExitFailure("XConvertSelection(,%s,%s,) failed", g_pTarget->pszName, pFmtDesc->pszName); + + /* + * Retrieve the data. + */ + Atom uAtomRetType = 0; + int cBitsActualFmt = 0; + unsigned long cbLeftToRead = 0; + unsigned long cItems = 0; + unsigned char *pbData = NULL; + rc = XGetWindowProperty(g_pX11Display, g_hX11Window, uAtomDst, + 0 /*offset*/, _64M, False/*fDelete*/, AnyPropertyType, + &uAtomRetType, &cBitsActualFmt, &cItems, &cbLeftToRead, &pbData); + if (g_uVerbosity > 1) + RTPrintf("XConvertSelection -> %d; uAtomRetType=%u cBitsActualFmt=%d cItems=%lu cbLeftToRead=%lu pbData=%p\n", + rc, uAtomRetType, cBitsActualFmt, cItems, cbLeftToRead, pbData); + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + if (pbData && cItems > 0) + { + *pcbData = cItems * (cBitsActualFmt / 8); + *ppvData = RTMemDup(pbData, *pcbData); + if (!*ppvData) + rcExit = RTMsgErrorExitFailure("Out of memory allocating %#zx bytes.", *pcbData); + } + if (pbData) + XFree(pbData); + XDeleteProperty(g_pX11Display, g_hX11Window, uAtomDst); + return rcExit; + } + } + else if (g_uVerbosity > 1) + RTPrintf("XNextEvent -> %d; type=%d\n", rc, Evt.type); + } + +#else + RT_NOREF(pFmtDesc); + return RTMsgErrorExitFailure("ReadClipboardData is not implemented\n"); +#endif +} + + +/** + * Puts the given data and format on the clipboard. + * + * @returns Success indicator. + * @param pFmtDesc The format. + * @param pvData The data. + * @param cbData The amount of data in bytes. + */ +static RTEXITCODE WriteClipboardData(PCCLIPUTILFORMAT pFmtDesc, void const *pvData, size_t cbData) +{ +#if defined(RT_OS_OS2) + RTEXITCODE rcExit = CuOs2OpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + /** @todo do we need to become owner? */ + + /* Convert to local code page if needed: */ + char *pszLocale = NULL; + if (pFmtDesc->fFlags & CLIPUTILFORMAT_F_CONVERT_UTF8) + { + int rc = RTStrUtf8ToCurrentCPEx(&pszLocale, (char *)pvData, cbData); + if (RT_SUCCESS(rc)) + { + pvData = pszLocale; + cbData = strlen(pszLocale) + 1; + } + else + return RTMsgErrorExitFailure("RTStrUtf8ToCurrentCPEx failed: %Rrc\n", rc); + } + + /* Allocate a bunch of shared memory for the object. */ + PVOID pvShared = NULL; + APIRET orc = DosAllocSharedMem(&pvShared, NULL, cbData, + OBJ_GIVEABLE | OBJ_GETTABLE | OBJ_TILE | PAG_READ | PAG_WRITE | PAG_COMMIT); + if (orc == NO_ERROR) + { + memcpy(pvShared, pvData, cbData); + + if (WinSetClipbrdData(g_hOs2Ab, (uintptr_t)pvShared, pFmtDesc->fFormat, CFI_POINTER)) + { + if (g_uVerbosity > 0) + RTMsgInfo("Put '%s' on the clipboard: %p LB %zu\n", pFmtDesc->pszName, pvShared, cbData); + rcExit = RTEXITCODE_SUCCESS; + } + else + { + rcExit = RTMsgErrorExitFailure("WinSetClipbrdData(,%p LB %#x,%s,) failed: %#x\n", + pvShared, cbData, pFmtDesc->pszName, WinGetLastError(g_hOs2Ab)); + DosFreeMem(pvShared); + } + } + else + rcExit = RTMsgErrorExitFailure("DosAllocSharedMem(,, %#x,) -> %u", cbData, orc); + RTStrFree(pszLocale); + } + return rcExit; + + +#elif defined(RT_OS_WINDOWS) + RTEXITCODE rcExit = WinOpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + /* + * Do input data conversion. + */ + PRTUTF16 pwszFree = NULL; + if (pFmtDesc->fFlags & CLIPUTILFORMAT_F_CONVERT_UTF8) + { + size_t cwcConv = 0; + int rc = RTStrToUtf16Ex((char const *)pvData, cbData, &pwszFree, 0, &cwcConv); + if (RT_SUCCESS(rc)) + { + pvData = pwszFree; + cbData = cwcConv * sizeof(RTUTF16); + } + else + return RTMsgErrorExitFailure("RTStrToTUtf16Ex failed: %Rrc\n", rc); + } + + /* + * Text formats generally include the zero terminator. + */ + uint32_t cbZeroPadding = 0; + if (pFmtDesc->fFormat == CF_UNICODETEXT) + cbZeroPadding = sizeof(WCHAR); + else if (pFmtDesc->fFormat == CF_TEXT) + cbZeroPadding = sizeof(char); + + HANDLE hDstData = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, cbData + cbZeroPadding); + if (hDstData) + { + if (cbData) + { + PVOID pvDstData = GlobalLock(hDstData); + if (pvDstData) + memcpy(pvDstData, pvData, cbData); + else + rcExit = RTMsgErrorExitFailure("GlobalLock failed: %u (%#x)\n", GetLastError(), GetLastError()); + } + if (rcExit == RTEXITCODE_SUCCESS) + { + if (SetClipboardData(pFmtDesc->fFormat, hDstData)) + { + if (g_uVerbosity > 0) + RTMsgInfo("Put '%s' on the clipboard: %p LB %zu\n", pFmtDesc->pszName, hDstData, cbData + cbZeroPadding); + } + else + { + rcExit = RTMsgErrorExitFailure("SetClipboardData(%s) failed: %u (%#x)\n", + pFmtDesc->pszName, GetLastError(), GetLastError()); + GlobalFree(hDstData); + } + } + else + GlobalFree(hDstData); + } + else + rcExit = RTMsgErrorExitFailure("GlobalAlloc(,%#zx) failed: %u (%#x)\n", + cbData + cbZeroPadding, GetLastError(), GetLastError()); + } + return rcExit; + +#else + RT_NOREF(pFmtDesc, pvData, cbData); + return RTMsgErrorExitFailure("WriteClipboardData is not implemented\n"); +#endif +} + + +/** + * Check if the given data + format matches what's actually on the clipboard. + * + * @returns Success indicator. + * @param pFmtDesc The format to compare. + * @param pvExpect The expected clipboard data. + * @param cbExpect The size of the expected clipboard data. + */ +static RTEXITCODE CompareDataWithClipboard(PCCLIPUTILFORMAT pFmtDesc, void const *pvExpect, size_t cbExpect) +{ + void *pvData = NULL; + size_t cbData = 0; + RTEXITCODE rcExit = ReadClipboardData(pFmtDesc, &pvData, &cbData); + if (rcExit == RTEXITCODE_SUCCESS) + { + if ( cbData == cbExpect + && memcmp(pvData, pvExpect, cbData) == 0) + rcExit = RTEXITCODE_SUCCESS; + else + rcExit = RTMsgErrorExitFailure("Mismatch for '%s' (cbData=%#zx cbExpect=%#zx)\n", + pFmtDesc->pszName, cbData, cbExpect); + RTMemFree(pvData); + } + return rcExit; +} + + +/** + * Gets the given clipboard format. + * + * @returns Success indicator. + * @param pFmtDesc The format to get. + * @param pStrmOut Where to output the data. + * @param fIsStdOut Set if @a pStrmOut is standard output, clear if not. + */ +static RTEXITCODE ClipboardContentToStdOut(PCCLIPUTILFORMAT pFmtDesc) +{ + void *pvData = NULL; + size_t cbData = 0; + RTEXITCODE rcExit = ReadClipboardData(pFmtDesc, &pvData, &cbData); + if (rcExit == RTEXITCODE_SUCCESS) + { + int rc = RTStrmWrite(g_pStdOut, pvData, cbData); + RTMemFree(pvData); + if (RT_FAILURE(rc)) + rcExit = RTMsgErrorExitFailure("Error writing %#zx bytes to standard output: %Rrc", cbData, rc); + } + return rcExit; +} + + +/** + * Gets the given clipboard format. + * + * @returns Success indicator. + * @param pFmtDesc The format to get. + * @param pszFilename The output filename. + */ +static RTEXITCODE ClipboardContentToFile(PCCLIPUTILFORMAT pFmtDesc, const char *pszFilename) +{ + void *pvData = NULL; + size_t cbData = 0; + RTEXITCODE rcExit = ReadClipboardData(pFmtDesc, &pvData, &cbData); + if (rcExit == RTEXITCODE_SUCCESS) + { + RTFILE hFile = NIL_RTFILE; + int rc = RTFileOpen(&hFile, pszFilename, + RTFILE_O_WRITE | RTFILE_O_DENY_WRITE | RTFILE_O_CREATE_REPLACE + | (0770 << RTFILE_O_CREATE_MODE_SHIFT)); + if (RT_SUCCESS(rc)) + { + rc = RTFileWrite(hFile, pvData, cbData, NULL); + int const rc2 = RTFileClose(hFile); + if (RT_FAILURE(rc) || RT_FAILURE(rc2)) + { + if (RT_FAILURE_NP(rc)) + RTMsgError("Writing %#z bytes to '%s' failed: %Rrc", cbData, pszFilename, rc); + else + RTMsgError("Closing '%s' failed: %Rrc", pszFilename, rc2); + RTMsgInfo("Deleting '%s'.", pszFilename); + RTFileDelete(pszFilename); + rcExit = RTEXITCODE_FAILURE; + } + } + else + rcExit = RTMsgErrorExitFailure("Failed to open '%s' for writing: %Rrc", pszFilename, rc); + RTMemFree(pvData); + } + return rcExit; +} + + +/** + * Puts the given format + data onto the clipboard. + * + * @returns Success indicator. + * @param pFmtDesc The format to put. + * @param pszData The string data. + */ +static RTEXITCODE PutStringOnClipboard(PCCLIPUTILFORMAT pFmtDesc, const char *pszData) +{ + return WriteClipboardData(pFmtDesc, pszData, strlen(pszData)); +} + + +/** + * Puts a format + file content onto the clipboard. + * + * @returns Success indicator. + * @param pFmtDesc The format to put. + * @param pszFilename The filename. + */ +static RTEXITCODE PutFileOnClipboard(PCCLIPUTILFORMAT pFmtDesc, const char *pszFilename) +{ + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + void *pvData = NULL; + size_t cbData = 0; + int rc = RTFileReadAll(pszFilename, &pvData, &cbData); + if (RT_SUCCESS(rc)) + { + rcExit = WriteClipboardData(pFmtDesc, pvData, cbData); + RTFileReadAllFree(pvData, cbData); + } + else + rcExit = RTMsgErrorExitFailure("Failed to open and read '%s' into memory: %Rrc", pszFilename, rc); + return rcExit; +} + + +/** + * Checks if the given format + data matches what's on the clipboard. + * + * @returns Success indicator. + * @param pFmtDesc The format to check. + * @param pszData The string data. + */ +static RTEXITCODE CheckStringAgainstClipboard(PCCLIPUTILFORMAT pFmtDesc, const char *pszData) +{ + return CompareDataWithClipboard(pFmtDesc, pszData, strlen(pszData)); +} + + +/** + * Check if the given format + file content matches what's on the clipboard. + * + * @returns Success indicator. + * @param pFmtDesc The format to check. + * @param pszFilename The filename. + */ +static RTEXITCODE CheckFileAgainstClipboard(PCCLIPUTILFORMAT pFmtDesc, const char *pszFilename) +{ + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + void *pvData = NULL; + size_t cbData = 0; + int rc = RTFileReadAll(pszFilename, &pvData, &cbData); + if (RT_SUCCESS(rc)) + { + rcExit = CompareDataWithClipboard(pFmtDesc, pvData, cbData); + RTFileReadAllFree(pvData, cbData); + } + else + rcExit = RTMsgErrorExitFailure("Failed to open and read '%s' into memory: %Rrc", pszFilename, rc); + return rcExit; +} + + +/** + * Check that the given format is not on the clipboard. + * + * @returns Success indicator. + * @param pFmtDesc The format to check. + */ +static RTEXITCODE CheckFormatNotOnClipboard(PCCLIPUTILFORMAT pFmtDesc) +{ +#if defined(RT_OS_OS2) + RTEXITCODE rcExit = CuOs2OpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + ULONG fFmtInfo = 0; + if (WinQueryClipbrdFmtInfo(g_hOs2Ab, pFmtDesc->fFormat, &fFmtInfo)) + rcExit = RTMsgErrorExitFailure("Format '%s' is present"); + } + return rcExit; + +#elif defined(RT_OS_WINDOWS) + RTEXITCODE rcExit = WinOpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + if (IsClipboardFormatAvailable(pFmtDesc->fFormat)) + rcExit = RTMsgErrorExitFailure("Format '%s' is present"); + } + return rcExit; + +#else + RT_NOREF(pFmtDesc); + return RTMsgErrorExitFailure("CheckFormatNotOnClipboard is not implemented"); +#endif +} + + +/** + * Empties the clipboard. + * + * @returns Success indicator. + */ +static RTEXITCODE ZapAllClipboardData(void) +{ +#if defined(RT_OS_OS2) + RTEXITCODE rcExit = CuOs2OpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + ULONG fFmtInfo = 0; + if (WinEmptyClipbrd(g_hOs2Ab)) + { + WinSetClipbrdOwner(g_hOs2Ab, g_hOs2Wnd); /* Probably unnecessary? */ + WinSetClipbrdOwner(g_hOs2Ab, NULLHANDLE); + g_fOs2ClipboardOwner = false; + } + else + rcExit = RTMsgErrorExitFailure("WinEmptyClipbrd() failed: %#x\n", WinGetLastError(g_hOs2Ab)); + } + return rcExit; + +#elif defined(RT_OS_WINDOWS) + RTEXITCODE rcExit = WinOpenClipboardIfNecessary(); + if (rcExit == RTEXITCODE_SUCCESS) + { + if (!EmptyClipboard()) + rcExit = RTMsgErrorExitFailure("EmptyClipboard() failed: %u (%#x)\n", GetLastError(), GetLastError()); + } + return rcExit; + +#else + return RTMsgErrorExitFailure("ZapAllClipboardData is not implemented"); +#endif +} + + +/** + * Waits/delays at least @a cMsWait milliseconds. + * + * @returns Success indicator. + * @param cMsWait Minimum wait/delay time in milliseconds. + */ +static RTEXITCODE DoWait(uint32_t cMsWait) +{ + uint64_t const msStart = RTTimeMilliTS(); + if (g_uVerbosity > 1) + RTMsgInfo("Waiting %u ms...\n", cMsWait); + +#if defined(RT_OS_OS2) + /* + * Arm a timer which will timeout after the desired period and + * quit when we've dispatched it. + */ + g_fOs2TimerTicked = false; + if (WinStartTimer(g_hOs2Ab, g_hOs2Wnd, 1 /*idEvent*/, cMsWait + 1) != 0) + { + QMSG Msg; + while (WinGetMsg(g_hOs2Ab, &Msg, NULL, 0, 0)) + { + WinDispatchMsg(g_hOs2Ab, &Msg); + if (g_fOs2TimerTicked || RTTimeMilliTS() - msStart >= cMsWait) + break; + } + + if (!WinStopTimer(g_hOs2Ab, g_hOs2Wnd, 1 /*idEvent*/)) + RTMsgWarning("WinStopTimer failed: %#x", WinGetLastError(g_hOs2Ab)); + } + else + return RTMsgErrorExitFailure("WinStartTimer(,,,%u ms) failed: %#x", cMsWait + 1, WinGetLastError(g_hOs2Ab)); + +#elif defined(RT_OS_WINDOWS) + /* + * Arm a timer which will timeout after the desired period and + * quit when we've dispatched it. + */ + g_fWinTimerTicked = false; + if (SetTimer(g_hWinWnd, 1 /*idEvent*/, cMsWait + 1, NULL /*pfnTimerProc*/) != 0) + { + MSG Msg; + while (GetMessageW(&Msg, NULL, 0, 0)) + { + TranslateMessage(&Msg); + DispatchMessageW(&Msg); + if (g_fWinTimerTicked || RTTimeMilliTS() - msStart >= cMsWait) + break; + } + + if (!KillTimer(g_hWinWnd, 1 /*idEvent*/)) + RTMsgWarning("KillTimer failed: %u (%#x)", GetLastError(), GetLastError()); + } + else + return RTMsgErrorExitFailure("SetTimer(,,%u ms,) failed: %u (%#x)", cMsWait + 1, GetLastError(), GetLastError()); + +#else +/** @todo X11 needs to run it's message queue too, because if we're offering + * things on the "clipboard" we must reply to requests for them. */ + /* + * Just a plain simple RTThreadSleep option (will probably not be used in the end): + */ + for (;;) + { + uint64_t cMsElapsed = RTTimeMilliTS() - msStart; + if (cMsElapsed >= cMsWait) + break; + RTThreadSleep(cMsWait - cMsElapsed); + } +#endif + + if (g_uVerbosity > 2) + RTMsgInfo("Done waiting after %u ms.\n", RTTimeMilliTS() - msStart); + return RTEXITCODE_SUCCESS; +} + + +/** + * Display the usage to @a pStrm. + */ +static void Usage(PRTSTREAM pStrm) +{ + RTStrmPrintf(pStrm, + "usage: %s [--get <fmt> [--get ...]] [--get-file <fmt> <file> [--get-file ...]]\n" + " %s [--zap] [--put <fmt> <content> [--put ...]] [--put-file <fmt> <file> [--put-file ...]] [--wait <ms>]\n" + " %s [--check <fmt> <expected> [--check ...]] [--check-file <fmt> <file> [--check-file ...]]\n" + " [--check-no <fmt> [--check-no ...]]\n" + , RTProcShortName(), RTProcShortName(), RTProcShortName()); + RTStrmPrintf(pStrm, "\n"); + RTStrmPrintf(pStrm, "Actions/Options:\n"); + + for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) + { + const char *pszHelp; + switch (g_aCmdOptions[i].iShort) + { + case 'l': pszHelp = "List the clipboard content."; break; + case 'g': pszHelp = "Get given clipboard format and writes it to standard output."; break; + case 'G': pszHelp = "Get given clipboard format and writes it to the specified file."; break; + case 'p': pszHelp = "Puts given format and content on the clipboard."; break; + case 'P': pszHelp = "Puts given format and file content on the clipboard."; break; + case 'c': pszHelp = "Checks that the given format and content matches the clipboard."; break; + case 'C': pszHelp = "Checks that the given format and file content matches the clipboard."; break; + case 'n': pszHelp = "Checks that the given format is not on the clipboard."; break; + case 'z': pszHelp = "Zaps the clipboard content."; break; +#ifdef MULTI_TARGET_CLIPBOARD + case 't': pszHelp = "Selects the target clipboard."; break; +#endif +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) + case 'k': pszHelp = "Closes the clipboard if open (win,os2)."; break; +#endif + case 'w': pszHelp = "Waits a given number of milliseconds before continuing."; break; + case 'v': pszHelp = "More verbose execution."; break; + case 'q': pszHelp = "Quiet execution."; break; + case 'h': pszHelp = "Displays this help and exit"; break; + case 'V': pszHelp = "Displays the program revision"; break; + + default: + pszHelp = "Option undocumented"; + break; + } + if ((unsigned)g_aCmdOptions[i].iShort < 127U) + { + char szOpt[64]; + RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); + RTStrmPrintf(pStrm, " %-19s %s\n", szOpt, pszHelp); + } + else + RTStrmPrintf(pStrm, " %-19s %s\n", g_aCmdOptions[i].pszLong, pszHelp); + } + RTStrmPrintf(pStrm, + "\n" + "Note! Options are processed in the order they are given.\n"); + + RTStrmPrintf(pStrm, "\nFormats:\n"); + for (size_t i = 0; i < RT_ELEMENTS(g_aFormats); i++) + RTStrmPrintf(pStrm, " %-12s: %s\n", g_aFormats[i].pszName, g_aFormats[i].pszDesc); + +#ifdef MULTI_TARGET_CLIPBOARD + RTStrmPrintf(pStrm, "\nTarget:\n"); + for (size_t i = 0; i < RT_ELEMENTS(g_aTargets); i++) + RTStrmPrintf(pStrm, " %-12s: %s\n", g_aTargets[i].pszName, g_aTargets[i].pszDesc); +#endif +} + + +int main(int argc, char *argv[]) +{ + /* + * Init IPRT. + */ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Host specific init. + */ +#ifdef RT_OS_DARWIN + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; +#elif defined(RT_OS_OS2) + RTEXITCODE rcExit = CuOs2Init(); +#elif defined(RT_OS_WINDOWS) + RTEXITCODE rcExit = CuWinInit(); +#else + RTEXITCODE rcExit = CuX11Init(); +#endif + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + /* + * Process options (in order). + */ + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); + while ((rc = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + RTEXITCODE rcExit2 = RTEXITCODE_SUCCESS; + switch (rc) + { +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) + case 'k': + rcExit2 = CuCloseClipboard(); + break; +#endif + + case 'l': + rcExit2 = ListClipboardContent(); + break; + + case 'g': + { + PCCLIPUTILFORMAT pFmtDesc = GetFormatDesc(ValueUnion.psz); + if (pFmtDesc) + rcExit2 = ClipboardContentToStdOut(pFmtDesc); + else + rcExit2 = RTEXITCODE_FAILURE; + break; + } + + case 'G': + { + PCCLIPUTILFORMAT pFmtDesc = GetFormatDesc(ValueUnion.psz); + if (pFmtDesc) + { + rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_STRING); + if (RT_SUCCESS(rc)) + rcExit2 = ClipboardContentToFile(pFmtDesc, ValueUnion.psz); + else + return RTMsgErrorExitFailure("No filename given with --get-file"); + } + else + rcExit2 = RTEXITCODE_FAILURE; + break; + } + + case 'p': + { + PCCLIPUTILFORMAT pFmtDesc = GetFormatDesc(ValueUnion.psz); + if (pFmtDesc) + { + rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_STRING); + if (RT_SUCCESS(rc)) + rcExit2 = PutStringOnClipboard(pFmtDesc, ValueUnion.psz); + else + return RTMsgErrorExitFailure("No data string given with --put"); + } + else + rcExit2 = RTEXITCODE_FAILURE; + break; + } + + case 'P': + { + PCCLIPUTILFORMAT pFmtDesc = GetFormatDesc(ValueUnion.psz); + if (pFmtDesc) + { + rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_STRING); + if (RT_SUCCESS(rc)) + rcExit2 = PutFileOnClipboard(pFmtDesc, ValueUnion.psz); + else + return RTMsgErrorExitFailure("No filename given with --put-file"); + } + else + rcExit2 = RTEXITCODE_FAILURE; + break; + } + + case 'c': + { + PCCLIPUTILFORMAT pFmtDesc = GetFormatDesc(ValueUnion.psz); + if (pFmtDesc) + { + rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_STRING); + if (RT_SUCCESS(rc)) + rcExit2 = CheckStringAgainstClipboard(pFmtDesc, ValueUnion.psz); + else + return RTMsgErrorExitFailure("No data string given with --check"); + } + else + rcExit2 = RTEXITCODE_FAILURE; + break; + } + + case 'C': + { + PCCLIPUTILFORMAT pFmtDesc = GetFormatDesc(ValueUnion.psz); + if (pFmtDesc) + { + rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_STRING); + if (RT_SUCCESS(rc)) + rcExit2 = CheckFileAgainstClipboard(pFmtDesc, ValueUnion.psz); + else + return RTMsgErrorExitFailure("No filename given with --check-file"); + } + else + rcExit2 = RTEXITCODE_FAILURE; + break; + } + + case 'n': + { + PCCLIPUTILFORMAT pFmtDesc = GetFormatDesc(ValueUnion.psz); + if (pFmtDesc) + rcExit2 = CheckFormatNotOnClipboard(pFmtDesc); + else + rcExit2 = RTEXITCODE_FAILURE; + break; + } + + + case 'z': + rcExit2 = ZapAllClipboardData(); + break; + +#ifdef MULTI_TARGET_CLIPBOARD + case 't': + { + CLIPUTILTARGET *pNewTarget = NULL; + for (size_t i = 0; i < RT_ELEMENTS(g_aTargets); i++) + if (strcmp(ValueUnion.psz, g_aTargets[i].pszName) == 0) + { + pNewTarget = &g_aTargets[i]; + break; + } + if (!pNewTarget) + return RTMsgErrorExitFailure("Unknown target '%s'", ValueUnion.psz); + if (pNewTarget != g_pTarget && g_uVerbosity > 0) + RTMsgInfo("Switching from '%s' to '%s'\n", g_pTarget->pszName, pNewTarget->pszName); + g_pTarget = pNewTarget; + break; + } +#endif + + case 'w': + rcExit2 = DoWait(ValueUnion.u32); + break; + + case 'q': + g_uVerbosity = 0; + break; + + case 'v': + g_uVerbosity++; + break; + + case 'h': + Usage(g_pStdOut); + return RTEXITCODE_SUCCESS; + + case 'V': + { + char szRev[] = "$Revision: 155244 $"; + szRev[RT_ELEMENTS(szRev) - 2] = '\0'; + RTPrintf(RTStrStrip(strchr(szRev, ':') + 1)); + return RTEXITCODE_SUCCESS; + } + + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + + if (rcExit2 != RTEXITCODE_SUCCESS && rcExit == RTEXITCODE_SUCCESS) + rcExit = rcExit2; + } + + /* + * Host specific cleanup. + */ +#if defined(RT_OS_OS2) + RTEXITCODE rcExit2 = CuOs2Term(); +#elif defined(RT_OS_WINDOWS) + RTEXITCODE rcExit2 = CuWinTerm(); +#else + RTEXITCODE rcExit2 = RTEXITCODE_SUCCESS; +#endif + if (rcExit2 != RTEXITCODE_SUCCESS && rcExit != RTEXITCODE_SUCCESS) + rcExit = rcExit2; + + return rcExit; +} + diff --git a/src/VBox/ValidationKit/utils/clipboard/Makefile.kmk b/src/VBox/ValidationKit/utils/clipboard/Makefile.kmk new file mode 100644 index 00000000..13e54dba --- /dev/null +++ b/src/VBox/ValidationKit/utils/clipboard/Makefile.kmk @@ -0,0 +1,53 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Clipboard tests. +# + +# +# Copyright (C) 2021-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Clipboard Utility. +# +PROGRAMS += ClipUtil +ClipUtil_TEMPLATE = VBoxValidationKitR3 +ClipUtil_SOURCES = ClipUtil.cpp +ifn1of ($(KBUILD_TARGET), darwin os2 win) + ClipUtil_LIBPATH = $(VBOX_LIBPATH_X11) + ClipUtil_LIBS = X11 Xmu +endif + +$(evalcall def_vbox_validationkit_process_python_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/cpu/Makefile.kmk b/src/VBox/ValidationKit/utils/cpu/Makefile.kmk new file mode 100644 index 00000000..219f928b --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/Makefile.kmk @@ -0,0 +1,84 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - CPU Test Utilities. +# + +# +# Copyright (C) 2009-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +if1of ($(KBUILD_TARGET_ARCH), x86 amd64) + PROGRAMS += xmmsaving + xmmsaving_TEMPLATE = VBoxValidationKitR3 + xmmsaving_SOURCES = xmmsaving.cpp xmmsaving-asm.asm +endif + +if1of ($(KBUILD_TARGET_ARCH), x86 amd64) + PROGRAMS += exceptionsR3 + exceptionsR3_TEMPLATE = VBoxValidationKitR3 + exceptionsR3_SOURCES = exceptionsR3.cpp exceptionsR3-asm.asm +endif + +PROGRAMS += cpu-numa +cpu-numa_TEMPLATE = VBoxValidationKitR3 +cpu-numa_SOURCES = cpu-numa.cpp + +PROGRAMS += cpu-alloc-all-mem +cpu-alloc-all-mem_TEMPLATE = VBoxValidationKitR3 +cpu-alloc-all-mem_SOURCES = cpu-alloc-all-mem.cpp + +if1of ($(KBUILD_TARGET_ARCH), x86 amd64) + ifneq ($(KBUILD_HOST),os2) + PROGRAMS += cidet-app + endif + cidet-app_TEMPLATE = VBoxValidationKitR3 + cidet-app_SOURCES = \ + cidet-app.cpp \ + cidet-appA.asm \ + cidet-core.cpp \ + cidet-instr-1.cpp + cidet-app_DEFS = IN_DIS + cidet-app_DEFS.linux = CIDET_REDUCED_CTX + cidet-app_LIBS = $(PATH_STAGE_LIB)/DisasmR3Static$(VBOX_SUFF_LIB) + cidet-app_VBOX_IMPORT_CHECKER.win.x86 = $(NO_SUCH_VARIABLE) # doesn't work on NT4 yet. +endif + +if1of ($(KBUILD_TARGET_ARCH), x86 amd64) + PROGRAMS += rdtsc + rdtsc_TEMPLATE = VBoxValidationKitR3 + rdtsc_SOURCES = rdtsc.cpp rdtsc-asm.asm +endif + + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/cpu/cidet-app.cpp b/src/VBox/ValidationKit/utils/cpu/cidet-app.cpp new file mode 100644 index 00000000..40ab8e7b --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cidet-app.cpp @@ -0,0 +1,1376 @@ +/* $Id: cidet-app.cpp $ */ +/** @file + * CPU Instruction Decoding & Execution Tests - Ring-3 Driver Application. + */ + +/* + * Copyright (C) 2014-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include "cidet.h" + +#include <iprt/asm-amd64-x86.h> +#include <iprt/buildconfig.h> +#include <iprt/err.h> +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/param.h> +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#ifdef RT_OS_WINDOWS +# include <iprt/win/windows.h> +#else +# define USE_SIGNALS +# include <signal.h> +# include <unistd.h> +# include <sys/ucontext.h> +#endif + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** @def CIDET_LEAVE_GS_ALONE + * Leave GS alone on 64-bit darwin (gs is 0, no ldt or gdt entry to load that'll + * restore the lower 32-bits of the base when saving and restoring the register). + */ +#if (defined(RT_OS_DARWIN) && defined(RT_ARCH_AMD64)) || defined(DOXYGEN_RUNNING) +# define CIDET_LEAVE_GS_ALONE +#endif + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** + * CIDET driver app buffer. + */ +typedef struct CIDETAPPBUF +{ + /** The buffer size. */ + size_t cb; + /** The normal allocation. + * There is a fence page before this as well as at pbNormal+cb. */ + uint8_t *pbNormal; + /** The low memory allocation (32-bit addressable if 64-bit host, 16-bit + * addressable if 32-bit host). */ + uint8_t *pbLow; + /** Set if we're using the normal buffer, clear if it's the low one. */ + bool fUsingNormal : 1; + /** Set if the buffer is armed, clear if mostly accessible. */ + bool fArmed : 1; + /** Set if this is a code buffer. */ + bool fIsCode : 1; + /** The memory protection for the pages (RTMEM_PROT_XXX). */ + uint8_t fDefaultProt : 3; + /** The memory protection for the last page (RTMEM_PROT_XXX). */ + uint8_t fLastPageProt : 3; + /** The buffer index. */ + uint16_t idxCfg; +} CIDETAPPBUF; +/** Pointer to a CIDET driver app buffer. */ +typedef CIDETAPPBUF *PCIDETAPPBUF; + +/** Number of code buffers. */ +#define CIDETAPP_CODE_BUF_COUNT 1 +/** Number of data buffers. */ +#define CIDETAPP_DATA_BUF_COUNT 1 + + +/** + * CIDET driver app instance. + */ +typedef struct CIDETAPP +{ + /** The core structure. */ + CIDETCORE Core; + /** The execute return context. */ + CIDETCPUCTX ExecuteCtx; + /** Code buffers (runs parallel to g_aCodeBufCfgs). */ + CIDETAPPBUF aCodeBuffers[CIDETAPP_CODE_BUF_COUNT]; + /** Data buffers (runs parallel to g_aDataBufCfgs). */ + CIDETAPPBUF aDataBuffers[CIDETAPP_DATA_BUF_COUNT]; + + /** The lowest stack address. */ + uint8_t *pbStackLow; + /** The end of the stack allocation (highest address). */ + uint8_t *pbStackEnd; + /** Stack size (= pbStackEnd - pbStackLow). */ + uint32_t cbStack; + /** Whether we're currently using the 'lock int3' to deal with tricky stack. */ + bool fUsingLockedInt3; +} CIDETAPP; +/** Pointer to a CIDET driver app instance. */ +typedef CIDETAPP *PCIDETAPP; +/** Pointer to a pointer to a CIDET driver app instance. */ +typedef PCIDETAPP *PPCIDETAPP; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** The test instance handle. */ +static RTTEST g_hTest; +/** Points to the instance data while executing, NULL if not executing or if + * we've already handled the first exception while executing. */ +static PCIDETAPP volatile g_pExecutingThis; +#ifdef USE_SIGNALS +/** The default signal mask. */ +static sigset_t g_ProcSigMask; +/** The alternative signal stack. */ +static stack_t g_AltStack; +#endif + + +/** Code buffer configurations (parallel to CIDETAPP::aCodeBuffers). */ +static CIDETBUFCFG g_aCodeBufCfgs[CIDETAPP_CODE_BUF_COUNT] = +{ + { + "Normal", + CIDETBUF_PROT_RWX | CIDETBUF_DPL_3 | CIDETBUF_DPL_SAME | CIDETBUF_SEG_ER | CIDETBUF_KIND_CODE, + }, +}; + +/** Data buffer configurations (parallel to CIDETAPP::aDataBuffers). */ +static CIDETBUFCFG g_aDataBufCfgs[CIDETAPP_DATA_BUF_COUNT] = +{ + { + "Normal", + CIDETBUF_PROT_RWX | CIDETBUF_DPL_3 | CIDETBUF_DPL_SAME | CIDETBUF_SEG_RW | CIDETBUF_KIND_DATA, + }, +}; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +DECLASM(void) CidetAppSaveAndRestoreCtx(void); +DECLASM(void) CidetAppRestoreCtx(PCCIDETCPUCTX pRestoreCtx); +DECLASM(void) CidetAppExecute(PCIDETCPUCTX pSaveCtx, PCCIDETCPUCTX pRestoreCtx); + + +/* + * + * + * Exception and signal handling. + * Exception and signal handling. + * Exception and signal handling. + * + * + */ + +#ifdef RT_OS_WINDOWS +static int CidetAppXcptFilter(EXCEPTION_POINTERS *pXcptPtrs) RT_NOTHROW_DEF +{ + /* + * Grab the this point. We expect at most one signal. + */ + PCIDETAPP pThis = g_pExecutingThis; + g_pExecutingThis = NULL; + if (pThis == NULL) + { + /* we're up the infamous creek... */ + for (;;) ExitProcess(2); + } + + /* + * Gather CPU state information from the context structure. + */ + CONTEXT *pSrcCtx = pXcptPtrs->ContextRecord; +# ifdef RT_ARCH_AMD64 + if ( (pSrcCtx->ContextFlags & (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)) + != (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)) + __debugbreak(); + pThis->Core.ActualCtx.rip = pSrcCtx->Rip; + pThis->Core.ActualCtx.rfl = pSrcCtx->EFlags; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xAX] = pSrcCtx->Rax; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xCX] = pSrcCtx->Rcx; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDX] = pSrcCtx->Rdx; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBX] = pSrcCtx->Rbx; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSP] = pSrcCtx->Rsp; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBP] = pSrcCtx->Rbp; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSI] = pSrcCtx->Rsi; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDI] = pSrcCtx->Rdi; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x8] = pSrcCtx->R8; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x9] = pSrcCtx->R9; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x10] = pSrcCtx->R10; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x11] = pSrcCtx->R11; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x12] = pSrcCtx->R12; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x13] = pSrcCtx->R13; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x14] = pSrcCtx->R14; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x15] = pSrcCtx->R15; + pThis->Core.ActualCtx.aSRegs[X86_SREG_ES] = pSrcCtx->SegEs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_CS] = pSrcCtx->SegCs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_SS] = pSrcCtx->SegSs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_DS] = pSrcCtx->SegDs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_FS] = pSrcCtx->SegFs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_GS] = pSrcCtx->SegGs; + if (pSrcCtx->ContextFlags & CONTEXT_FLOATING_POINT) + { + /* ... */ + } + if (pSrcCtx->ContextFlags & CONTEXT_DEBUG_REGISTERS) + { + /* ... */ + } + +# elif defined(RT_ARCH_X86) + if ( (pSrcCtx->ContextFlags & (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)) + != (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)) + __debugbreak(); + pThis->Core.ActualCtx.rip = pSrcCtx->Eip; + pThis->Core.ActualCtx.rfl = pSrcCtx->EFlags; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xAX] = pSrcCtx->Eax; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xCX] = pSrcCtx->Ecx; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDX] = pSrcCtx->Edx; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBX] = pSrcCtx->Ebx; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSP] = pSrcCtx->Esp; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBP] = pSrcCtx->Ebp; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSI] = pSrcCtx->Esi; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDI] = pSrcCtx->Edi; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x8] = 0; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x9] = 0; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x10] = 0; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x11] = 0; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x12] = 0; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x13] = 0; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x14] = 0; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x15] = 0; + pThis->Core.ActualCtx.aSRegs[X86_SREG_ES] = pSrcCtx->SegEs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_CS] = pSrcCtx->SegCs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_SS] = pSrcCtx->SegSs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_DS] = pSrcCtx->SegDs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_FS] = pSrcCtx->SegFs; + pThis->Core.ActualCtx.aSRegs[X86_SREG_GS] = pSrcCtx->SegGs; + if (pSrcCtx->ContextFlags & CONTEXT_FLOATING_POINT) + { + /* ... */ + } + if (pSrcCtx->ContextFlags & CONTEXT_EXTENDED_REGISTERS) + { + /* ... */ + } + if (pSrcCtx->ContextFlags & CONTEXT_DEBUG_REGISTERS) + { + /* ... */ + } +# else +# error "Not supported" +# endif + + /* + * Add/Adjust CPU state information according to the exception code. + */ + pThis->Core.ActualCtx.uErr = UINT64_MAX; + switch (pXcptPtrs->ExceptionRecord->ExceptionCode) + { + case EXCEPTION_INT_DIVIDE_BY_ZERO: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_DE; + break; + case EXCEPTION_SINGLE_STEP: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_DB; + break; + case EXCEPTION_BREAKPOINT: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_BP; + break; + case EXCEPTION_INT_OVERFLOW: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_OF; + break; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_BR; + break; + case EXCEPTION_ILLEGAL_INSTRUCTION: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_UD; + break; + + case EXCEPTION_PRIV_INSTRUCTION: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_GP; + pThis->Core.ActualCtx.uErr = 0; + break; + + case EXCEPTION_ACCESS_VIOLATION: + { + pThis->Core.ActualCtx.uXcpt = X86_XCPT_PF; + pThis->Core.ActualCtx.cr2 = pXcptPtrs->ExceptionRecord->ExceptionInformation[1]; + pThis->Core.ActualCtx.uErr = 0; + if (pXcptPtrs->ExceptionRecord->ExceptionInformation[0] == EXCEPTION_WRITE_FAULT) + pThis->Core.ActualCtx.uErr = X86_TRAP_PF_RW; + else if (pXcptPtrs->ExceptionRecord->ExceptionInformation[0] == EXCEPTION_EXECUTE_FAULT) + pThis->Core.ActualCtx.uErr = X86_TRAP_PF_ID; + else if (pXcptPtrs->ExceptionRecord->ExceptionInformation[0] != EXCEPTION_READ_FAULT) + AssertFatalFailed(); + + MEMORY_BASIC_INFORMATION MemInfo = {0}; + if (VirtualQuery((PVOID)pXcptPtrs->ExceptionRecord->ExceptionInformation[1], &MemInfo, sizeof(MemInfo)) > 0) + switch (MemInfo.Protect & 0xff) + { + case PAGE_NOACCESS: + break; + case PAGE_READONLY: + case PAGE_READWRITE: + case PAGE_WRITECOPY: + case PAGE_EXECUTE: + case PAGE_EXECUTE_READ: + case PAGE_EXECUTE_READWRITE: + case PAGE_EXECUTE_WRITECOPY: + pThis->Core.ActualCtx.uErr |= X86_TRAP_PF_P; + break; + default: + AssertFatalFailed(); + } + break; + } + + case EXCEPTION_FLT_DENORMAL_OPERAND: + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + case EXCEPTION_FLT_INEXACT_RESULT: + case EXCEPTION_FLT_INVALID_OPERATION: + case EXCEPTION_FLT_OVERFLOW: + case EXCEPTION_FLT_STACK_CHECK: + case EXCEPTION_FLT_UNDERFLOW: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_MF; + break; + + case EXCEPTION_DATATYPE_MISALIGNMENT: + pThis->Core.ActualCtx.uXcpt = X86_XCPT_AC; + break; + + default: + pThis->Core.ActualCtx.uXcpt = pXcptPtrs->ExceptionRecord->ExceptionCode; + break; + } + + /* + * Our own personal long jump implementation. + */ + CidetAppRestoreCtx(&pThis->ExecuteCtx); + + /* Won't return...*/ + return EXCEPTION_EXECUTE_HANDLER; +} + + +/** + * Vectored exception handler. + * + * @returns Long jumps or terminates the process. + * @param pXcptPtrs The exception record. + */ +static LONG CALLBACK CidetAppVectoredXcptHandler(EXCEPTION_POINTERS *pXcptPtrs) RT_NOTHROW_DEF +{ + RTStrmPrintf(g_pStdErr, "CidetAppVectoredXcptHandler!\n"); + CidetAppXcptFilter(pXcptPtrs); + + /* won't get here. */ + return EXCEPTION_CONTINUE_SEARCH; +} + + +/** + * Unhandled exception filter. + * + * @returns Long jumps or terminates the process. + * @param pXcptPtrs The exception record. + */ +static LONG CALLBACK CidetAppUnhandledXcptFilter(EXCEPTION_POINTERS *pXcptPtrs) RT_NOTHROW_DEF +{ + RTStrmPrintf(g_pStdErr, "CidetAppUnhandledXcptFilter!\n"); + CidetAppXcptFilter(pXcptPtrs); + + /* won't get here. */ + return EXCEPTION_CONTINUE_SEARCH; +} + + +#elif defined(USE_SIGNALS) +/** + * Signal handler. + */ +static void CidetAppSigHandler(int iSignal, siginfo_t *pSigInfo, void *pvCtx) +{ +# if 1 + if ( !g_pExecutingThis + || !g_pExecutingThis->fUsingLockedInt3 + || iSignal != SIGILL) + { + RTStrmPrintf(g_pStdErr, "signal %d pSigInfo=%p pvCtx=%p", iSignal, pSigInfo, pvCtx); + if (pSigInfo) + RTStrmPrintf(g_pStdErr, " si_addr=%p si_code=%#x sival_ptr=%p sival_int=%d", + pSigInfo->si_addr, pSigInfo->si_code, pSigInfo->si_value.sival_ptr, pSigInfo->si_value.sival_int); + RTStrmPrintf(g_pStdErr, "\n"); + } +# endif + + /* + * Grab the this point. We expect at most one signal. + */ + PCIDETAPP pThis = g_pExecutingThis; + g_pExecutingThis = NULL; + if (pThis == NULL) + { + /* we're up the infamous creek... */ + RTStrmPrintf(g_pStdErr, "Creek time!\n"); + for (;;) _exit(2); + } + + /* + * Gather all the CPU state information available. + */ +# ifdef RT_OS_LINUX + ucontext_t const *pCtx = (ucontext_t const *)pvCtx; +# ifdef RT_ARCH_AMD64 + + pThis->Core.ActualCtx.aGRegs[X86_GREG_xAX] = pCtx->uc_mcontext.gregs[REG_RAX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xCX] = pCtx->uc_mcontext.gregs[REG_RCX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDX] = pCtx->uc_mcontext.gregs[REG_RDX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBX] = pCtx->uc_mcontext.gregs[REG_RBX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSP] = pCtx->uc_mcontext.gregs[REG_RSP]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBP] = pCtx->uc_mcontext.gregs[REG_RBP]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSI] = pCtx->uc_mcontext.gregs[REG_RSI]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDI] = pCtx->uc_mcontext.gregs[REG_RDI]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x8 ] = pCtx->uc_mcontext.gregs[REG_R8]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x9 ] = pCtx->uc_mcontext.gregs[REG_R9]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x10] = pCtx->uc_mcontext.gregs[REG_R10]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x11] = pCtx->uc_mcontext.gregs[REG_R11]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x12] = pCtx->uc_mcontext.gregs[REG_R12]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x13] = pCtx->uc_mcontext.gregs[REG_R13]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x14] = pCtx->uc_mcontext.gregs[REG_R14]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_x15] = pCtx->uc_mcontext.gregs[REG_R15]; + pThis->Core.ActualCtx.aSRegs[X86_SREG_CS] = RT_LO_U16((uint32_t)pCtx->uc_mcontext.gregs[REG_CSGSFS]); + pThis->Core.ActualCtx.aSRegs[X86_SREG_GS] = RT_HI_U16((uint32_t)pCtx->uc_mcontext.gregs[REG_CSGSFS]); + pThis->Core.ActualCtx.aSRegs[X86_SREG_FS] = (uint16_t)RT_HI_U32(pCtx->uc_mcontext.gregs[REG_CSGSFS]); + pThis->Core.ActualCtx.aSRegs[X86_SREG_DS] = ASMGetDS(); + pThis->Core.ActualCtx.aSRegs[X86_SREG_ES] = ASMGetES(); + pThis->Core.ActualCtx.aSRegs[X86_SREG_SS] = ASMGetSS(); + pThis->Core.ActualCtx.rip = pCtx->uc_mcontext.gregs[REG_RIP]; + pThis->Core.ActualCtx.rfl = pCtx->uc_mcontext.gregs[REG_EFL]; + pThis->Core.ActualCtx.cr2 = pCtx->uc_mcontext.gregs[REG_CR2]; + pThis->Core.ActualCtx.uXcpt = pCtx->uc_mcontext.gregs[REG_TRAPNO]; + pThis->Core.ActualCtx.uErr = pCtx->uc_mcontext.gregs[REG_ERR]; + + /* Fudge the FS and GS registers as setup_sigcontext returns 0. */ + if (pThis->Core.ActualCtx.aSRegs[X86_SREG_FS] == 0) + pThis->Core.ActualCtx.aSRegs[X86_SREG_FS] = pThis->Core.ExpectedCtx.aSRegs[X86_SREG_FS]; + if (pThis->Core.ActualCtx.aSRegs[X86_SREG_GS] == 0) + pThis->Core.ActualCtx.aSRegs[X86_SREG_GS] = pThis->Core.ExpectedCtx.aSRegs[X86_SREG_GS]; + +# elif defined(RT_ARCH_X86) + pThis->Core.ActualCtx.aGRegs[X86_GREG_xAX] = pCtx->uc_mcontext.gregs[REG_EAX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xCX] = pCtx->uc_mcontext.gregs[REG_ECX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDX] = pCtx->uc_mcontext.gregs[REG_EDX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBX] = pCtx->uc_mcontext.gregs[REG_EBX]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSP] = pCtx->uc_mcontext.gregs[REG_ESP]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xBP] = pCtx->uc_mcontext.gregs[REG_EBP]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xSI] = pCtx->uc_mcontext.gregs[REG_ESI]; + pThis->Core.ActualCtx.aGRegs[X86_GREG_xDI] = pCtx->uc_mcontext.gregs[REG_EDI]; + pThis->Core.ActualCtx.aSRegs[X86_SREG_CS] = pCtx->uc_mcontext.gregs[REG_CS]; + pThis->Core.ActualCtx.aSRegs[X86_SREG_DS] = pCtx->uc_mcontext.gregs[REG_DS]; + pThis->Core.ActualCtx.aSRegs[X86_SREG_ES] = pCtx->uc_mcontext.gregs[REG_ES]; + pThis->Core.ActualCtx.aSRegs[X86_SREG_FS] = pCtx->uc_mcontext.gregs[REG_FS]; + pThis->Core.ActualCtx.aSRegs[X86_SREG_GS] = pCtx->uc_mcontext.gregs[REG_GS]; + pThis->Core.ActualCtx.aSRegs[X86_SREG_SS] = pCtx->uc_mcontext.gregs[REG_SS]; + pThis->Core.ActualCtx.rip = pCtx->uc_mcontext.gregs[REG_EIP]; + pThis->Core.ActualCtx.rfl = pCtx->uc_mcontext.gregs[REG_EFL]; + pThis->Core.ActualCtx.cr2 = pCtx->uc_mcontext.cr2; + pThis->Core.ActualCtx.uXcpt = pCtx->uc_mcontext.gregs[REG_TRAPNO]; + pThis->Core.ActualCtx.uErr = pCtx->uc_mcontext.gregs[REG_ERR]; + +# else +# error "Unsupported arch." +# endif + + /* Adjust uErr. */ + switch (pThis->Core.ActualCtx.uXcpt) + { + case X86_XCPT_TS: + case X86_XCPT_NP: + case X86_XCPT_SS: + case X86_XCPT_GP: + case X86_XCPT_PF: + case X86_XCPT_AC: + case X86_XCPT_DF: + break; + default: + pThis->Core.ActualCtx.uErr = UINT64_MAX; + break; + } + +# if 0 + /* Fudge the resume flag (it's probably always set here). */ + if ( (pThis->Core.ActualCtx.rfl & X86_EFL_RF) + && !(pThis->Core.ExpectedCtx.rfl & X86_EFL_RF)) + pThis->Core.ActualCtx.rfl &= ~X86_EFL_RF; +# endif + +# else + /** @todo */ +# endif + + + /* + * Check for the 'lock int3' instruction used for tricky stacks. + */ + if ( pThis->fUsingLockedInt3 + && pThis->Core.ActualCtx.uXcpt == X86_XCPT_UD + && pThis->Core.ActualCtx.rip == pThis->Core.CodeBuf.uEffBufAddr - pThis->Core.CodeBuf.offSegBase + + pThis->Core.CodeBuf.offActive + pThis->Core.CodeBuf.cbActive ) + { + pThis->Core.ActualCtx.uXcpt = UINT32_MAX; + Assert(pThis->Core.ActualCtx.uErr == UINT64_MAX); + pThis->Core.ActualCtx.rfl &= ~X86_EFL_RF; + } + + /* + * Jump back to CidetAppCbExecute. + */ + CidetAppRestoreCtx(&pThis->ExecuteCtx); +} +#endif + + + +/* + * + * Buffer handling + * Buffer handling + * Buffer handling + * + * + */ + +static int cidetAppAllocateAndConfigureOneBuffer(PCIDETAPP pThis, PCIDETAPPBUF pBuf, uint16_t idxBuf, bool fIsCode, + uint32_t fFlags) +{ + RT_NOREF_PV(pThis); + static uint8_t const s_afBufProtToDefaultMemProt[] = + { + /* [0] = */ RTMEM_PROT_NONE, + /* [1] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE | RTMEM_PROT_EXEC, + /* [2] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE, + /* [3] = */ RTMEM_PROT_READ | RTMEM_PROT_EXEC, + /* [4] = */ RTMEM_PROT_READ, + /* [5] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE | RTMEM_PROT_EXEC, + /* [6] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE | RTMEM_PROT_EXEC, + /* [7] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE | RTMEM_PROT_EXEC, + /* [8] = */ RTMEM_PROT_NONE, + /* [9] = */ RTMEM_PROT_NONE, + /* [10] = */ RTMEM_PROT_NONE, + /* [11] = */ RTMEM_PROT_NONE, + /* [12] = */ RTMEM_PROT_NONE, + /* [13] = */ RTMEM_PROT_NONE, + /* [14] = */ RTMEM_PROT_NONE, + /* [15] = */ RTMEM_PROT_NONE, + }; + static uint8_t const s_afBufProtToLastPageMemProt[] = + { + /* [0] = */ RTMEM_PROT_NONE, + /* [1] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE | RTMEM_PROT_EXEC, + /* [2] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE, + /* [3] = */ RTMEM_PROT_READ | RTMEM_PROT_EXEC, + /* [4] = */ RTMEM_PROT_READ, + /* [5] = */ RTMEM_PROT_NONE, + /* [6] = */ RTMEM_PROT_READ | RTMEM_PROT_WRITE, + /* [7] = */ RTMEM_PROT_READ, + /* [8] = */ RTMEM_PROT_NONE, + /* [9] = */ RTMEM_PROT_NONE, + /* [10] = */ RTMEM_PROT_NONE, + /* [11] = */ RTMEM_PROT_NONE, + /* [12] = */ RTMEM_PROT_NONE, + /* [13] = */ RTMEM_PROT_NONE, + /* [14] = */ RTMEM_PROT_NONE, + /* [15] = */ RTMEM_PROT_NONE, + }; + + int rc; + Assert(CIDETBUF_IS_CODE(fFlags) == fIsCode); + pBuf->fIsCode = fIsCode; + pBuf->idxCfg = idxBuf; + pBuf->fUsingNormal = true; + pBuf->fDefaultProt = s_afBufProtToDefaultMemProt[fFlags & CIDETBUF_PROT_MASK]; + pBuf->fLastPageProt = s_afBufProtToLastPageMemProt[fFlags & CIDETBUF_PROT_MASK]; + if (pBuf->fDefaultProt != RTMEM_PROT_NONE) + { + /* + * Allocate a 3 page buffer plus two fence pages. + */ + pBuf->cb = fIsCode ? CIDET_CODE_BUF_SIZE : CIDET_DATA_BUF_SIZE; + pBuf->pbNormal = (uint8_t *)RTMemPageAlloc(PAGE_SIZE + pBuf->cb + PAGE_SIZE); + if (pBuf->pbNormal) + { + memset(pBuf->pbNormal, 0x55, PAGE_SIZE); + memset(pBuf->pbNormal + PAGE_SIZE, 0xcc, pBuf->cb); + memset(pBuf->pbNormal + PAGE_SIZE + pBuf->cb, 0x77, PAGE_SIZE); + + /* Set up fence pages. */ + rc = RTMemProtect(pBuf->pbNormal, PAGE_SIZE, RTMEM_PROT_NONE); /* fence */ + if (RT_SUCCESS(rc)) + rc = RTMemProtect(pBuf->pbNormal + PAGE_SIZE + pBuf->cb, PAGE_SIZE, RTMEM_PROT_NONE); /* fence */ + pBuf->pbNormal += PAGE_SIZE; + + /* Default protection + read + write. */ + if (RT_SUCCESS(rc)) + rc = RTMemProtect(pBuf->pbNormal, pBuf->cb, pBuf->fDefaultProt | RTMEM_PROT_READ | RTMEM_PROT_WRITE); + + /* + * Allocate a low memory buffer or LDT if necessary. + */ + if ( RT_SUCCESS(rc) + && (uintptr_t)pBuf->pbNormal + pBuf->cb > RT_BIT_64(sizeof(uintptr_t) / 2 * 8)) + { + /** @todo Buffers for the other addressing mode. */ + pBuf->pbLow = NULL; + } + else + pBuf->pbLow = pBuf->pbNormal; + if (RT_SUCCESS(rc)) + return VINF_SUCCESS; + + } + else + rc = RTTestIFailedRc(VERR_NO_PAGE_MEMORY, "Error allocating three pages."); + } + else + rc = RTTestIFailedRc(VERR_NO_PAGE_MEMORY, "Unsupported buffer config: fFlags=%#x, idxBuf=%u", fFlags, idxBuf); + return rc; +} + + +static void CidetAppDeleteBuffer(PCIDETAPPBUF pBuf) +{ + RTMemProtect(pBuf->pbNormal - PAGE_SIZE, PAGE_SIZE + pBuf->cb + PAGE_SIZE, RTMEM_PROT_READ | RTMEM_PROT_WRITE); + RTMemPageFree(pBuf->pbNormal - PAGE_SIZE, PAGE_SIZE + pBuf->cb + PAGE_SIZE); + if (pBuf->pbLow != pBuf->pbNormal && pBuf->pbLow) + { + RTMemProtect(pBuf->pbLow, pBuf->cb, RTMEM_PROT_READ | RTMEM_PROT_WRITE); + RTMemFreeEx(pBuf->pbLow, pBuf->cb); + } +} + + +static bool CidetAppArmBuf(PCIDETAPP pThis, PCIDETAPPBUF pAppBuf) +{ + RT_NOREF_PV(pThis); + uint8_t *pbUsingBuf = (pAppBuf->fUsingNormal ? pAppBuf->pbNormal : pAppBuf->pbLow); + if (pAppBuf->fLastPageProt == pAppBuf->fDefaultProt) + { + if ((pAppBuf->fDefaultProt & (RTMEM_PROT_READ | RTMEM_PROT_WRITE)) != (RTMEM_PROT_READ | RTMEM_PROT_WRITE)) + RTTESTI_CHECK_RC_RET(RTMemProtect(pbUsingBuf, pAppBuf->cb, pAppBuf->fDefaultProt), VINF_SUCCESS, false); + } + else + { + if ((pAppBuf->fDefaultProt & (RTMEM_PROT_READ | RTMEM_PROT_WRITE)) != (RTMEM_PROT_READ | RTMEM_PROT_WRITE)) + RTTESTI_CHECK_RC_RET(RTMemProtect(pbUsingBuf, pAppBuf->cb - PAGE_SIZE, pAppBuf->fDefaultProt), VINF_SUCCESS, false); + RTTESTI_CHECK_RC_RET(RTMemProtect(pbUsingBuf + pAppBuf->cb - PAGE_SIZE, PAGE_SIZE, pAppBuf->fLastPageProt), + VINF_SUCCESS, false); + } + pAppBuf->fArmed = true; + return true; +} + + +static bool CidetAppDearmBuf(PCIDETAPP pThis, PCIDETAPPBUF pAppBuf) +{ + RT_NOREF_PV(pThis); + uint8_t *pbUsingBuf = (pAppBuf->fUsingNormal ? pAppBuf->pbNormal : pAppBuf->pbLow); + int rc = RTMemProtect(pbUsingBuf, pAppBuf->cb, pAppBuf->fDefaultProt | RTMEM_PROT_READ | RTMEM_PROT_WRITE); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTMemProtect failed on %s buf #%u: %Rrc", pAppBuf->fIsCode ? "code" : "data", pAppBuf->idxCfg, rc); + return false; + } + pAppBuf->fArmed = false; + return true; +} + + +/** + * @interface_method_impl{CIDETCORE,pfnReInitDataBuf} + */ +static DECLCALLBACK(bool) CidetAppCbReInitDataBuf(PCIDETCORE pThis, PCIDETBUF pBuf) +{ + PCIDETAPP pThisApp = (PCIDETAPP)pThis; + PCIDETAPPBUF pAppBuf = &pThisApp->aDataBuffers[pBuf->idxCfg]; + Assert(CIDETBUF_IS_DATA(pBuf->pCfg->fFlags)); + + /* + * De-arm the buffer. + */ + if (pAppBuf->fArmed) + if (RT_UNLIKELY(!CidetAppDearmBuf(pThisApp, pAppBuf))) + return false; + + /* + * Check the allocation requirements. + */ + if (RT_UNLIKELY((size_t)pBuf->off + pBuf->cb > pAppBuf->cb)) + { + RTTestIFailed("Buffer too small; off=%#x cb=%#x pAppBuf->cb=%#x (%s)", + pBuf->off, pBuf->cb, pAppBuf->cb, pBuf->pCfg->pszName); + return false; + } + + /* + * Do we need to use the low buffer? Check that we have one, if we need it. + */ + bool fUseNormal = pThis->cbAddrMode == ARCH_BITS / 8; + if (!fUseNormal && !pAppBuf->pbLow) + return false; + + /* + * Update the state. + */ + pAppBuf->fUsingNormal = fUseNormal; + + pBuf->offActive = pBuf->off; + pBuf->cbActive = pBuf->cb; + pBuf->cbPrologue = 0; + pBuf->cbEpilogue = 0; + pBuf->uSeg = UINT32_MAX; + pBuf->cbActiveSegLimit = UINT64_MAX; + pBuf->uSegBase = 0; + if (fUseNormal) + pBuf->uEffBufAddr = (uintptr_t)pAppBuf->pbNormal; + else + pBuf->uEffBufAddr = (uintptr_t)pAppBuf->pbLow; + + return true; +} + + +/** + * @interface_method_impl{CIDETCORE,pfnSetupDataBuf} + */ +static DECLCALLBACK(bool) CidetAppCbSetupDataBuf(PCIDETCORE pThis, PCIDETBUF pBuf, void const *pvSrc) +{ + PCIDETAPP pThisApp = (PCIDETAPP)pThis; + PCIDETAPPBUF pAppBuf = &pThisApp->aDataBuffers[pBuf->idxCfg]; + Assert(CIDETBUF_IS_DATA(pBuf->pCfg->fFlags)); + Assert(!pAppBuf->fArmed); + + + /* + * Copy over the data. + */ + uint8_t *pbUsingBuf = (pAppBuf->fUsingNormal ? pAppBuf->pbNormal : pAppBuf->pbLow); + memcpy(pbUsingBuf + pBuf->offActive, pvSrc, pBuf->cbActive); + + /* + * Arm the buffer. + */ + return CidetAppArmBuf(pThisApp, pAppBuf); +} + + +/** + * @interface_method_impl{CIDETCORE,pfnIsBufEqual} + */ +static DECLCALLBACK(bool) CidetAppCbIsBufEqual(PCIDETCORE pThis, struct CIDETBUF *pBuf, void const *pvExpected) +{ + PCIDETAPP pThisApp = (PCIDETAPP)pThis; + PCIDETAPPBUF pAppBuf = CIDETBUF_IS_CODE(pBuf->pCfg->fFlags) + ? &pThisApp->aCodeBuffers[pBuf->idxCfg] + : &pThisApp->aDataBuffers[pBuf->idxCfg]; + + /* + * Disarm the buffer if we can't read it all. + */ + if ( pAppBuf->fArmed + && ( !(pAppBuf->fLastPageProt & RTMEM_PROT_READ) + || !(pAppBuf->fDefaultProt & RTMEM_PROT_READ)) ) + if (RT_UNLIKELY(!CidetAppDearmBuf(pThisApp, pAppBuf))) + return false; + + /* + * Do the comparing. + */ + uint8_t *pbUsingBuf = (pAppBuf->fUsingNormal ? pAppBuf->pbNormal : pAppBuf->pbLow); + if (memcmp(pbUsingBuf + pBuf->offActive, pvExpected, pBuf->cbActive) != 0) + { + /** @todo RTMEM_PROT_NONE may kill content on some hosts... */ + return false; + } + + /** @todo check padding. */ + return true; +} + + +/* + * + * Code buffer, prologue, epilogue, and execution. + * Code buffer, prologue, epilogue, and execution. + * Code buffer, prologue, epilogue, and execution. + * + * + */ + + +/** + * @interface_method_impl{CIDETCORE,pfnReInitCodeBuf} + */ +static DECLCALLBACK(bool) CidetAppCbReInitCodeBuf(PCIDETCORE pThis, PCIDETBUF pBuf) +{ + PCIDETAPP pThisApp = (PCIDETAPP)pThis; + PCIDETAPPBUF pAppBuf = &pThisApp->aCodeBuffers[pBuf->idxCfg]; + Assert(CIDETBUF_IS_CODE(pBuf->pCfg->fFlags)); + Assert(pAppBuf->fUsingNormal); + + /* + * De-arm the buffer. + */ + if (pAppBuf->fArmed) + if (RT_UNLIKELY(!CidetAppDearmBuf(pThisApp, pAppBuf))) + return false; + + /* + * Determin the prologue and epilogue sizes. + */ + uint16_t cbPrologue = 0; + uint16_t cbEpilogue = ARCH_BITS == 64 ? 0x56 : 0x4e; + if (pThis->InCtx.fTrickyStack) + cbEpilogue = 16; + + /* + * Check the allocation requirements. + */ + if (RT_UNLIKELY( cbPrologue > pBuf->off + || (size_t)pBuf->off + pBuf->cb + cbEpilogue > pAppBuf->cb)) + { + RTTestIFailed("Buffer too small; off=%#x cb=%#x cbPro=%#x cbEpi=%#x pAppBuf->cb=%#x (%s)", + pBuf->off, pBuf->cb, cbPrologue, cbEpilogue, pAppBuf->cb, pBuf->pCfg->pszName); + return false; + } + + /* + * Update the state. + */ + pAppBuf->fUsingNormal = true; + + pBuf->cbActive = pBuf->cb; + pBuf->offActive = pBuf->off; + pBuf->cbPrologue = cbPrologue; + pBuf->cbEpilogue = cbEpilogue; + pBuf->uSeg = UINT32_MAX; + pBuf->cbActiveSegLimit = UINT64_MAX; + pBuf->uSegBase = 0; + pBuf->uEffBufAddr = (uintptr_t)pAppBuf->pbNormal; + + return true; +} + + +/** + * @interface_method_impl{CIDETCORE,pfnSetupCodeBuf} + */ +static DECLCALLBACK(bool) CidetAppCbSetupCodeBuf(PCIDETCORE pThis, PCIDETBUF pBuf, void const *pvInstr) +{ + PCIDETAPP pThisApp = (PCIDETAPP)pThis; + PCIDETAPPBUF pAppBuf =&pThisApp->aCodeBuffers[pBuf->idxCfg]; + Assert(CIDETBUF_IS_CODE(pBuf->pCfg->fFlags)); + Assert(pAppBuf->fUsingNormal); + Assert(!pAppBuf->fArmed); + + /* + * Emit prologue code. + */ + uint8_t *pbDst = pAppBuf->pbNormal + pBuf->offActive - pBuf->cbPrologue; + + /* + * Copy over the code. + */ + Assert(pbDst == &pAppBuf->pbNormal[pBuf->offActive]); + memcpy(pbDst, pvInstr, pBuf->cbActive); + pbDst += pBuf->cbActive; + + /* + * Emit epilogue code. + */ + if (!pThis->InCtx.fTrickyStack) + { + /* + * The stack is reasonably good, do minimal work. + * + * Note! Ideally, we would just fill in 16 int3s here and check that + * we hit the first right one. However, if we wish to run this + * code with IEM, we better skip unnecessary trips to ring-0. + */ + uint8_t * const pbStartEpilogue = pbDst; + + /* jmp $+6 */ + *pbDst++ = 0xeb; + *pbDst++ = 0x06; /* This is a push es, so if the decoder is one off, we'll hit the int 3 below. */ + + /* Six int3s for trapping incorrectly decoded instructions. */ + *pbDst++ = 0xcc; + *pbDst++ = 0xcc; + *pbDst++ = 0xcc; + *pbDst++ = 0xcc; + *pbDst++ = 0xcc; + *pbDst++ = 0xcc; + + /* push rip / call $+0 */ + *pbDst++ = 0xe8; + *pbDst++ = 0x00; + *pbDst++ = 0x00; + *pbDst++ = 0x00; + *pbDst++ = 0x00; + uint8_t offRipAdjust = (uint8_t)(uintptr_t)(pbStartEpilogue - pbDst); + + /* push xCX */ + *pbDst++ = 0x51; + + /* mov xCX, [xSP + xCB] */ + *pbDst++ = 0x48; + *pbDst++ = 0x8b; + *pbDst++ = 0x4c; + *pbDst++ = 0x24; + *pbDst++ = sizeof(uintptr_t); + + /* lea xCX, [xCX - 24] */ + *pbDst++ = 0x48; + *pbDst++ = 0x8d; + *pbDst++ = 0x49; + *pbDst++ = offRipAdjust; + + /* mov xCX, [xSP + xCB] */ + *pbDst++ = 0x48; + *pbDst++ = 0x89; + *pbDst++ = 0x4c; + *pbDst++ = 0x24; + *pbDst++ = sizeof(uintptr_t); + + /* mov xCX, &pThis->ActualCtx */ +#ifdef RT_ARCH_AMD64 + *pbDst++ = 0x48; +#endif + *pbDst++ = 0xb9; + *(uintptr_t *)pbDst = (uintptr_t)&pThisApp->Core.ActualCtx; + pbDst += sizeof(uintptr_t); + + /* pop [ss:rcx + ActualCtx.aGRegs[X86_GREG_xCX]] */ + *pbDst++ = 0x36; + *pbDst++ = 0x8f; + *pbDst++ = 0x41; + *pbDst++ = RT_UOFFSETOF(CIDETCPUCTX, aGRegs[X86_GREG_xCX]); + Assert(RT_UOFFSETOF(CIDETCPUCTX, aGRegs[X86_GREG_xCX]) < 0x7f); + + /* mov [ss:rcx + ActualCtx.aGRegs[X86_GREG_xDX]], rdx */ + *pbDst++ = 0x36; +#ifdef RT_ARCH_AMD64 + *pbDst++ = 0x48; +#endif + *pbDst++ = 0x89; + *pbDst++ = 0x51; + *pbDst++ = RT_UOFFSETOF(CIDETCPUCTX, aGRegs[X86_GREG_xDX]); + Assert(RT_UOFFSETOF(CIDETCPUCTX, aGRegs[X86_GREG_xDX]) < 0x7f); + + /* mov [ss:rcx + ActualCtx.aSRegs[X86_GREG_DS]], ds */ + *pbDst++ = 0x36; + *pbDst++ = 0x8c; + *pbDst++ = 0x99; + *(uint32_t *)pbDst = RT_UOFFSETOF(CIDETCPUCTX, aSRegs[X86_SREG_DS]); + pbDst += sizeof(uint32_t); + + /* mov edx, 0XXYYh */ + *pbDst++ = 0xba; + *(uint32_t *)pbDst = pThisApp->Core.InTemplateCtx.aSRegs[X86_SREG_DS]; + pbDst += sizeof(uint32_t); + + /* mov ds, dx */ + *pbDst++ = 0x8e; + *pbDst++ = 0xda; + + /* mov xDX, &pThisApp->ExecuteCtx */ +#ifdef RT_ARCH_AMD64 + *pbDst++ = 0x48; +#endif + *pbDst++ = 0xba; + *(uintptr_t *)pbDst = (uintptr_t)&pThisApp->ExecuteCtx; + pbDst += sizeof(uintptr_t); + +#ifdef RT_ARCH_AMD64 + /* jmp [cs:$ wrt rip] */ + *pbDst++ = 0xff; + *pbDst++ = 0x25; + *(uint32_t *)pbDst = 0; + pbDst += sizeof(uint32_t); +#else + /* jmp NAME(CidetAppSaveAndRestoreCtx) */ + *pbDst++ = 0xb9; +#endif + *(uintptr_t *)pbDst = (uintptr_t)CidetAppSaveAndRestoreCtx; + pbDst += sizeof(uintptr_t); + + /* int3 */ + *pbDst++ = 0xcc; + + pThisApp->fUsingLockedInt3 = false; + + } + else + { + /* + * Tricky stack, so just make it raise #UD after a successful run. + */ + *pbDst++ = 0xf0; /* lock prefix */ + memset(pbDst, 0xcc, 15); /* int3 */ + pbDst += 15; + + pThisApp->fUsingLockedInt3 = true; + } + + AssertMsg(pbDst == &pAppBuf->pbNormal[pBuf->offActive + pBuf->cb + pBuf->cbEpilogue], + ("cbEpilogue=%#x, actual %#x\n", pBuf->cbEpilogue, pbDst - &pAppBuf->pbNormal[pBuf->offActive + pBuf->cb])); + + /* + * Arm the buffer. + */ + return CidetAppArmBuf(pThisApp, pAppBuf); +} + + +/** + * @interface_method_impl{CIDETCORE,pfnExecute} + */ +static DECLCALLBACK(bool) CidetAppCbExecute(PCIDETCORE pThis) +{ +#if defined(RT_OS_WINDOWS) || defined(RT_OS_DARWIN) + /* Skip tricky stack because windows cannot dispatch exception if RSP/ESP is bad. */ + if (pThis->InCtx.fTrickyStack) + return false; +#endif + + g_pExecutingThis = (PCIDETAPP)pThis; +#ifdef RT_OS_WINDOWS + __try + { + CidetAppExecute(&((PCIDETAPP)pThis)->ExecuteCtx, &pThis->InCtx); + } + __except (CidetAppXcptFilter(GetExceptionInformation())) + { + /* Won't end up here... */ + } + g_pExecutingThis = NULL; +#else + CidetAppExecute(&((PCIDETAPP)pThis)->ExecuteCtx, &pThis->InCtx); + if (g_pExecutingThis) + g_pExecutingThis = NULL; + else + { + RTTESTI_CHECK_RC(sigprocmask(SIG_SETMASK, &g_ProcSigMask, NULL), 0); + RTTESTI_CHECK_RC(sigaltstack(&g_AltStack, NULL), 0); + } +#endif + + return true; +} + + + + +/* + * + * + * CIDET Application. + * CIDET Application. + * CIDET Application. + * + * + */ + + +/** + * @interface_method_impl{CIDETCORE,pfnFailure} + */ +static DECLCALLBACK(void) CidetAppCbFailureV(PCIDETCORE pThis, const char *pszFormat, va_list va) +{ + RT_NOREF_PV(pThis); + RTTestIFailedV(pszFormat, va); +} + + +static int cidetAppAllocateAndConfigureBuffers(PCIDETAPP pThis) +{ + /* + * Code buffers. + */ + for (uint32_t i = 0; i < RT_ELEMENTS(pThis->aCodeBuffers); i++) + { + int rc = cidetAppAllocateAndConfigureOneBuffer(pThis, &pThis->aCodeBuffers[i], i, true /*fCode*/, + g_aCodeBufCfgs[i].fFlags); + if (RT_FAILURE(rc)) + return rc; + } + + /* + * Data buffers. + */ + for (uint32_t i = 0; i < RT_ELEMENTS(pThis->aDataBuffers); i++) + { + int rc = cidetAppAllocateAndConfigureOneBuffer(pThis, &pThis->aDataBuffers[i], i, false /*fCode*/, + g_aDataBufCfgs[i].fFlags); + if (RT_FAILURE(rc)) + return rc; + } + + /* + * Stack. + */ + pThis->cbStack = _32K; + pThis->pbStackLow = (uint8_t *)RTMemPageAlloc(pThis->cbStack); + if (!pThis->pbStackLow) + { + RTTestIFailed("Failed to allocate %u bytes for stack\n", pThis->cbStack); + return false; + } + pThis->pbStackEnd = pThis->pbStackLow + pThis->cbStack; + + return true; +} + + +static int CidetAppCreate(PPCIDETAPP ppThis) +{ + *ppThis = NULL; + + PCIDETAPP pThis = (PCIDETAPP)RTMemAlloc(sizeof(*pThis)); + if (!pThis) + return RTTestIFailedRc(VERR_NO_MEMORY, "Error allocating %zu bytes.", sizeof(*pThis)); + + /* Create a random source. */ + RTRAND hRand; + int rc = RTRandAdvCreateParkMiller(&hRand); + if (RT_SUCCESS(rc)) + { + uint64_t uSeed = ASMReadTSC(); + rc = RTRandAdvSeed(hRand, uSeed); + if (RT_SUCCESS(rc)) + RTTestIPrintf(RTTESTLVL_ALWAYS, "Random seed %#llx\n", uSeed); + + /* Initialize the CIDET structure. */ + rc = CidetCoreInit(&pThis->Core, hRand); + if (RT_SUCCESS(rc)) + { + pThis->Core.pfnReInitDataBuf = CidetAppCbReInitDataBuf; + pThis->Core.pfnSetupDataBuf = CidetAppCbSetupDataBuf; + pThis->Core.pfnIsBufEqual = CidetAppCbIsBufEqual; + pThis->Core.pfnReInitCodeBuf = CidetAppCbReInitCodeBuf; + pThis->Core.pfnSetupCodeBuf = CidetAppCbSetupCodeBuf; + pThis->Core.pfnExecute = CidetAppCbExecute; + pThis->Core.pfnFailure = CidetAppCbFailureV; + + pThis->Core.paCodeBufConfigs = g_aCodeBufCfgs; + pThis->Core.cCodeBufConfigs = CIDETAPP_CODE_BUF_COUNT; + pThis->Core.paDataBufConfigs = g_aDataBufCfgs; + pThis->Core.cDataBufConfigs = CIDETAPP_DATA_BUF_COUNT; + + rc = cidetAppAllocateAndConfigureBuffers(pThis); + if (RT_SUCCESS(rc)) + { + rc = CidetCoreSetTargetMode(&pThis->Core, ARCH_BITS == 32 ? CIDETMODE_PP_32 : CIDETMODE_LM_64); + if (RT_SUCCESS(rc)) + { + pThis->Core.InTemplateCtx.aSRegs[X86_SREG_CS] = ASMGetCS(); + pThis->Core.InTemplateCtx.aSRegs[X86_SREG_DS] = ASMGetDS(); + pThis->Core.InTemplateCtx.aSRegs[X86_SREG_ES] = ASMGetES(); + pThis->Core.InTemplateCtx.aSRegs[X86_SREG_FS] = ASMGetFS(); + pThis->Core.InTemplateCtx.aSRegs[X86_SREG_GS] = ASMGetGS(); + pThis->Core.InTemplateCtx.aSRegs[X86_SREG_SS] = ASMGetSS(); + pThis->Core.InTemplateCtx.aGRegs[X86_GREG_xSP] = (uintptr_t)pThis->pbStackEnd - 64; + + pThis->Core.fTestCfg |= CIDET_TESTCFG_SEG_PRF_CS; + pThis->Core.fTestCfg |= CIDET_TESTCFG_SEG_PRF_DS; + pThis->Core.fTestCfg |= CIDET_TESTCFG_SEG_PRF_ES; +#if !defined(RT_OS_WINDOWS) + pThis->Core.fTestCfg |= CIDET_TESTCFG_SEG_PRF_FS; +#endif +#if !defined(CIDET_LEAVE_GS_ALONE) + pThis->Core.fTestCfg |= CIDET_TESTCFG_SEG_PRF_GS; +#endif + + *ppThis = pThis; + return VINF_SUCCESS; + } + rc = RTTestIFailedRc(rc, "Error setting target mode: %Rrc", rc); + } + CidetCoreDelete(&pThis->Core); + } + else + { + rc = RTTestIFailedRc(rc, "CidetCoreInit failed: %Rrc", rc); + RTRandAdvDestroy(hRand); + } + } + else + rc = RTTestIFailedRc(rc, "RTRandAdvCreate failed: %Rrc", rc); + RTMemFree(pThis); + return rc; +} + + +static void CidetAppDestroy(PCIDETAPP pThis) +{ + CidetCoreDelete(&pThis->Core); + + for (uint32_t i = 0; i < RT_ELEMENTS(pThis->aCodeBuffers); i++) + CidetAppDeleteBuffer(&pThis->aCodeBuffers[i]); + for (uint32_t i = 0; i < RT_ELEMENTS(pThis->aDataBuffers); i++) + CidetAppDeleteBuffer(&pThis->aDataBuffers[i]); + RTMemPageFree(pThis->pbStackLow, pThis->cbStack); + + RTMemFree(pThis); +} + + +static void CidetAppTestBunch(PCIDETAPP pThis, PCCIDETINSTR paInstructions, uint32_t cInstructions, const char *pszBunchName) +{ + for (uint32_t iInstr = 0; iInstr < cInstructions; iInstr++) + { + RTTestSubF(g_hTest, "%s - %s", pszBunchName, paInstructions[iInstr].pszMnemonic); + CidetCoreTestInstruction(&pThis->Core, &paInstructions[iInstr]); + } +} + + +int main(int argc, char **argv) +{ + /* + * Initialize the runtime. + */ + RTEXITCODE rcExit = RTTestInitExAndCreate(argc, &argv, 0, "cidet-app", &g_hTest); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + + /* + * Parse arguments. + */ + static const RTGETOPTDEF s_aOptions[] = + { + { "--noop", 'n', RTGETOPT_REQ_NOTHING }, + }; + + int chOpt; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0); + while ((chOpt = RTGetOpt(&GetState, &ValueUnion))) + { + switch (chOpt) + { + case 'n': + break; + + case 'h': + RTPrintf("usage: %s\n", argv[0]); + return RTEXITCODE_SUCCESS; + + case 'V': + RTPrintf("%sr%d\n", RTBldCfgVersion(), RTBldCfgRevision()); + return RTEXITCODE_SUCCESS; + + default: + return RTGetOptPrintError(chOpt, &ValueUnion); + } + } + +#ifdef USE_SIGNALS + /* + * Set up signal handlers with alternate stack. + */ + /* Get the default signal mask. */ + RTTESTI_CHECK_RC_RET(sigprocmask(SIG_BLOCK, NULL, &g_ProcSigMask), 0, RTEXITCODE_FAILURE); + + /* Alternative stack so we can play with esp/rsp. */ + RT_ZERO(g_AltStack); + g_AltStack.ss_flags = 0; +# ifdef SIGSTKSZ + g_AltStack.ss_size = RT_MAX(SIGSTKSZ, _128K); +# else + g_AltStack.ss_size = _128K; +# endif +#ifdef RT_OS_FREEBSD + g_AltStack.ss_sp = (char *)RTMemPageAlloc(g_AltStack.ss_size); +#else + g_AltStack.ss_sp = RTMemPageAlloc(g_AltStack.ss_size); +#endif + RTTESTI_CHECK_RET(g_AltStack.ss_sp != NULL, RTEXITCODE_FAILURE); + RTTESTI_CHECK_RC_RET(sigaltstack(&g_AltStack, NULL), 0, RTEXITCODE_FAILURE); + + /* Default signal action config. */ + struct sigaction Act; + RT_ZERO(Act); + Act.sa_sigaction = CidetAppSigHandler; + Act.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigfillset(&Act.sa_mask); + + /* Hook the signals we might need. */ + sigaction(SIGILL, &Act, NULL); + sigaction(SIGTRAP, &Act, NULL); +# ifdef SIGEMT + sigaction(SIGEMT, &Act, NULL); +# endif + sigaction(SIGFPE, &Act, NULL); + sigaction(SIGBUS, &Act, NULL); + sigaction(SIGSEGV, &Act, NULL); + +#elif defined(RT_OS_WINDOWS) + /* + * Register vectored exception handler and override the default unhandled + * exception filter, just to be on the safe side... + */ + RTTESTI_CHECK(AddVectoredExceptionHandler(1 /* first */, CidetAppVectoredXcptHandler) != NULL); + SetUnhandledExceptionFilter(CidetAppUnhandledXcptFilter); +#endif + + /* + * Do the work. + */ + RTTestBanner(g_hTest); + + PCIDETAPP pThis; + int rc = CidetAppCreate(&pThis); + if (RT_SUCCESS(rc)) + { + CidetAppTestBunch(pThis, g_aCidetInstructions1, g_cCidetInstructions1, "First Bunch"); + + CidetAppDestroy(pThis); + } + + return RTTestSummaryAndDestroy(g_hTest); +} + diff --git a/src/VBox/ValidationKit/utils/cpu/cidet-appA.asm b/src/VBox/ValidationKit/utils/cpu/cidet-appA.asm new file mode 100644 index 00000000..55feb013 --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cidet-appA.asm @@ -0,0 +1,319 @@ +; $Id: cidet-appA.asm $ +;; @file +; CPU Instruction Decoding & Execution Tests - Ring-3 Driver Application, Assembly Code. +; + +; +; Copyright (C) 2009-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 <https://www.gnu.org/licenses>. +; +; The contents of this file may alternatively be used under the terms +; of the Common Development and Distribution License Version 1.0 +; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +; in the VirtualBox distribution, in which case the provisions of the +; CDDL are applicable instead of those of the GPL. +; +; You may elect to license modified versions of this file under the +; terms and conditions of either the GPL or the CDDL or both. +; +; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +; + + +;******************************************************************************* +;* Header Files * +;******************************************************************************* +%include "iprt/asmdefs.mac" +%include "iprt/x86.mac" +%include "cidet.mac" + + +;******************************************************************************* +;* Global Variables * +;******************************************************************************* +%ifdef RT_ARCH_X86 +;; Used by CidetAppSaveAndRestoreCtx when we have a tricky target stack. +g_uTargetEip dd 0 +g_uTargetCs dw 0 +%endif + + +;; +; Leave GS alone on 64-bit darwin (gs is 0, no ldt or gdt entry to load that'll +; restore the lower 32-bits of the base when saving and restoring the register). +%ifdef RT_OS_DARWIN + %ifdef RT_ARCH_AMD64 + %define CIDET_LEAVE_GS_ALONE + %endif +%endif + + + +BEGINCODE + +;; +; ASSUMES that it's called and the EIP/RIP is found on the stack. +; +; @param pSaveCtx ds:xCX The context to save; DS, xDX and xCX have +; already been saved by the caller. +; @param pRestoreCtx ds:xDX The context to restore. +; +BEGINPROC CidetAppSaveAndRestoreCtx + ; + ; Save the stack pointer and program counter first so we can later + ; bypass this step if we need to. + ; + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xAX * 8], xAX ; need scratch register. + lea xAX, [xSP + xCB] + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xSP * 8], xAX + mov word [xCX + CIDETCPUCTX.aSRegs + X86_SREG_SS * 2], ss + mov word [xCX + CIDETCPUCTX.aSRegs + X86_SREG_CS * 2], cs + mov xAX, [xSP] + mov [xCX + CIDETCPUCTX.rip], xAX + jmp CidetAppSaveAndRestoreCtx_1 + +GLOBALNAME CidetAppSaveAndRestoreCtx_NoSsSpCsIp + mov [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xAX * 8], xAX +CidetAppSaveAndRestoreCtx_1: + + ; Flags. +%ifdef RT_ARCH_AMD64 + pushfq +%else + pushfd +%endif + pop xAX + mov [xCX + CIDETCPUCTX.rfl], xAX + + ; Segment registers. + mov word [xCX + CIDETCPUCTX.aSRegs + X86_SREG_ES * 2], es + mov word [xCX + CIDETCPUCTX.aSRegs + X86_SREG_FS * 2], fs + mov word [xCX + CIDETCPUCTX.aSRegs + X86_SREG_GS * 2], gs + + ; Remaining GPRs. + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xBX * 8], xBX + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xBP * 8], xBP + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xSI * 8], xSI + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xDI * 8], xDI +%ifdef RT_ARCH_AMD64 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x8 * 8], r8 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x9 * 8], r9 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x10 * 8], r10 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x11 * 8], r11 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x12 * 8], r12 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x13 * 8], r13 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x14 * 8], r14 + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x15 * 8], r15 + xor eax, eax + mov [xCX + CIDETCPUCTX.cr2], rax + %ifndef CIDET_REDUCED_CTX + mov [xCX + CIDETCPUCTX.cr0], rax + mov [xCX + CIDETCPUCTX.cr3], rax + mov [xCX + CIDETCPUCTX.cr4], rax + mov [xCX + CIDETCPUCTX.cr8], rax + mov [xCX + CIDETCPUCTX.dr0], rax + mov [xCX + CIDETCPUCTX.dr1], rax + mov [xCX + CIDETCPUCTX.dr2], rax + mov [xCX + CIDETCPUCTX.dr3], rax + mov [xCX + CIDETCPUCTX.dr6], rax + mov [xCX + CIDETCPUCTX.dr7], rax + mov [xCX + CIDETCPUCTX.tr], ax + mov [xCX + CIDETCPUCTX.ldtr], ax + %endif +%else + xor eax, eax + mov [xCX + CIDETCPUCTX.rfl + 4], eax + mov [xCX + CIDETCPUCTX.rip + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xAX * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xCX * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xDX * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xBX * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xSP * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xBP * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xSI * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xDI * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x8 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x8 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x9 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x9 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x10 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x10 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x11 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x11 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x12 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x12 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x13 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x13 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x14 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x14 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x15 * 8 ], eax + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_x15 * 8 + 4], eax + mov [xCX + CIDETCPUCTX.cr2 ], eax + mov [xCX + CIDETCPUCTX.cr2 + 4], eax + %ifndef CIDET_REDUCED_CTX + mov [xCX + CIDETCPUCTX.cr0 ], eax + mov [xCX + CIDETCPUCTX.cr0 + 4], eax + mov [xCX + CIDETCPUCTX.cr3 ], eax + mov [xCX + CIDETCPUCTX.cr3 + 4], eax + mov [xCX + CIDETCPUCTX.cr4 ], eax + mov [xCX + CIDETCPUCTX.cr4 + 4], eax + mov [xCX + CIDETCPUCTX.cr8 ], eax + mov [xCX + CIDETCPUCTX.cr8 + 4], eax + mov [xCX + CIDETCPUCTX.dr0 ], eax + mov [xCX + CIDETCPUCTX.dr0 + 4], eax + mov [xCX + CIDETCPUCTX.dr1 ], eax + mov [xCX + CIDETCPUCTX.dr1 + 4], eax + mov [xCX + CIDETCPUCTX.dr2 ], eax + mov [xCX + CIDETCPUCTX.dr2 + 4], eax + mov [xCX + CIDETCPUCTX.dr3 ], eax + mov [xCX + CIDETCPUCTX.dr3 + 4], eax + mov [xCX + CIDETCPUCTX.dr6 ], eax + mov [xCX + CIDETCPUCTX.dr6 + 4], eax + mov [xCX + CIDETCPUCTX.dr7 ], eax + mov [xCX + CIDETCPUCTX.dr7 + 4], eax + mov [xCX + CIDETCPUCTX.tr], ax + mov [xCX + CIDETCPUCTX.ldtr], ax + %endif +%endif + dec xAX + mov [xCX + CIDETCPUCTX.uErr], xAX +%ifdef RT_ARCH_X86 + mov [xCX + CIDETCPUCTX.uErr + 4], eax +%endif + mov [xCX + CIDETCPUCTX.uXcpt], eax + + ; + ; Restore the other state (pointer in xDX). + ; +NAME(CidetAppSaveAndRestoreCtx_Restore): + + ; Restore ES, FS, and GS. + mov es, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_ES * 2] + mov fs, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_FS * 2] +%ifndef CIDET_LEAVE_GS_ALONE + mov gs, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_GS * 2] +%endif + + ; Restore most GPRs (except xCX, xAX and xSP). + mov xCX, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xCX * 8] + mov xBX, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xBX * 8] + mov xBP, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xBP * 8] + mov xSI, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xSI * 8] + mov xDI, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xDI * 8] +%ifdef RT_ARCH_AMD64 + mov r8, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x8 * 8] + mov r9, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x9 * 8] + mov r10, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x10 * 8] + mov r11, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x11 * 8] + mov r12, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x12 * 8] + mov r13, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x13 * 8] + mov r14, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x14 * 8] + mov r15, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_x15 * 8] +%endif + +%ifdef RT_ARCH_AMD64 + ; Create an iret frame which restores SS:RSP, RFLAGS, and CS:RIP. + movzx eax, word [xDX + CIDETCPUCTX.aSRegs + X86_SREG_SS * 2] + push xAX + push qword [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xSP * 8] + push qword [xDX + CIDETCPUCTX.rfl] + movzx eax, word [xDX + CIDETCPUCTX.aSRegs + X86_SREG_CS * 2] + push xAX + push qword [xDX + CIDETCPUCTX.rip] + + ; Restore DS, xAX and xDX then do the iret. + mov ds, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_DS * 2] + mov xAX, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xAX * 8] + mov xDX, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xDX * 8] + iretq +%else + ; In 32-bit mode iret doesn't restore CS:ESP for us, so we have to + ; make a choice whether the SS:ESP is more important than EFLAGS. + cmp byte [xDX + CIDETCPUCTX.fTrickyStack], 0 + jne .tricky_stack + + mov ss, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_SS * 2] + mov xSP, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xSP * 8] + + push dword [xDX + CIDETCPUCTX.rfl] ; iret frame + movzx eax, word [xDX + CIDETCPUCTX.aSRegs + X86_SREG_CS * 2] ; iret frame + push xAX ; iret frame + push dword [xDX + CIDETCPUCTX.rip] ; iret frame + + mov xAX, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xAX * 8] + mov ds, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_DS * 2] + mov xDX, [cs:xDX + CIDETCPUCTX.aGRegs + X86_GREG_xDX * 8] + iretd + +.tricky_stack: + mov xAX, [xDX + CIDETCPUCTX.rip] + mov [g_uTargetEip], xAX + mov ax, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_CS * 2] + mov [g_uTargetCs], ax + push dword [xDX + CIDETCPUCTX.rfl] + popfd + mov ss, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_SS * 2] + mov xSP, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xSP * 8] + mov xAX, [xDX + CIDETCPUCTX.aGRegs + X86_GREG_xAX * 8] + mov ds, [xDX + CIDETCPUCTX.aSRegs + X86_SREG_DS * 2] + mov xDX, [cs:xDX + CIDETCPUCTX.aGRegs + X86_GREG_xDX * 8] + jmp far [cs:g_uTargetEip] +%endif +ENDPROC CidetAppSaveAndRestoreCtx + + +;; +; C callable version of CidetAppSaveAndRestoreCtx more or less. +; +; @param pSaveCtx x86:esp+4 gcc:rdi msc:rcx +; @param pRestoreCtx x86:esp+8 gcc:rsi msc:rdx +BEGINPROC CidetAppExecute +%ifdef RT_ARCH_X86 + mov ecx, [esp + 4] + mov edx, [esp + 8] +%elifdef ASM_CALL64_GCC + mov rcx, rdi + mov rdx, rsi +%elifndef ASM_CALL64_MSC + %error "unsupport arch." +%endif + mov word [xCX + CIDETCPUCTX.aSRegs + X86_SREG_DS * 2], ds + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xDX * 8], xDX + mov [xCX + CIDETCPUCTX.aGRegs + X86_GREG_xCX * 8], xCX + jmp NAME(CidetAppSaveAndRestoreCtx) +ENDPROC CidetAppExecute + + +;; +; C callable restore function. +; +; @param pRestoreCtx x86:esp+4 gcc:rdi msc:rcx +BEGINPROC CidetAppRestoreCtx +%ifdef RT_ARCH_X86 + mov edx, [esp + 4] +%elifdef ASM_CALL64_GCC + mov rdx, rdi +%elifdef ASM_CALL64_MSC + mov rdx, rcx +%else + %error "unsupport arch." +%endif + mov ds, [cs:xDX + CIDETCPUCTX.aSRegs + X86_SREG_DS * 2] + jmp NAME(CidetAppSaveAndRestoreCtx_Restore) +ENDPROC CidetAppRestoreCtx + diff --git a/src/VBox/ValidationKit/utils/cpu/cidet-core.cpp b/src/VBox/ValidationKit/utils/cpu/cidet-core.cpp new file mode 100644 index 00000000..ff350861 --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cidet-core.cpp @@ -0,0 +1,2368 @@ +/* $Id: cidet-core.cpp $ */ +/** @file + * CPU Instruction Decoding & Execution Tests - Simple Instructions. + */ + +/* + * Copyright (C) 2014-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +#define CIDET_INSTR_TEST_OP_FLAG(a_pInstr, a_fFlag) \ + ( ((a_pInstr)->afOperands[0] & (a_fFlag)) \ + || ((a_pInstr)->afOperands[1] & (a_fFlag)) \ + || ( (a_pInstr)->cOperands > 2 \ + && ( ((a_pInstr)->afOperands[2] & (a_fFlag)) \ + || ((a_pInstr)->afOperands[3] & (a_fFlag)) ) ) ) + +#define CIDET_INSTR_TEST_OP_MASK_VALUE(a_pInstr, a_fMask, a_fValue) \ + ( ((a_pInstr)->afOperands[0] & (a_fMask)) == (a_fValue) \ + || ((a_pInstr)->afOperands[1] & (a_fMask)) == (a_fValue) \ + || ( (a_pInstr)->cOperands > 2 \ + && ( ((a_pInstr)->afOperands[2] & (a_fMask)) == (a_fValue) \ + || ((a_pInstr)->afOperands[3] & (a_fMask)) == (a_fValue) ) ) ) + +/** @def CIDET_DPRINTF + * Debug printf. */ +#if 1 //def DEBUG_bird +# define CIDET_DPRINTF(a) do { RTPrintf a; } while (0) +# define CIDET_DPRINTF_ENABLED +#else +# define CIDET_DPRINTF(a) do { } while (0) +#endif + +/** @def CIDET_DEBUG_DISAS + * Enables instruction disassembly. */ +#if defined(DOXYGEN_RUNNING) +# define CIDET_DEBUG_DISAS 1 +#endif + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include "cidet.h" + +#include <iprt/assert.h> +#include <iprt/rand.h> +#include <iprt/param.h> +#include <iprt/string.h> +#include <iprt/errcore.h> +#if defined(CIDET_DPRINTF_ENABLED) || defined(CIDET_DEBUG_DISAS) +# include <VBox/dis.h> +# include <iprt/stream.h> +#endif + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** For translating CIDET_OF_Z_XXX values (after shifting). */ +uint16_t const g_acbCidetOfSizes[] = +{ + /* [CIDET_OF_Z_NONE] = */ 0, + /* [CIDET_OF_Z_BYTE] = */ 1, + /* [CIDET_OF_Z_WORD] = */ 2, + /* [CIDET_OF_Z_DWORD] = */ 4, + /* [CIDET_OF_Z_QWORD] = */ 8, + /* [CIDET_OF_Z_TBYTE] = */ 10, + /* [CIDET_OF_Z_OWORD] = */ 16, + /* [CIDET_OF_Z_YWORD] = */ 32, + /* [CIDET_OF_Z_ZWORD] = */ 64, + /* [CIDET_OF_Z_VAR_WDQ] = */ UINT16_MAX, + /* [0xa] = */ 0, + /* [0xb] = */ 0, + /* [0xc] = */ 0, + /* [0xd] = */ 0, + /* [0xe] = */ 0, + /* [CIDET_OF_Z_SPECIAL] = */ UINT16_MAX - 1, +}; + + +/** Converts operand sizes in bytes to 64-bit masks. */ +static const uint64_t g_au64ByteSizeToMask[] = +{ + UINT64_C(0x0000000000000000), + UINT64_C(0x00000000000000ff), + UINT64_C(0x000000000000ffff), + UINT64_C(0x0000000000ffffff), + UINT64_C(0x00000000ffffffff), + UINT64_C(0x000000ffffffffff), + UINT64_C(0x0000ffffffffffff), + UINT64_C(0x00ffffffffffffff), + UINT64_C(0xffffffffffffffff), +}; + +/** Converts operand sizes in bytes to 64-bit signed max values. */ +static const int64_t g_ai64ByteSizeToMax[] = +{ + INT64_C(0x0000000000000000), + INT64_C(0x000000000000007f), + INT64_C(0x0000000000007fff), + INT64_C(0x00000000007fffff), + INT64_C(0x000000007fffffff), + INT64_C(0x0000007fffffffff), + INT64_C(0x00007fffffffffff), + INT64_C(0x007fffffffffffff), + INT64_C(0x7fffffffffffffff), +}; + + +bool CidetInstrHasMrmMemOperand(PCCIDETINSTR pInstr) +{ + return CIDET_INSTR_TEST_OP_FLAG(pInstr, CIDET_OF_M_RM_ONLY_M); +} + + +bool CidetInstrHasMrmRegOperand(PCCIDETINSTR pInstr) +{ + return CIDET_INSTR_TEST_OP_FLAG(pInstr, CIDET_OF_M_RM_ONLY_R); +} + + +bool CidetInstrRespondsToOperandSizePrefixes(PCCIDETINSTR pInstr) +{ + return CIDET_INSTR_TEST_OP_MASK_VALUE(pInstr, CIDET_OF_Z_MASK, CIDET_OF_Z_VAR_WDQ); +} + + + + +int CidetCoreInit(PCIDETCORE pThis, RTRAND hRand) +{ + AssertPtr(pThis); + AssertPtr(hRand); + + RT_ZERO(*pThis); + pThis->u32Magic = CIDETCORE_MAGIC; + pThis->hRand = hRand; + return VINF_SUCCESS; +} + + +void CidetCoreDelete(PCIDETCORE pThis) +{ + AssertPtr(pThis); Assert(pThis->u32Magic == CIDETCORE_MAGIC); + + RTRandAdvDestroy(pThis->hRand); + RT_ZERO(*pThis); +} + + +/** + * Report a test failure via CIDET::pfnFailure + * + * @returns false + * @param pThis Pointer to the core structure. + * @param pszFormat Format string containing failure details. + * @param va Arguments referenced in @a pszFormat. + */ +int CidetCoreSetErrorV(PCIDETCORE pThis, const char *pszFormat, va_list va) +{ + pThis->pfnFailure(pThis, pszFormat, va); + return false; +} + + +/** + * Report a test failure via CIDET::pfnFailure + * + * @returns false + * @param pThis Pointer to the core structure. + * @param pszFormat Format string containing failure details. + * @param ... Arguments referenced in @a pszFormat. + */ +bool CidetCoreSetError(PCIDETCORE pThis, const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + CidetCoreSetErrorV(pThis, pszFormat, va); + va_end(va); + return false; +} + + +/** + * Get a signed random number, with a given number of significant bytes. + * + * @returns Random number. + * @param pThis Pointer to the core structure. + * @param cbSignificant The number of significant bytes. + */ +int64_t CidetCoreGetRandS64(PCIDETCORE pThis, uint8_t cbSignificant) +{ + int64_t iVal = RTRandAdvS64(pThis->hRand); + switch (cbSignificant) + { + case 1: return (int8_t)iVal; + case 2: return (int16_t)iVal; + case 4: return (int32_t)iVal; + case 8: return iVal; + default: + AssertReleaseFailed(); + return iVal; + } +} + + +/** + * Get an unsigned random number, with a given number of significant bytes. + * + * @returns Random number. + * @param pThis Pointer to the core structure. + * @param cbSignificant The number of significant bytes. + */ +uint64_t CidetCoreGetRandU64(PCIDETCORE pThis, uint8_t cbSignificant) +{ + Assert(cbSignificant == 1 || cbSignificant == 2 || cbSignificant == 4 || cbSignificant == 8); + + uint64_t uVal = RTRandAdvU64(pThis->hRand); + uVal &= g_au64ByteSizeToMask[cbSignificant]; + + return uVal; +} + + + +void CidetCoreInitializeCtxTemplate(PCIDETCORE pThis) +{ + pThis->InTemplateCtx.rip = UINT64_MAX; + pThis->InTemplateCtx.rfl = X86_EFL_1 | X86_EFL_ID | X86_EFL_IF; + + unsigned i = RT_ELEMENTS(pThis->InTemplateCtx.aGRegs); + if (CIDETMODE_IS_LM(pThis->bMode)) + while (i-- > 0) + pThis->InTemplateCtx.aGRegs[i] = UINT64_C(0x3fefcc00daba005d) + | ((uint64_t)i << 32) + | ((uint32_t)i << 8); + else + while (i-- > 0) + pThis->InTemplateCtx.aGRegs[i] = UINT64_C(0xfada009b) + | ((uint32_t)i << 12) + | ((uint32_t)i << 8); + i = RT_ELEMENTS(pThis->InTemplateCtx.aSRegs); + while (i-- > 0) + pThis->InTemplateCtx.aSRegs[i] = 0; /* Front end sets these afterwards. */ + pThis->InTemplateCtx.cr2 = 0; +#ifndef CIDET_REDUCED_CTX + pThis->InTemplateCtx.tr = 0; + pThis->InTemplateCtx.ldtr = 0; + pThis->InTemplateCtx.cr0 = 0; + pThis->InTemplateCtx.cr3 = 0; + pThis->InTemplateCtx.cr4 = 0; + pThis->InTemplateCtx.cr8 = 0; +#endif + pThis->InTemplateCtx.fIgnoredRFlags = 0; + pThis->InTemplateCtx.uXcpt = UINT32_MAX; + pThis->InTemplateCtx.uErr = UINT64_MAX; + pThis->InTemplateCtx.fTrickyStack = false; +} + + +/** + * Sets the target mode. + * + * Caller must set up default selector values after calling this function. + * + * @returns VBox status code. + * @param pThis Pointer to the core structure. + * @param bMode The new mode. + */ +int CidetCoreSetTargetMode(PCIDETCORE pThis, uint8_t bMode) +{ + AssertPtrReturn(pThis, VERR_INVALID_HANDLE); AssertReturn(pThis->u32Magic == CIDETCORE_MAGIC, VERR_INVALID_HANDLE); + switch (bMode) + { + //case CIDETMODE_RM: + //case CIDETMODE_PE_16: + //case CIDETMODE_PE_32: + //case CIDETMODE_PE_V86: + //case CIDETMODE_PP_16: + case CIDETMODE_PP_32: + //case CIDETMODE_PP_V86: + //case CIDETMODE_PAE_16: + case CIDETMODE_PAE_32: + //case CIDETMODE_PAE_V86: + //case CIDETMODE_LM_S16: + //case CIDETMODE_LM_32: + case CIDETMODE_LM_64: + break; + default: + return VERR_NOT_IMPLEMENTED; + } + pThis->bMode = bMode; + CidetCoreInitializeCtxTemplate(pThis); + return VINF_SUCCESS; +} + + +bool CidetCoreIsEncodingCompatibleWithInstruction(PCIDETCORE pThis) +{ + RT_NOREF_PV(pThis); + return true; +} + + +/** + * Selects the next address size mode. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + */ +static bool cidetCoreSetupNextBaseEncoding_AddressSize(PCIDETCORE pThis) +{ + if (pThis->fAddrSizePrf) + { + /* + * Reset to default. + */ + pThis->cbAddrMode = CIDETMODE_GET_BYTE_COUNT(pThis->bMode); + pThis->fAddrSizePrf = false; + } + else + { + /* + * The other addressing size. + */ + if (CIDETMODE_IS_64BIT(pThis->bMode)) + pThis->cbAddrMode = 4; + else if (CIDETMODE_IS_32BIT(pThis->bMode)) + pThis->cbAddrMode = 2; + else + { + AssertRelease(CIDETMODE_IS_16BIT(pThis->bMode)); + pThis->cbAddrMode = 2; + } + pThis->fAddrSizePrf = true; + } + return pThis->fAddrSizePrf; +} + + +/** + * Selects the first REG encoding. + * + * @param pThis The core state structure. + */ +static void cidetCoreSetupFirstBaseEncoding_MrmReg(PCIDETCORE pThis) +{ + pThis->aOperands[pThis->idxMrmRegOp].iReg = 0; + pThis->aOperands[pThis->idxMrmRegOp].fIsMem = false; + pThis->aOperands[pThis->idxMrmRegOp].fIsRipRelative = false; + pThis->aOperands[pThis->idxMrmRegOp].fIsHighByteRegister = false; + pThis->aOperands[pThis->idxMrmRegOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRegOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRegOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRegOp].uMemScale = 1; + pThis->aOperands[pThis->idxMrmRegOp].iEffSeg = UINT8_MAX; + pThis->bModRm &= ~X86_MODRM_REG_MASK; + pThis->fRexR = false; +} + + +/** + * Selects the next REG (ModR/M) encoding. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + */ +static bool cidetCoreSetupNextBaseEncoding_MrmReg(PCIDETCORE pThis, uint8_t iReg) +{ + Assert(pThis->idxMrmRegOp < RT_ELEMENTS(pThis->aOperands) && !pThis->aOperands[pThis->idxMrmRegOp].fIsMem); + Assert(iReg < 16); + + /* + * Clear the collision flags here because of the byte register kludge. + */ + pThis->fHasRegCollisionDirect = false; + pThis->fHasRegCollisionMemBase = false; + pThis->fHasRegCollisionMemIndex = false; + pThis->fHasRegCollisionMem = false; + + /* + * Clear the REX prefix and high byte register tracking too. ASSUMES MrmReg is after MrmRmMod. + */ + Assert(!pThis->fNoRexPrefixMrmRm); + Assert(!pThis->fHasHighByteRegInMrmRm); + pThis->fNoRexPrefixMrmReg = false; + pThis->fNoRexPrefix = false; + pThis->fHasHighByteRegInMrmReg = false; + pThis->aOperands[pThis->idxMrmRegOp].fIsHighByteRegister = false; + + /* + * Special kludge for ah, ch, dh, bh, spl, bpl, sil, and dil. + * Needs extra care in 64-bit mode and special collision detection code. + */ + CIDET_DPRINTF(("aOperands[%u].cb=%u fGpr=%u iReg=%d fRex=%d fRexW=%u fRexX=%u fRexB=%u fRexR=%d\n", + pThis->idxMrmRegOp, pThis->aOperands[pThis->idxMrmRegOp].cb, CIDET_OF_K_IS_GPR(pThis->fMrmRegOp), iReg, + pThis->fRex, pThis->fRexW, pThis->fRexX, pThis->fRexB, pThis->fRexR)); + if ( pThis->aOperands[pThis->idxMrmRegOp].cb == 1 + && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp) + && iReg >= 3 + && ( iReg <= 6 + || (CIDETMODE_IS_64BIT(pThis->bMode) && iReg == 7 && !pThis->fRex)) ) + + { + if (!pThis->fRex && iReg >= 4 && CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fNoRexPrefix) + { + /* The AMD64 low variants: spl, bpl, sil and dil. */ + pThis->fRex = true; + pThis->fHasStackRegInMrmReg = iReg == X86_GREG_xSP; + + /* Check for collisions. */ + if (pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands)) + { + Assert(!pThis->fHasHighByteRegInMrmRm); + if (!pThis->aOperands[pThis->idxMrmRmOp].fIsMem) + pThis->fHasRegCollisionDirect = CIDET_OF_K_IS_GPR(pThis->fMrmRmOp) + && iReg == pThis->aOperands[pThis->idxMrmRmOp].iReg; + else + { + Assert(!pThis->fUsesVexIndexRegs || pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg == UINT8_MAX); + + pThis->fHasRegCollisionMemBase = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg; + pThis->fHasRegCollisionMemIndex = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg; + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + } + } + } + else + { + /* Next register: ah, ch, dh and bh. */ + iReg++; + pThis->aOperands[pThis->idxMrmRegOp].iReg = iReg; + pThis->bModRm &= ~X86_MODRM_REG_MASK; + pThis->bModRm |= (iReg & X86_MODRM_REG_SMASK) << X86_MODRM_REG_SHIFT; + pThis->fRex = false; + pThis->fRexR = false; + pThis->fNoRexPrefixMrmReg = true; + pThis->fNoRexPrefix = true; + pThis->fHasHighByteRegInMrmReg = true; + pThis->fHasStackRegInMrmReg = false; + pThis->aOperands[pThis->idxMrmRegOp].fIsHighByteRegister = true; + Assert(!pThis->fRexW); Assert(!pThis->fRexX); Assert(!pThis->fRexB); + + /* Check for collisions. */ + if (pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands)) + { + if (!pThis->aOperands[pThis->idxMrmRmOp].fIsMem) + pThis->fHasRegCollisionDirect = CIDET_OF_K_IS_GPR(pThis->fMrmRmOp) + && ( ( pThis->aOperands[pThis->idxMrmRmOp].cb == 1 + && iReg == pThis->aOperands[pThis->idxMrmRmOp].iReg + && pThis->fHasHighByteRegInMrmRm) + || ( pThis->aOperands[pThis->idxMrmRmOp].cb > 1 + && iReg - 4 == pThis->aOperands[pThis->idxMrmRmOp].iReg)); + else + { + Assert(!pThis->fUsesVexIndexRegs || pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg == UINT8_MAX); + + pThis->fHasRegCollisionMemBase = iReg - 4 == pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg; + pThis->fHasRegCollisionMemIndex = iReg - 4 == pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg; + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + } + } + } + return true; + } + + Assert(!pThis->fRex || (iReg == 7 && CIDETMODE_IS_64BIT(pThis->bMode))); + pThis->fRex = false; + + /* + * Next register. + */ + iReg = (iReg + 1) & (CIDETMODE_IS_64BIT(pThis->bMode) ? 15 : 7); + + pThis->aOperands[pThis->idxMrmRegOp].iReg = iReg; + pThis->bModRm &= ~X86_MODRM_REG_MASK; + pThis->bModRm |= (iReg & X86_MODRM_REG_SMASK) << X86_MODRM_REG_SHIFT; + pThis->fRexR = iReg >= 8; + pThis->fHasStackRegInMrmReg = iReg == X86_GREG_xSP && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + + /* + * Register collision detection. + */ + if (pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands)) + { + if (!pThis->aOperands[pThis->idxMrmRmOp].fIsMem) + pThis->fHasRegCollisionDirect = iReg == pThis->aOperands[pThis->idxMrmRmOp].iReg + && CIDET_OF_K_IS_SAME(pThis->fMrmRmOp, pThis->fMrmRegOp); + else if (CIDET_OF_K_IS_GPR(pThis->fMrmRegOp)) + { + Assert(!pThis->fUsesVexIndexRegs || pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg == UINT8_MAX); + pThis->fHasRegCollisionMemBase = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg; + pThis->fHasRegCollisionMemIndex = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg; + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + } + } + Assert(!pThis->fSib); + + return iReg != 0; +} + + +/** + * Selects the next MOD & R/M encoding, 16-bit addressing variant. + * + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + */ +static void cidetCoreSetupFirstBaseEncoding_MrmRmMod_16bit(PCIDETCORE pThis, uint8_t iReg) +{ + if (CidetInstrHasMrmRegOperand(pThis->pCurInstr)) + { + pThis->aOperands[pThis->idxMrmRmOp].iReg = 0; + pThis->aOperands[pThis->idxMrmRmOp].fIsMem = false; + pThis->aOperands[pThis->idxMrmRmOp].fIsRipRelative = false; + pThis->aOperands[pThis->idxMrmRmOp].fIsHighByteRegister = false; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + pThis->aOperands[pThis->idxMrmRmOp].iEffSeg = UINT8_MAX; + pThis->bModRm &= ~(X86_MODRM_RM_MASK | X86_MODRM_MOD_MASK); + pThis->bModRm |= 3 << X86_MODRM_MOD_SHIFT; + pThis->fRexB = false; + pThis->fRexX = false; + pThis->fHasMemoryOperand = false; + pThis->fHasRegCollisionDirect = iReg == 0 + && CIDET_OF_K_IS_SAME(pThis->fMrmRmOp, pThis->fMrmRegOp); + pThis->fHasRegCollisionMem = false; + pThis->fHasRegCollisionMemBase = false; + pThis->fHasRegCollisionMemIndex = false; + pThis->fHasStackRegInMrmRmBase = false; + } + else + { + Assert(CidetInstrHasMrmMemOperand(pThis->pCurInstr)); + pThis->aOperands[pThis->idxMrmRmOp].iReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].fIsMem = true; + pThis->aOperands[pThis->idxMrmRmOp].fIsRipRelative = false; + pThis->aOperands[pThis->idxMrmRmOp].fIsHighByteRegister = false; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = X86_GREG_xBX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = X86_GREG_xSI; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + pThis->aOperands[pThis->idxMrmRmOp].iEffSeg = UINT8_MAX; + pThis->bModRm &= ~(X86_MODRM_RM_MASK | X86_MODRM_MOD_MASK); + pThis->fRexB = false; + pThis->fRexX = false; + pThis->fHasMemoryOperand = true; + pThis->fHasRegCollisionDirect = false; + iReg -= pThis->fHasHighByteRegInMrmReg * 4; + pThis->fHasRegCollisionMemBase = iReg == X86_GREG_xBX && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + pThis->fHasRegCollisionMemIndex = iReg == X86_GREG_xSI && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + pThis->fHasStackRegInMrmRmBase = false; + } +} + + +/** + * Selects the next MOD & R/M encoding, 16-bit addressing variant. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + */ +static bool cidetCoreSetupNextBaseEncoding_MrmRmMod_16bit(PCIDETCORE pThis, uint8_t iReg) +{ + AssertRelease(!pThis->fRexB); + AssertRelease(!pThis->fRexX); + uint8_t iRm = pThis->bModRm & X86_MODRM_RM_MASK; + uint8_t iMod = (pThis->bModRm >> X86_MODRM_MOD_SHIFT) & X86_MODRM_MOD_SMASK; + if (iMod == 3) + { + /* + * Register access mode. + */ + Assert(pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands) && !pThis->aOperands[pThis->idxMrmRmOp].fIsMem); + Assert(!pThis->fHasMemoryOperand); + Assert(!pThis->fHasRegCollisionMem); + Assert(!pThis->fHasRegCollisionMemBase); + Assert(!pThis->fHasRegCollisionMemIndex); + if (iRm < 7) + { + iRm++; + pThis->aOperands[pThis->idxMrmRmOp].iReg = iRm; + pThis->bModRm &= ~X86_MODRM_RM_MASK; + pThis->bModRm |= iRm; + pThis->fHasRegCollisionDirect = iRm == iReg + && CIDET_OF_K_IS_SAME(pThis->fMrmRmOp, pThis->fMrmRegOp); + pThis->fHasStackRegInMrmRmBase = iRm == X86_GREG_xSP && CIDET_OF_K_IS_GPR(pThis->fMrmRmOp); + return true; + } + + /* If no memory modes, we're done. */ + if (!CidetInstrHasMrmMemOperand(pThis->pCurInstr)) + { + cidetCoreSetupFirstBaseEncoding_MrmRmMod_16bit(pThis, iReg); + return false; + } + + /* Next mode: 16-bit memory addressing without displacement. */ + pThis->aOperands[pThis->idxMrmRmOp].fIsMem = true; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + iMod = 0; + } + else + { + /* + * Memory access mode. + */ + Assert(pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands) && pThis->aOperands[pThis->idxMrmRmOp].fIsMem); + Assert(pThis->fHasMemoryOperand); + if (iRm < 7) + { + iRm++; + switch (iRm) + { + case 1: + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = X86_GREG_xBX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = X86_GREG_xDI; + break; + case 2: + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = X86_GREG_xBP; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = X86_GREG_xSI; + break; + case 3: + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = X86_GREG_xBP; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = X86_GREG_xDI; + break; + case 4: + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = X86_GREG_xSI; + break; + case 5: + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = X86_GREG_xDI; + break; + case 6: + if (iMod == 0) + { + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 2; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = UINT8_MAX; + } + else + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = X86_GREG_xBP; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + break; + case 7: + if (iMod == 0) + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = X86_GREG_xBX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + break; + default: AssertReleaseFailed(); + } + pThis->bModRm &= ~X86_MODRM_RM_MASK; + pThis->bModRm |= iRm; + if (CIDET_OF_K_IS_GPR(pThis->fMrmRegOp)) + { + iReg -= pThis->fHasHighByteRegInMrmReg * 4; + pThis->fHasRegCollisionMemBase = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg; + pThis->fHasRegCollisionMemIndex = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg; + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + } + return true; + } + + /* Last mode? */ + if (iMod >= 2) + { + cidetCoreSetupFirstBaseEncoding_MrmRmMod_16bit(pThis, iReg); + return false; + } + + /* Next memory addressing mode (if any). */ + iMod++; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp++; + } + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = X86_GREG_xBX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = X86_GREG_xSI; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + pThis->bModRm &= ~(X86_MODRM_RM_MASK | X86_MODRM_MOD_MASK); + pThis->bModRm |= iMod << X86_MODRM_MOD_SHIFT; + pThis->fHasMemoryOperand = true; + pThis->fHasRegCollisionDirect = false; + pThis->fHasStackRegInMrmRmBase = false; + if (CIDET_OF_K_IS_GPR(pThis->fMrmRmOp)) + { + iReg -= pThis->fHasHighByteRegInMrmReg * 4; + pThis->fHasRegCollisionMemBase = iReg == X86_GREG_xBX; + pThis->fHasRegCollisionMemIndex = iReg == X86_GREG_xSI; + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + } + return true; +} + + +/** + * Selects the first MOD & R/M encoding, 32-bit and 64-bit addressing variant. + * + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + * @param f64Bit Set if 64-bit, clear if 32-bit. + */ +static void cidetCoreSetupFirstBaseEncoding_MrmRmMod_32bit64bit(PCIDETCORE pThis, uint8_t iReg, bool f64Bit) +{ + RT_NOREF_PV(f64Bit); + if (CidetInstrHasMrmRegOperand(pThis->pCurInstr)) + { + pThis->aOperands[pThis->idxMrmRmOp].iReg = 0; + pThis->aOperands[pThis->idxMrmRmOp].fIsMem = false; + pThis->aOperands[pThis->idxMrmRmOp].fIsRipRelative = false; + pThis->aOperands[pThis->idxMrmRmOp].fIsHighByteRegister = false; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + pThis->aOperands[pThis->idxMrmRmOp].iEffSeg = UINT8_MAX; + pThis->bModRm &= ~(X86_MODRM_RM_MASK | X86_MODRM_MOD_MASK); + pThis->bModRm |= 3 << X86_MODRM_MOD_SHIFT; + pThis->fRexB = false; + pThis->fRexX = false; + pThis->fHasMemoryOperand = false; + pThis->fHasRegCollisionDirect = iReg == 0 + && CIDET_OF_K_IS_SAME(pThis->fMrmRmOp, pThis->fMrmRegOp); + pThis->fHasRegCollisionMem = false; + pThis->fHasRegCollisionMemBase = false; + pThis->fHasRegCollisionMemIndex = false; + pThis->fHasStackRegInMrmRmBase = false; + } + else + { + Assert(CidetInstrHasMrmMemOperand(pThis->pCurInstr)); + pThis->aOperands[pThis->idxMrmRmOp].iReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].fIsMem = true; + pThis->aOperands[pThis->idxMrmRmOp].fIsRipRelative = false; + pThis->aOperands[pThis->idxMrmRmOp].fIsHighByteRegister = false; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + pThis->aOperands[pThis->idxMrmRmOp].iEffSeg = UINT8_MAX; + pThis->bModRm &= ~(X86_MODRM_RM_MASK | X86_MODRM_MOD_MASK); + pThis->fRexB = false; + pThis->fRexX = false; + pThis->fHasMemoryOperand = true; + pThis->fHasRegCollisionDirect = false; + pThis->fHasRegCollisionMemIndex = false; + pThis->fHasRegCollisionMemBase = iReg == pThis->fHasHighByteRegInMrmReg * 4 && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase; + pThis->fHasStackRegInMrmRmBase = false; + } +} + + +/** + * Selects the next MOD & R/M encoding, 32-bit and 64-bit addressing variant. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + * @param f64Bit Set if 64-bit, clear if 32-bit. + */ +static bool cidetCoreSetupNextBaseEncoding_MrmRmMod_32bit64bit(PCIDETCORE pThis, uint8_t iReg, bool f64Bit) +{ + AssertRelease(!pThis->fRexX || CIDETMODE_IS_64BIT(pThis->bMode)); + AssertRelease(!pThis->fRexB || CIDETMODE_IS_64BIT(pThis->bMode)); + uint8_t iRm = (pThis->bModRm & X86_MODRM_RM_MASK) + pThis->fRexB * 8; + uint8_t iMod = (pThis->bModRm >> X86_MODRM_MOD_SHIFT) & X86_MODRM_MOD_SMASK; + if (iMod == 3) + { + /* + * Register access mode. + */ + Assert(pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands) && !pThis->aOperands[pThis->idxMrmRmOp].fIsMem); + Assert(!pThis->fHasMemoryOperand); + Assert(!pThis->fHasRegCollisionMem); + Assert(!pThis->fHasRegCollisionMemBase); + Assert(!pThis->fHasRegCollisionMemIndex); + + if (CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fRexX && !pThis->fNoRexPrefix) /* should be ignored. */ + { + pThis->fRexX = true; + return true; + } + + /* Reset the byte register kludges variables. */ + pThis->aOperands[pThis->idxMrmRmOp].fIsHighByteRegister = false; + pThis->fHasHighByteRegInMrmRm = false; + pThis->fNoRexPrefixMrmRm = false; + pThis->fNoRexPrefix = pThis->fNoRexPrefixMrmReg; + + if (iRm < (CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fNoRexPrefix ? 15 : 7)) + { + /* + * Byte register kludge. + */ + if ( pThis->aOperands[pThis->idxMrmRmOp].cb == 1 + && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp) + && iRm >= 3 + && ( iRm <= 6 + || (iRm == 7 && CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fRexX) ) ) + { + if (!pThis->fRexX && iRm >= 4 && CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fNoRexPrefix) + { + /* The AMD64 low variants: spl, bpl, sil and dil. (Using fRexX here as REG covers fRex.) */ + pThis->fRexX = true; + pThis->fHasRegCollisionDirect = CIDET_OF_K_IS_GPR(pThis->fMrmRegOp) + && iRm == iReg - pThis->fHasHighByteRegInMrmReg * 4; + pThis->fHasStackRegInMrmRmBase = iRm == X86_GREG_xSP && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + } + else + { + /* Next register: ah, ch, dh and bh. */ + iRm++; + pThis->aOperands[pThis->idxMrmRmOp].iReg = iRm; + pThis->bModRm &= ~X86_MODRM_RM_MASK; + pThis->bModRm |= iRm & X86_MODRM_RM_MASK; + pThis->fRexB = false; + pThis->fRexX = false; + if (!pThis->fRexR && !pThis->fRexW && !pThis->fRex) + { + pThis->fNoRexPrefixMrmRm = true; + pThis->fNoRexPrefix = true; + pThis->fHasHighByteRegInMrmRm = true; + pThis->aOperands[pThis->idxMrmRmOp].fIsHighByteRegister = true; + pThis->fHasRegCollisionDirect = CIDET_OF_K_IS_GPR(pThis->fMrmRegOp) + && iRm - 4 == iReg - pThis->fHasHighByteRegInMrmReg * 4; + pThis->fHasStackRegInMrmRmBase = false; + + } + else + { + /* Can't do the high stuff, so do the spl, bpl, sil and dil variation instead. + Note! We don't set the RexX yet since the base register or operand width holds it down. */ + pThis->fHasRegCollisionDirect = CIDET_OF_K_IS_GPR(pThis->fMrmRegOp) + && iRm == iReg - pThis->fHasHighByteRegInMrmReg * 4; + pThis->fHasStackRegInMrmRmBase = iRm == X86_GREG_xSP && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + } + } + } + /* + * Normal register. + */ + else + { + iRm++; + pThis->aOperands[pThis->idxMrmRmOp].iReg = iRm; + pThis->bModRm &= ~X86_MODRM_RM_MASK; + pThis->bModRm |= iRm & X86_MODRM_RM_MASK; + pThis->fRexB = iRm >= 8; + pThis->fRexX = false; + pThis->fHasRegCollisionDirect = iRm == iReg && CIDET_OF_K_IS_SAME(pThis->fMrmRmOp, pThis->fMrmRegOp); + pThis->fHasStackRegInMrmRmBase = iRm == X86_GREG_xSP && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + } + return true; + } + + /* If no memory modes, we're done. */ + if (!CidetInstrHasMrmMemOperand(pThis->pCurInstr)) + { + cidetCoreSetupFirstBaseEncoding_MrmRmMod_32bit64bit(pThis, iReg, f64Bit); + return false; + } + + /* Next mode: 32-bit/64-bit memory addressing without displacement. */ + pThis->aOperands[pThis->idxMrmRmOp].fIsMem = true; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + iMod = 0; + } + else + { + /* + * Memory access mode. + */ + Assert(pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands) && pThis->aOperands[pThis->idxMrmRmOp].fIsMem); + Assert(pThis->fHasMemoryOperand); + Assert(!pThis->fHasStackRegInMrmRmBase); + if (iRm < (CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fNoRexPrefix ? 15 : 7)) + { + iRm++; + if (iRm == 12) + iRm++; /* Leave REX.B=1 to the next-sib-base function. */ + if (iRm == 4) + { + /* SIB */ + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = 0; + pThis->fSib = true; + pThis->bSib = 0; + } + else if ((iRm & 7) == 5 && iMod == 0) + { + /* Absolute or wrt rip addressing. */ + pThis->aOperands[pThis->idxMrmRmOp].fIsRipRelative = CIDETMODE_IS_64BIT(pThis->bMode); + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 4; + } + else + { + if ((iRm & 7) == 6 && iMod == 0) + { + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRmOp].fIsRipRelative = false; + } + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = iRm; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + } + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + pThis->bModRm &= ~X86_MODRM_RM_MASK; + pThis->bModRm |= iRm & X86_MODRM_RM_MASK; + pThis->fRexB = iRm >= 8; + pThis->fRexX = false; + if (CIDET_OF_K_IS_GPR(pThis->fMrmRegOp)) + { + iReg -= pThis->fHasHighByteRegInMrmReg * 4; + pThis->fHasRegCollisionMemBase = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg; + pThis->fHasRegCollisionMemIndex = iReg == pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg; + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + } + return true; + } + + /* Last mode? */ + if (iMod >= 2) + { + cidetCoreSetupFirstBaseEncoding_MrmRmMod_32bit64bit(pThis, iReg, f64Bit); + return false; + } + + /* Next memory addressing mode (if any). */ + iMod++; + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = iMod == 1 ? 1 : 4; + } + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + pThis->bModRm &= ~(X86_MODRM_RM_MASK | X86_MODRM_MOD_MASK); + pThis->bModRm |= iMod << X86_MODRM_MOD_SHIFT; + pThis->fRexB = false; + pThis->fRexX = false; + pThis->fHasMemoryOperand = true; + pThis->fHasRegCollisionDirect = false; + pThis->fHasRegCollisionMemIndex = false; + pThis->fHasRegCollisionMemBase = iReg == pThis->fHasHighByteRegInMrmReg * 4 + && CIDET_OF_K_IS_GPR(pThis->fMrmRmOp); + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase; + pThis->fHasStackRegInMrmRmBase = false; + return true; +} + + +/** + * Selects the next MOD & R/M encoding. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + */ +static bool cidetCoreSetupNextBaseEncoding_MrmRmMod(PCIDETCORE pThis, uint8_t iReg) +{ + if (pThis->cbAddrMode == 2) + return cidetCoreSetupNextBaseEncoding_MrmRmMod_16bit(pThis, iReg); + if (pThis->cbAddrMode == 4) + return cidetCoreSetupNextBaseEncoding_MrmRmMod_32bit64bit(pThis, iReg, false); + if (pThis->cbAddrMode == 8) + return cidetCoreSetupNextBaseEncoding_MrmRmMod_32bit64bit(pThis, iReg, true); + AssertReleaseFailedReturn(false); +} + + + +/** + * Selects the next SIB base register (/ encoding). + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + */ +static bool cidetCoreSetupNextBaseEncoding_SibBase(PCIDETCORE pThis, uint8_t iReg) +{ + AssertRelease(!pThis->fRexB || CIDETMODE_IS_64BIT(pThis->bMode)); + + uint8_t iBase = (pThis->bSib & X86_SIB_BASE_MASK) + pThis->fRexB * 8; + iBase = (iBase + 1) & (CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fNoRexPrefix ? 15 : 7); + + if ((iBase & 7) == 5 && (pThis->bModRm & X86_MODRM_MOD_MASK) == 0) + { + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 4; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = UINT8_MAX; + } + else + { + if ((iBase & 7) == 6 && (pThis->bModRm & X86_MODRM_MOD_MASK) == 0) + pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp = 0; + pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg = iBase; + } + pThis->bSib &= ~X86_SIB_BASE_MASK; + pThis->bSib |= iBase & X86_SIB_BASE_MASK; + pThis->fRexB = iBase >= 8; + pThis->fHasRegCollisionMemBase = pThis->aOperands[pThis->idxMrmRmOp].iMemBaseReg + == iReg - pThis->fHasHighByteRegInMrmReg * 4 + && CIDET_OF_K_IS_GPR(pThis->fMrmRegOp); + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + pThis->fHasStackRegInMrmRmBase = iBase == X86_GREG_xSP; + + return iBase != 0; +} + + +/** + * Selects the next SIB index register (/ encoding). + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + */ +static bool cidetCoreSetupNextBaseEncoding_SibIndex(PCIDETCORE pThis, uint8_t iReg) +{ + AssertRelease(!pThis->fRexX || CIDETMODE_IS_64BIT(pThis->bMode)); + Assert(pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands) && pThis->aOperands[pThis->idxMrmRmOp].fIsMem); + + uint8_t iIndex = ((pThis->bSib >> X86_SIB_INDEX_SHIFT) & X86_SIB_INDEX_SMASK) + pThis->fRexX * 8; + iIndex = (iIndex + 1) & (CIDETMODE_IS_64BIT(pThis->bMode) && !pThis->fNoRexPrefix ? 15 : 7); + + if (iIndex == 4 && !pThis->fUsesVexIndexRegs) + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = UINT8_MAX; + else + pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg = iIndex; + pThis->bSib &= ~X86_SIB_INDEX_MASK; + pThis->bSib |= (iIndex & X86_SIB_INDEX_SMASK) << X86_SIB_INDEX_SHIFT; + pThis->fRexX = iIndex >= 8; + pThis->fHasRegCollisionMemIndex = pThis->aOperands[pThis->idxMrmRmOp].iMemIndexReg + == iReg - pThis->fHasHighByteRegInMrmReg * 4 + && ( !pThis->fUsesVexIndexRegs + ? CIDET_OF_K_IS_GPR(pThis->fMrmRegOp) : CIDET_OF_K_IS_VRX(pThis->fMrmRegOp) ); + pThis->fHasRegCollisionMem = pThis->fHasRegCollisionMemBase || pThis->fHasRegCollisionMemIndex; + + return iIndex != 0; +} + + +/** + * Selects the next SIB scale. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + * @param iReg The value of MODRM.REG /w REX.R applied. + */ +static bool cidetCoreSetupNextBaseEncoding_SibScale(PCIDETCORE pThis, uint8_t iReg) +{ + RT_NOREF_PV(iReg); + switch ((pThis->bSib >> X86_SIB_SCALE_SHIFT) & X86_SIB_SCALE_SMASK) + { + case 0: + pThis->bSib |= 1 << X86_SIB_SCALE_SHIFT; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 2; + return true; + case 1: + pThis->bSib &= ~X86_SIB_SCALE_MASK; + pThis->bSib |= 2 << X86_SIB_SCALE_SHIFT; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 4; + return true; + case 2: + pThis->bSib |= 3 << X86_SIB_SCALE_SHIFT; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 8; + return true; + case 3: + pThis->bSib &= ~X86_SIB_SCALE_MASK; + pThis->aOperands[pThis->idxMrmRmOp].uMemScale = 1; + return false; + + default: AssertReleaseFailedReturn(false); + } +} + + +/** + * Selects the next segment prefix. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + */ +static bool cidetCoreSetupNextBaseEncoding_SegmentPrefix(PCIDETCORE pThis) +{ + if ( pThis->fHasMemoryOperand + && (pThis->fTestCfg & CIDET_TESTCFG_SEG_PRF_MASK)) + { + switch (pThis->uSegPrf) + { + case X86_SREG_COUNT: + pThis->uSegPrf = X86_SREG_ES; + if (pThis->fTestCfg & CIDET_TESTCFG_SEG_PRF_ES) + return true; + RT_FALL_THRU(); + case X86_SREG_ES: + pThis->uSegPrf = X86_SREG_CS; + if (pThis->fTestCfg & CIDET_TESTCFG_SEG_PRF_CS) + return true; + RT_FALL_THRU(); + case X86_SREG_CS: + pThis->uSegPrf = X86_SREG_SS; + if (pThis->fTestCfg & CIDET_TESTCFG_SEG_PRF_SS) + return true; + RT_FALL_THRU(); + case X86_SREG_SS: + pThis->uSegPrf = X86_SREG_DS; + if (pThis->fTestCfg & CIDET_TESTCFG_SEG_PRF_DS) + return true; + RT_FALL_THRU(); + case X86_SREG_DS: + pThis->uSegPrf = X86_SREG_FS; + if (pThis->fTestCfg & CIDET_TESTCFG_SEG_PRF_FS) + return true; + RT_FALL_THRU(); + case X86_SREG_FS: + pThis->uSegPrf = X86_SREG_GS; + if (pThis->fTestCfg & CIDET_TESTCFG_SEG_PRF_GS) + return true; + RT_FALL_THRU(); + case X86_SREG_GS: + break; + default: AssertReleaseFailedBreak(); + } + pThis->uSegPrf = X86_SREG_COUNT; + } + return false; +} + + +/** + * Updates the variable sized operands. + * + * @param pThis The core state structure. + */ +static void cidetCoreUpdateOperandSizes(PCIDETCORE pThis) +{ + uint8_t iOp = pThis->cOperands; + while (iOp-- > 0) + pThis->aOperands[iOp].cb = (uint8_t)CidetCoreGetOperandSize(pThis, iOp); +} + + +/** + * Selects the next operand size. + * + * @returns @c true if done, @c false if the next wheel needs to be moved. + * @param pThis The core state structure. + */ +static bool cidetCoreSetupNextBaseEncoding_OperandSize(PCIDETCORE pThis) +{ + if (CidetInstrRespondsToOperandSizePrefixes(pThis->pCurInstr)) + { + if (CIDETMODE_IS_64BIT(pThis->bMode)) + { + switch (pThis->fOpSizePrf + pThis->fRexW * 2) + { + case 0: + pThis->fOpSizePrf = true; + cidetCoreUpdateOperandSizes(pThis); + return true; + case 1: + pThis->fOpSizePrf = false; + if (pThis->fNoRexPrefix) + break; + pThis->fRexW = true; + cidetCoreUpdateOperandSizes(pThis); + return true; + case 2: + pThis->fOpSizePrf = true; /* check that it's ignored. */ + cidetCoreUpdateOperandSizes(pThis); + return true; + default: AssertReleaseFailed(); + case 3: + break; + } + } + else + { + if (!pThis->fOpSizePrf) + { + pThis->fOpSizePrf = true; + cidetCoreUpdateOperandSizes(pThis); + return true; + } + } + pThis->fRexW = false; + pThis->fOpSizePrf = false; + cidetCoreUpdateOperandSizes(pThis); + } + return false; +} + + +bool CidetCoreSetupNextBaseEncoding(PCIDETCORE pThis) +{ + if (pThis->fUsesModRm) + { + /* + * The wheels are lined up as follows: + * 1. Address size prefix. + * 2. MODRM.MOD + * 3. MODRM.REG + REX.R + * 4. MODRM.R/M + REX.B + * 5. SIB - MODRM.R/M == 4 && MODRM.MOD != 3: + * 5a) SIB.BASE + REX.B + * 5b) SIB.INDEX + REX.X + * 5c) SIB.SCALE + * 6. Segment prefix overrides if applicable and supported (memory). + * 7. Operand size prefix and REX.W if applicable. + */ + if (cidetCoreSetupNextBaseEncoding_OperandSize(pThis)) + return true; + if (cidetCoreSetupNextBaseEncoding_SegmentPrefix(pThis)) + return true; + + /* The ModR/M register value for collision detection. */ + uint8_t iReg = ((pThis->bModRm >> X86_MODRM_REG_SHIFT) & X86_MODRM_REG_SMASK) + pThis->fRexR * 8; + + if (pThis->fSib) + { + AssertRelease(pThis->fHasMemoryOperand); + if (cidetCoreSetupNextBaseEncoding_SibScale(pThis, iReg)) + return true; + if (cidetCoreSetupNextBaseEncoding_SibIndex(pThis, iReg)) + return true; + if (cidetCoreSetupNextBaseEncoding_SibBase(pThis, iReg)) + return true; + Assert(pThis->bSib == 0); + pThis->fSib = false; + } + + if (cidetCoreSetupNextBaseEncoding_MrmRmMod(pThis, iReg)) + return true; + if (cidetCoreSetupNextBaseEncoding_MrmReg(pThis, iReg)) + return true; + if (cidetCoreSetupNextBaseEncoding_AddressSize(pThis)) + return true; + } + else + AssertFailedReturn(false); + return false; +} + + +bool CidetCoreSetupFirstBaseEncoding(PCIDETCORE pThis) +{ + /* + * Reset all the knobs and wheels. + */ + pThis->fSib = false; + pThis->uSegPrf = X86_SREG_COUNT; + pThis->fAddrSizePrf = false; + pThis->fOpSizePrf = false; + pThis->fRexW = false; + pThis->fRexR = false; + pThis->fRexX = false; + pThis->fRexB = false; + pThis->fRex = false; + pThis->bModRm = 0; + pThis->bSib = 0; + + /* Indicators. */ + pThis->cbAddrMode = CIDETMODE_GET_BYTE_COUNT(pThis->bMode); + pThis->fHasMemoryOperand = false; + pThis->fHasRegCollisionMem = false; + pThis->fHasRegCollisionMemBase = false; + pThis->fHasRegCollisionMemIndex = false; + pThis->fHasStackRegInMrmRmBase = false; + + /* + * Now, drill down on the instruction encoding. + */ + if (pThis->pCurInstr->fFlags & CIDET_IF_MODRM) + { + Assert(pThis->fUsesModRm == true); + cidetCoreSetupFirstBaseEncoding_MrmReg(pThis); + if (pThis->cbAddrMode == 2) + cidetCoreSetupFirstBaseEncoding_MrmRmMod_16bit(pThis, 0); + else if (pThis->cbAddrMode == 4) + cidetCoreSetupFirstBaseEncoding_MrmRmMod_32bit64bit(pThis, 0, false); + else if (pThis->cbAddrMode == 8) + cidetCoreSetupFirstBaseEncoding_MrmRmMod_32bit64bit(pThis, 0, true); + else + AssertReleaseFailedReturn(false); + } + else + AssertFailedReturn(false); + return true; +} + + +/** + * The next memory operand configuration. + * + * @returns true if new one to test, false if we've reached end already. + * @param pThis The core state structure. + */ +bool CidetCoreSetupNextMemoryOperandConfig(PCIDETCORE pThis) +{ + RT_NOREF_PV(pThis); + return false; +} + + +/** + * Sets up the first memory operand configuration and counts memory operands. + * + * @returns true on success, false if no data buffers configured or failure. + * @param pThis The core state structure. + */ +bool CidetCoreSetupFirstMemoryOperandConfig(PCIDETCORE pThis) +{ + pThis->cMemoryOperands = 0; + PCIDETBUF pDataBuf = &pThis->DataBuf; + uint8_t idxOp = pThis->cOperands; + while (idxOp-- > 0) + if (!pThis->aOperands[idxOp].fIsMem) + pThis->aOperands[idxOp].pDataBuf = NULL; + else + { + if (RT_UNLIKELY(!pThis->cDataBufConfigs)) + return false; + + pDataBuf->idxCfg = 0; + pDataBuf->pCfg = &pThis->paDataBufConfigs[0]; + pDataBuf->off = 0; + pDataBuf->cb = pThis->aOperands[idxOp].cb; + pDataBuf->cbSegLimit = UINT16_MAX; + pDataBuf->offSegBase = 0; + pDataBuf->fActive = false; + pDataBuf->idxOp = idxOp; + pDataBuf->fXcptAfterInstruction = false; + pDataBuf->enmExpectXcpt = kCidetExpectXcpt_None; + pThis->aOperands[idxOp].pDataBuf = pDataBuf; + pThis->cMemoryOperands++; + pDataBuf++; + } + + /** @todo implement more than one memory operand. */ + AssertReleaseReturn(pThis->cMemoryOperands <= 1, false); + return true; +} + + +/** + * The next code buffer configuration. + * + * @returns true if new one to test, false if we've reached end already. + * @param pThis The core state structure. + */ +bool CidetCoreSetupNextCodeBufferConfig(PCIDETCORE pThis) +{ + RT_NOREF_PV(pThis); + return false; +} + + +/** + * Sets up the first code buffer configuration. + * + * @returns true on success, false if no data buffers configured or failure. + * @param pThis The core state structure. + */ +bool CidetCoreSetupFirstCodeBufferConfig(PCIDETCORE pThis) +{ + Assert(pThis->cCodeBufConfigs > 0); + Assert(CIDETBUF_IS_CODE(pThis->paCodeBufConfigs[0].fFlags)); + pThis->CodeBuf.idxCfg = 0; + pThis->CodeBuf.pCfg = &pThis->paCodeBufConfigs[0]; + pThis->CodeBuf.off = 0; + pThis->CodeBuf.cb = 0x1000; + pThis->CodeBuf.cbSegLimit = UINT16_MAX; + pThis->CodeBuf.offSegBase = 0; + pThis->CodeBuf.fActive = true; + pThis->CodeBuf.idxOp = 7; + pThis->CodeBuf.fXcptAfterInstruction = false; + pThis->CodeBuf.enmExpectXcpt = kCidetExpectXcpt_None; + return true; +} + + +/** + * Gets the (encoded) size of the given operand in the current context. + * + * @returns Size in bytes. + * @param pThis The core state structure (for context). + * @param iOp The operand index. + */ +uint32_t CidetCoreGetOperandSize(PCIDETCORE pThis, uint8_t iOp) +{ + Assert(iOp < RT_ELEMENTS(pThis->aOperands)); + uint32_t cbOp = g_acbCidetOfSizes[(pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) >> CIDET_OF_Z_SHIFT]; + if (cbOp == UINT16_MAX) + { + Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_VAR_WDQ); + if (CIDETMODE_IS_64BIT(pThis->bMode)) + { + if (pThis->fRexW) + cbOp = 8; + else if (!pThis->fOpSizePrf) + cbOp = 4; + else + cbOp = 2; + } + else if (CIDETMODE_IS_32BIT(pThis->bMode)) + cbOp = !pThis->fOpSizePrf ? 4 : 2; + else + { + Assert(CIDETMODE_IS_16BIT(pThis->bMode)); + cbOp = !pThis->fOpSizePrf ? 2 : 4; + } + return cbOp; + } + + if (cbOp == UINT16_MAX - 1) + { + Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_SPECIAL); + AssertReleaseFailedReturn(0); + } + + if (cbOp) + { +#ifdef VBOX_STRICT + switch (cbOp) + { + case 1: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_BYTE); break; + case 2: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_WORD); break; + case 4: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_DWORD); break; + case 8: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_QWORD); break; + case 10: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_TBYTE); break; + case 16: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_OWORD); break; + case 32: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_YWORD); break; + case 64: Assert((pThis->aOperands[iOp].fFlags & CIDET_OF_Z_MASK) == CIDET_OF_Z_ZWORD); break; + default: AssertFailed(); + } +#endif + return cbOp; + } + AssertReleaseFailedReturn(0); +} + + +bool CideCoreSetInstruction(PCIDETCORE pThis, PCCIDETINSTR pInstr) +{ + AssertReleaseMsgReturn(RT_VALID_PTR(pInstr), ("%p\n", pInstr), false); + + pThis->pCurInstr = pInstr; + + /* + * Extract info from the instruction descriptor. + */ + pThis->fUsesModRm = false; + pThis->fUsesVexIndexRegs = false; + pThis->idxMrmRegOp = 7; + pThis->idxMrmRmOp = 7; + pThis->fMrmRegOp = 0; + pThis->fMrmRmOp = 0; + pThis->fInstrFlags = pInstr->fFlags; + pThis->cOperands = pInstr->cOperands; + if (pInstr->fFlags & CIDET_IF_MODRM) + { + pThis->fUsesModRm = true; + for (uint8_t iOp = 0; iOp < pInstr->cOperands; iOp++) + if (pInstr->afOperands[iOp] & CIDET_OF_M_REG) + { + pThis->idxMrmRegOp = iOp; + pThis->fMrmRegOp = pInstr->afOperands[iOp]; + } + else if (pInstr->afOperands[iOp] & CIDET_OF_M_RM) + { + pThis->idxMrmRmOp = iOp; + pThis->fMrmRmOp = pInstr->afOperands[iOp]; + } + } + else + AssertFailedReturn(false); + + uint8_t iOp; + for (iOp = 0; iOp < pInstr->cOperands; iOp++) + { + pThis->aOperands[iOp].fFlags = pInstr->afOperands[iOp]; + pThis->aOperands[iOp].iReg = UINT8_MAX; + pThis->aOperands[iOp].cb = (uint8_t)CidetCoreGetOperandSize(pThis, iOp); + pThis->aOperands[iOp].fIsImmediate = (pInstr->afOperands[iOp] & CIDET_OF_K_MASK) == CIDET_OF_K_IMM; + pThis->aOperands[iOp].fIsMem = (pInstr->afOperands[iOp] & CIDET_OF_K_MASK) == CIDET_OF_K_MEM; + pThis->aOperands[iOp].fIsRipRelative = false; + pThis->aOperands[iOp].cbMemDisp = 0; + pThis->aOperands[iOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[iOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[iOp].uMemScale = 1; + pThis->aOperands[iOp].iEffSeg = UINT8_MAX; + pThis->aOperands[iOp].offSeg = UINT64_MAX; + pThis->aOperands[iOp].uEffAddr = UINT64_MAX; + pThis->aOperands[iOp].uImmDispValue = UINT64_MAX; + pThis->aOperands[iOp].uMemBaseRegValue = UINT64_MAX; + pThis->aOperands[iOp].uMemIndexRegValue = UINT64_MAX; + pThis->aOperands[iOp].In.pv = NULL; + pThis->aOperands[iOp].Expected.pv = NULL; + pThis->aOperands[iOp].pDataBuf = NULL; + } + + for (; iOp < RT_ELEMENTS(pThis->aOperands); iOp++) + { + pThis->aOperands[iOp].fFlags = 0; + pThis->aOperands[iOp].iReg = UINT8_MAX; + pThis->aOperands[iOp].cb = 0; + pThis->aOperands[iOp].fIsImmediate = false; + pThis->aOperands[iOp].fIsMem = false; + pThis->aOperands[iOp].fIsRipRelative = false; + pThis->aOperands[iOp].cbMemDisp = 0; + pThis->aOperands[iOp].iMemBaseReg = UINT8_MAX; + pThis->aOperands[iOp].iMemIndexReg = UINT8_MAX; + pThis->aOperands[iOp].uMemScale = 1; + pThis->aOperands[iOp].iEffSeg = UINT8_MAX; + pThis->aOperands[iOp].offSeg = UINT64_MAX; + pThis->aOperands[iOp].uEffAddr = UINT64_MAX; + pThis->aOperands[iOp].uImmDispValue = UINT64_MAX; + pThis->aOperands[iOp].uMemBaseRegValue = UINT64_MAX; + pThis->aOperands[iOp].uMemIndexRegValue = UINT64_MAX; + pThis->aOperands[iOp].In.pv = NULL; + pThis->aOperands[iOp].Expected.pv = NULL; + pThis->aOperands[iOp].pDataBuf = NULL; + } + + /* + * Reset various things. + */ + for (uint32_t i = 0; i < RT_ELEMENTS(pThis->aiInOut); i++) + pThis->aiInOut[i] = 0; + + return true; +} + + +bool CidetCoreSetupInOut(PCIDETCORE pThis) +{ + /* + * Enumerate the operands. + */ + uint8_t *pbBuf = &pThis->abBuf[0]; + pbBuf = RT_ALIGN_PT(pbBuf, 16, uint8_t *); + + uint8_t idxOp = pThis->cOperands; + while (idxOp-- > 0) + { + if (pThis->aOperands[idxOp].fIsMem) + { + /* + * Memory operand. + */ + Assert(pThis->aOperands[idxOp].fIsMem); + + /* Set the In & Expected members to point to temporary buffer space. */ + pThis->aOperands[idxOp].Expected.pu8 = pbBuf; + pbBuf += pThis->aOperands[idxOp].cb; + pbBuf = RT_ALIGN_PT(pbBuf, 16, uint8_t *); + + pThis->aOperands[idxOp].In.pu8 = pbBuf; + pbBuf += pThis->aOperands[idxOp].cb; + pbBuf = RT_ALIGN_PT(pbBuf, 16, uint8_t *); + + /* Initialize the buffer we're gonna use. */ + pThis->aOperands[idxOp].iEffSeg = pThis->uSegPrf != X86_SREG_COUNT + ? pThis->uSegPrf + : !(pThis->aOperands[idxOp].fFlags & CIDET_OF_ALWAYS_SEG_ES) ? X86_SREG_DS + : X86_SREG_ES; + + PCIDETBUF pDataBuf = pThis->aOperands[idxOp].pDataBuf; + AssertReleaseReturn(pDataBuf, false); + Assert(pDataBuf->cb == pThis->aOperands[idxOp].cb); + Assert(pDataBuf->idxOp == idxOp); + if (!pThis->pfnReInitDataBuf(pThis, pDataBuf)) + { + pThis->cSkippedReInitDataBuf++; + return false; + } + pDataBuf->fActive = true; + + /* Calc buffer related operand members. */ + pThis->aOperands[idxOp].uEffAddr = pDataBuf->uEffBufAddr + pDataBuf->off; + uint64_t offSeg = pThis->aOperands[idxOp].uEffAddr - pDataBuf->uSegBase; + pThis->aOperands[idxOp].offSeg = offSeg; + AssertRelease(offSeg <= g_au64ByteSizeToMask[pThis->cbAddrMode]); + + /* + * Select register and displacement values for the buffer addressing (works on offSeg). + */ + uint8_t const iMemIndexReg = pThis->aOperands[idxOp].iMemIndexReg; + uint8_t const iMemBaseReg = pThis->aOperands[idxOp].iMemBaseReg; + if (pThis->aOperands[idxOp].fIsRipRelative) + { + /* rip relative. */ + pThis->aOperands[idxOp].uImmDispValue = offSeg - (pThis->InCtx.rip + pThis->cbInstr); + Assert(pThis->aOperands[idxOp].cbMemDisp == 4); + if ( (int64_t)pThis->aOperands[idxOp].uImmDispValue > INT32_MAX + || (int64_t)pThis->aOperands[idxOp].uImmDispValue < INT32_MIN) + { + pThis->cSkippedDataBufWrtRip++; + return false; + } + } + else if (iMemBaseReg != UINT8_MAX) + { + if ( iMemBaseReg != iMemIndexReg + || pThis->fUsesVexIndexRegs) + { + /* [base] or [base + disp] or [base + index * scale] or [base + index * scale + disp] */ + if (pThis->aOperands[idxOp].cbMemDisp > 0) + { + pThis->aOperands[idxOp].uImmDispValue = CidetCoreGetRandS64(pThis, pThis->aOperands[idxOp].cbMemDisp); + offSeg -= (int64_t)pThis->aOperands[idxOp].uImmDispValue; + } + + if (iMemIndexReg != UINT8_MAX) + { + pThis->aOperands[idxOp].uMemIndexRegValue = CidetCoreGetRandU64(pThis, pThis->cbAddrMode); + offSeg -= pThis->aOperands[idxOp].uMemIndexRegValue * pThis->aOperands[idxOp].uMemScale; + } + + pThis->aOperands[idxOp].uMemBaseRegValue = offSeg & g_au64ByteSizeToMask[pThis->cbAddrMode]; + } + else + { + /* base == index; [base + index * scale] or [base * (scale + 1)]. */ + uint8_t const uEffScale = pThis->aOperands[idxOp].uMemScale + 1; + if (pThis->aOperands[idxOp].cbMemDisp > 0) + { + pThis->aOperands[idxOp].uImmDispValue = CidetCoreGetRandS64(pThis, pThis->aOperands[idxOp].cbMemDisp); + offSeg -= (int64_t)pThis->aOperands[idxOp].uImmDispValue; + offSeg &= g_au64ByteSizeToMask[pThis->cbAddrMode]; + uint8_t uRemainder = offSeg % uEffScale; + if (uRemainder != 0) + { + Assert(pThis->aOperands[idxOp].cbMemDisp < 8); + Assert( (int64_t)pThis->aOperands[idxOp].uImmDispValue + <= g_ai64ByteSizeToMax[pThis->aOperands[idxOp].cbMemDisp]); + pThis->aOperands[idxOp].uImmDispValue = (int64_t)pThis->aOperands[idxOp].uImmDispValue + + uRemainder; + offSeg -= uRemainder; + if ( (int64_t)pThis->aOperands[idxOp].uImmDispValue + > g_ai64ByteSizeToMax[pThis->aOperands[idxOp].cbMemDisp]) + { + pThis->aOperands[idxOp].uImmDispValue -= uEffScale; + offSeg += uEffScale; + } + Assert(offSeg % uEffScale == 0); + } + } + else + { + offSeg &= g_au64ByteSizeToMask[pThis->cbAddrMode]; + if (offSeg % uEffScale != 0) + { + pThis->cSkippedSameBaseIndexRemainder++; + return false; + } + } + offSeg /= uEffScale; + pThis->aOperands[idxOp].uMemBaseRegValue = pThis->aOperands[idxOp].uMemIndexRegValue = offSeg; + } + } + else if (iMemIndexReg != UINT8_MAX) + { + /* [index * scale] or [index * scale + disp] */ + if (pThis->aOperands[idxOp].cbMemDisp > 0) + { + pThis->aOperands[idxOp].uImmDispValue = CidetCoreGetRandS64(pThis, pThis->aOperands[idxOp].cbMemDisp); + offSeg -= (int64_t)pThis->aOperands[idxOp].uImmDispValue; + pThis->aOperands[idxOp].uImmDispValue += offSeg & (RT_BIT_64(pThis->aOperands[idxOp].uMemScale) - 1); + offSeg &= ~(RT_BIT_64(pThis->aOperands[idxOp].uMemScale) - 1); + } + else if (offSeg & (RT_BIT_64(pThis->aOperands[idxOp].uMemScale) - 1)) + { + pThis->cSkippedOnlyIndexRemainder++; + return false; + } + + pThis->aOperands[idxOp].uMemIndexRegValue = offSeg / pThis->aOperands[idxOp].uMemScale; + Assert((offSeg % pThis->aOperands[idxOp].uMemScale) == 0); + AssertRelease(!pThis->fUsesVexIndexRegs); /** @todo implement VEX indexing */ + } + else + { + /* [disp] */ + Assert( pThis->aOperands[idxOp].cbMemDisp == 8 + || pThis->aOperands[idxOp].cbMemDisp == 4 + || pThis->aOperands[idxOp].cbMemDisp == 2 + || pThis->aOperands[idxOp].cbMemDisp == 1); + if ( pThis->aOperands[idxOp].cbMemDisp == 4 + ? (int64_t)offSeg != (int32_t)offSeg + : pThis->aOperands[idxOp].cbMemDisp == 2 + ? (int64_t)offSeg != (int16_t)offSeg + : pThis->aOperands[idxOp].cbMemDisp == 1 + ? (int64_t)offSeg != (int8_t)offSeg + : false /* 8 */) + { + pThis->cSkippedDirectAddressingOverflow++; + return false; + } + pThis->aOperands[idxOp].uImmDispValue = offSeg; + } + + /* + * Modify the input and expected output contexts with the base and + * index register values. To simplify verification and the work + * here, we update the uMemBaseRegValue and uMemIndexRegValue + * members to reflect the whole register. + */ + if (iMemBaseReg != UINT8_MAX) + { + if (pThis->cbAddrMode == 4) + { + pThis->aOperands[idxOp].uMemBaseRegValue &= UINT32_MAX; + pThis->aOperands[idxOp].uMemBaseRegValue |= pThis->InCtx.aGRegs[iMemBaseReg] & UINT64_C(0xffffffff00000000); + } + else if (pThis->cbAddrMode == 2) + { + pThis->aOperands[idxOp].uMemBaseRegValue &= UINT16_MAX; + pThis->aOperands[idxOp].uMemBaseRegValue |= pThis->InCtx.aGRegs[iMemBaseReg] & UINT64_C(0xffffffffffff0000); + } + pThis->InCtx.aGRegs[iMemBaseReg] = pThis->aOperands[idxOp].uMemBaseRegValue; + pThis->ExpectedCtx.aGRegs[iMemBaseReg] = pThis->aOperands[idxOp].uMemBaseRegValue; + } + + if (iMemIndexReg != UINT8_MAX) + { + if (pThis->cbAddrMode == 4) + { + pThis->aOperands[idxOp].uMemIndexRegValue &= UINT32_MAX; + pThis->aOperands[idxOp].uMemIndexRegValue |= pThis->InCtx.aGRegs[iMemIndexReg] & UINT64_C(0xffffffff00000000); + } + else if (pThis->cbAddrMode == 2) + { + pThis->aOperands[idxOp].uMemIndexRegValue &= UINT16_MAX; + pThis->aOperands[idxOp].uMemIndexRegValue |= pThis->InCtx.aGRegs[iMemIndexReg] & UINT64_C(0xffffffffffff0000); + } + pThis->InCtx.aGRegs[iMemIndexReg] = pThis->aOperands[idxOp].uMemIndexRegValue; + pThis->ExpectedCtx.aGRegs[iMemIndexReg] = pThis->aOperands[idxOp].uMemIndexRegValue; + } + } + else + { + /* + * Non-memory, so clear the memory related members. + */ + Assert(!pThis->aOperands[idxOp].fIsMem); + pThis->aOperands[idxOp].iEffSeg = UINT8_MAX; + pThis->aOperands[idxOp].offSeg = UINT64_MAX; + pThis->aOperands[idxOp].uEffAddr = UINT64_MAX; + pThis->aOperands[idxOp].pDataBuf = NULL; + + switch (pThis->aOperands[idxOp].fFlags & CIDET_OF_K_MASK) + { + case CIDET_OF_K_GPR: + if (!pThis->aOperands[idxOp].fIsHighByteRegister) + { + pThis->aOperands[idxOp].In.pv = &pThis->InCtx.aGRegs[pThis->aOperands[idxOp].iReg]; + pThis->aOperands[idxOp].Expected.pv = &pThis->ExpectedCtx.aGRegs[pThis->aOperands[idxOp].iReg]; + } + else + { + pThis->aOperands[idxOp].In.pv = &pThis->InCtx.aGRegs[pThis->aOperands[idxOp].iReg - 4]; + pThis->aOperands[idxOp].In.pu8++; + pThis->aOperands[idxOp].Expected.pv = &pThis->ExpectedCtx.aGRegs[pThis->aOperands[idxOp].iReg - 4]; + pThis->aOperands[idxOp].Expected.pu8++; + } + break; + + case CIDET_OF_K_IMM: + pThis->aOperands[idxOp].In.pv = NULL; + pThis->aOperands[idxOp].Expected.pv = NULL; + break; + + case CIDET_OF_K_SREG: + if (pThis->aOperands[idxOp].iReg < RT_ELEMENTS(pThis->InCtx.aSRegs)) + { + pThis->aOperands[idxOp].In.pv = &pThis->InCtx.aSRegs[pThis->aOperands[idxOp].iReg]; + pThis->aOperands[idxOp].Expected.pv = &pThis->ExpectedCtx.aSRegs[pThis->aOperands[idxOp].iReg]; + } + else + { + pThis->aOperands[idxOp].In.pv = NULL; + pThis->aOperands[idxOp].Expected.pv = NULL; + } + break; + + case CIDET_OF_K_CR: + case CIDET_OF_K_SSE: + case CIDET_OF_K_AVX: + case CIDET_OF_K_AVX512: + case CIDET_OF_K_FPU: + case CIDET_OF_K_MMX: + case CIDET_OF_K_AVXFUTURE: + case CIDET_OF_K_SPECIAL: + case CIDET_OF_K_TEST: + /** @todo Implement testing these registers. */ + case CIDET_OF_K_NONE: + default: + AssertReleaseFailedReturn(false); + } + } + } + AssertRelease((uintptr_t)pbBuf - (uintptr_t)&pThis->abBuf[0] <= sizeof(pThis->abBuf)); + + /* + * Call instruction specific setup function (for operand values and flags). + */ + int rc = pThis->pCurInstr->pfnSetupInOut(pThis, false /*fInvalid*/); + if (RT_FAILURE(rc)) + { + pThis->cSkippedSetupInOut++; + return false; + } + + /* + * Do the 2nd set of the memory operand preparations. + */ + if (pThis->fHasMemoryOperand) + { + idxOp = pThis->cOperands; + while (idxOp-- > 0) + if (pThis->aOperands[idxOp].fIsMem) + { + Assert(pThis->aOperands[idxOp].pDataBuf); + if (!pThis->pfnSetupDataBuf(pThis, pThis->aOperands[idxOp].pDataBuf, pThis->aOperands[idxOp].In.pv)) + { + pThis->cSkippedSetupDataBuf++; + return false; + } + + Assert( pThis->aOperands[idxOp].iMemBaseReg == UINT8_MAX + || pThis->InCtx.aGRegs[pThis->aOperands[idxOp].iMemBaseReg] == pThis->aOperands[idxOp].uMemBaseRegValue); + Assert( pThis->aOperands[idxOp].iMemIndexReg == UINT8_MAX + || ( !pThis->fUsesVexIndexRegs + ? pThis->InCtx.aGRegs[pThis->aOperands[idxOp].iMemIndexReg] + == pThis->aOperands[idxOp].uMemIndexRegValue + : false /** @todo VEX indexing */)); + } + } + + return true; +} + + +/** + * Figures the instruction length. + * + * This is a duplicate of CidetCoreAssemble() with the buffer updates removed. + * + * @returns true and pThis->cbInstr on success, false on failure. + * @param pThis The core state structure (for context). + */ +bool CidetCoreAssembleLength(PCIDETCORE pThis) +{ + uint8_t off = 0; + + /* + * Prefixes. + */ + if (1) + { + if (pThis->fAddrSizePrf) + off++; + if (pThis->fOpSizePrf) + off++; + } + else + { + /** @todo prefix list. */ + } + + /* + * Prefixes that must come right before the opcode. + */ + /** @todo VEX and EVEX. */ + if (pThis->fVex) + { + /** @todo VEX and EVEX. */ + } + else if (pThis->fEvex) + { + /** @todo VEX and EVEX. */ + } + else + { + if (pThis->fRexB || pThis->fRexX || pThis->fRexR || pThis->fRexW || pThis->fRex) + off++; + } + + /* + * The opcode. + */ + //uint8_t const *pbOpcode = pThis->pCurInstr->abOpcode; + switch (pThis->pCurInstr->cbOpcode) + { + case 3: off++; RT_FALL_THRU(); + case 2: off++; RT_FALL_THRU(); + case 1: off++; + break; + default: + AssertReleaseFailedReturn(false); + } + + /* + * Mod R/M + */ + if (pThis->fUsesModRm) + { + off++; + if (pThis->fSib) + off++; + if (pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands)) + { + //uint64_t uDispValue = pThis->aOperands[pThis->idxMrmRmOp].uImmDispValue; + switch (pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp) + { + case 0: break; + case 8: + case 7: + case 6: + case 5: + case 4: + case 3: + case 2: + case 1: + break; + default: AssertReleaseFailedReturn(false); + } + off += pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp; + } + } + + /* + * Immediates. + */ + uint8_t iOp = pThis->cOperands; + while (iOp-- > 0) + if ((pThis->aOperands[iOp].fFlags & CIDET_OF_K_MASK) == CIDET_OF_K_IMM) + { + //uint64_t uImmValue = pThis->aOperands[iOp].uImmDispValue; + switch (pThis->aOperands[iOp].cb) + { + case 8: + case 7: + case 6: + case 5: + case 4: + case 3: + case 2: + case 1: + break; + default: AssertReleaseFailedReturn(false); + } + off += pThis->aOperands[iOp].cb; + } + + pThis->cbInstr = off; + return true; +} + + +/** + * Assembles the instruction. + * + * This is a duplicate of CidetCoreAssembleLength() with buffer writes. + * + * @returns true and pThis->cbInstr and pThis->abInstr on success, false on + * failure. + * @param pThis The core state structure (for context). + */ +bool CidetCoreAssemble(PCIDETCORE pThis) +{ + uint8_t off = 0; + + /* + * Prefixes. + */ + if (1) + { + if (pThis->fAddrSizePrf) + pThis->abInstr[off++] = 0x67; + if (pThis->fOpSizePrf) + pThis->abInstr[off++] = 0x66; + } + else + { + /** @todo prefix list. */ + } + + /* + * Prefixes that must come right before the opcode. + */ + /** @todo VEX and EVEX. */ + if (pThis->fVex) + { + /** @todo VEX and EVEX. */ + } + else if (pThis->fEvex) + { + /** @todo VEX and EVEX. */ + } + else + { + if (pThis->fRexB || pThis->fRexX || pThis->fRexR || pThis->fRexW || pThis->fRex) + pThis->abInstr[off++] = 0x40 | (pThis->fRexB * 1) | (pThis->fRexX * 2) | (pThis->fRexR * 4) | (pThis->fRexW * 8); + } + + /* + * The opcode. + */ + uint8_t const *pbOpcode = pThis->pCurInstr->abOpcode; + switch (pThis->pCurInstr->cbOpcode) + { + case 3: pThis->abInstr[off++] = *pbOpcode++; RT_FALL_THRU(); + case 2: pThis->abInstr[off++] = *pbOpcode++; RT_FALL_THRU(); + case 1: pThis->abInstr[off++] = *pbOpcode++; + break; + default: + AssertReleaseFailedReturn(false); + } + + /* + * Mod R/M + */ + if (pThis->fUsesModRm) + { + pThis->abInstr[off++] = pThis->bModRm; + if (pThis->fSib) + pThis->abInstr[off++] = pThis->bSib; + if (pThis->idxMrmRmOp < RT_ELEMENTS(pThis->aOperands)) + { + uint64_t uDispValue = pThis->aOperands[pThis->idxMrmRmOp].uImmDispValue; + switch (pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp) + { + case 0: break; + case 8: pThis->abInstr[off + 3] = (uDispValue >> 56) & UINT8_C(0xff); RT_FALL_THRU(); + case 7: pThis->abInstr[off + 3] = (uDispValue >> 48) & UINT8_C(0xff); RT_FALL_THRU(); + case 6: pThis->abInstr[off + 3] = (uDispValue >> 40) & UINT8_C(0xff); RT_FALL_THRU(); + case 5: pThis->abInstr[off + 3] = (uDispValue >> 32) & UINT8_C(0xff); RT_FALL_THRU(); + case 4: pThis->abInstr[off + 3] = (uDispValue >> 24) & UINT8_C(0xff); RT_FALL_THRU(); + case 3: pThis->abInstr[off + 2] = (uDispValue >> 16) & UINT8_C(0xff); RT_FALL_THRU(); + case 2: pThis->abInstr[off + 1] = (uDispValue >> 8) & UINT8_C(0xff); RT_FALL_THRU(); + case 1: pThis->abInstr[off] = uDispValue & UINT8_C(0xff); + break; + default: AssertReleaseFailedReturn(false); + } + off += pThis->aOperands[pThis->idxMrmRmOp].cbMemDisp; + } + } + + /* + * Immediates. + */ + uint8_t iOp = pThis->cOperands; + while (iOp-- > 0) + if ((pThis->aOperands[iOp].fFlags & CIDET_OF_K_MASK) == CIDET_OF_K_IMM) + { + uint64_t uImmValue = pThis->aOperands[iOp].uImmDispValue; + switch (pThis->aOperands[iOp].cb) + { + case 8: pThis->abInstr[off + 3] = (uImmValue >> 56) & UINT8_C(0xff); RT_FALL_THRU(); + case 7: pThis->abInstr[off + 3] = (uImmValue >> 48) & UINT8_C(0xff); RT_FALL_THRU(); + case 6: pThis->abInstr[off + 3] = (uImmValue >> 40) & UINT8_C(0xff); RT_FALL_THRU(); + case 5: pThis->abInstr[off + 3] = (uImmValue >> 32) & UINT8_C(0xff); RT_FALL_THRU(); + case 4: pThis->abInstr[off + 3] = (uImmValue >> 24) & UINT8_C(0xff); RT_FALL_THRU(); + case 3: pThis->abInstr[off + 2] = (uImmValue >> 16) & UINT8_C(0xff); RT_FALL_THRU(); + case 2: pThis->abInstr[off + 1] = (uImmValue >> 8) & UINT8_C(0xff); RT_FALL_THRU(); + case 1: pThis->abInstr[off] = uImmValue & UINT8_C(0xff); + break; + default: AssertReleaseFailedReturn(false); + } + off += pThis->aOperands[iOp].cb; + } + + pThis->cbInstr = off; + return true; +} + + +bool CidetCoreReInitCodeBuf(PCIDETCORE pThis) +{ + /* + * Re-initialize the buffer. Requires instruction length and positioning. + */ + if (CidetCoreAssembleLength(pThis)) + { + pThis->CodeBuf.cb = pThis->cbInstr; + pThis->CodeBuf.off = CIDET_CODE_BUF_SIZE - PAGE_SIZE - pThis->cbInstr; + if (pThis->pfnReInitCodeBuf(pThis, &pThis->CodeBuf)) + { + pThis->CodeBuf.fActive = true; + + /* + * Update the RIP and CS values in the input and expected contexts. + */ + pThis->InCtx.rip = pThis->CodeBuf.uEffBufAddr + pThis->CodeBuf.offActive - pThis->CodeBuf.uSegBase; + pThis->ExpectedCtx.rip = pThis->InCtx.rip + pThis->cbInstr; /** @todo account for expected traps. */ + if (pThis->CodeBuf.uSeg != UINT32_MAX) + { + pThis->InCtx.aSRegs[X86_SREG_CS] = pThis->CodeBuf.uSeg; + pThis->ExpectedCtx.aSRegs[X86_SREG_CS] = pThis->CodeBuf.uSeg; + } + return true; + } + else + pThis->cSkippedReInitCodeBuf++; + } + else + pThis->cSkippedAssemble++; + return false; +} + + +#ifdef CIDET_DEBUG_DISAS +/** + * @callback_method_impl{FNDISREADBYTES} + */ +static DECLCALLBACK(int) cidetCoreDisReadBytes(PDISSTATE pDis, uint8_t offInstr, uint8_t cbMinRead, uint8_t cbMaxRead) +{ + PCIDETCORE pThis = (PCIDETCORE)pDis->pvUser; + memcpy(&pDis->abInstr[offInstr], &pThis->abInstr[offInstr], cbMaxRead); + pDis->cbCachedInstr = offInstr + cbMaxRead; + return VINF_SUCCESS; +} +#endif + + +bool CidetCoreSetupCodeBuf(PCIDETCORE pThis, unsigned iSubTest) +{ + if (CidetCoreAssemble(pThis)) + { + //CIDET_DPRINTF(("%04u: %.*Rhxs\n", i, pThis->cbInstr, pThis->abInstr)); +#ifdef CIDET_DEBUG_DISAS + DISCPUSTATE Dis; + char szInstr[80] = {0}; + uint32_t cbInstr; + int rcDis = DISInstrToStrEx(pThis->InCtx.rip, + CIDETMODE_IS_64BIT(pThis->bMode) ? DISCPUMODE_64BIT + : CIDETMODE_IS_32BIT(pThis->bMode) ? DISCPUMODE_32BIT : DISCPUMODE_16BIT, + cidetCoreDisReadBytes, + pThis, + DISOPTYPE_ALL, + &Dis, + &cbInstr, + szInstr, sizeof(szInstr)); + CIDET_DPRINTF(("%04u: %s", iSubTest, szInstr)); + Assert(cbInstr == pThis->cbInstr); +#else + RT_NOREF_PV(iSubTest); +#endif + if (pThis->pfnSetupCodeBuf(pThis, &pThis->CodeBuf, pThis->abInstr)) + { + return true; + } + pThis->cSkippedSetupCodeBuf++; + } + else + pThis->cSkippedAssemble++; + return false; +} + + +/** + * Compares the output with the output expectations. + * + * @returns true if ok, false if not (calls pfnFailure too). + * @param pThis The core state structure. + */ +bool CidetCoreCheckResults(PCIDETCORE pThis) +{ + if (memcmp(&pThis->ActualCtx, &pThis->ExpectedCtx, CIDETCPUCTX_COMPARE_SIZE) == 0) + return true; + + unsigned cDiffs = 0; +#define IF_FIELD_DIFFERS_SET_ERROR(a_Field, a_Fmt) \ + if (pThis->ActualCtx.a_Field != pThis->ExpectedCtx.a_Field) \ + { \ + CidetCoreSetError(pThis, #a_Field " differs: got %#llx expected %#llx", \ + pThis->ActualCtx.a_Field, pThis->ExpectedCtx.a_Field); \ + cDiffs++; \ + } else do { } while (0) + + IF_FIELD_DIFFERS_SET_ERROR(rip, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(rfl, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xAX], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xBX], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xCX], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xDX], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xSP], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xBP], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xSI], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_xDI], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x8], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x9], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x9], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x10], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x11], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x12], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x13], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x14], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aGRegs[X86_GREG_x15], "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(aSRegs[X86_SREG_CS], "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(aSRegs[X86_SREG_SS], "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(aSRegs[X86_SREG_DS], "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(aSRegs[X86_SREG_ES], "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(aSRegs[X86_SREG_FS], "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(aSRegs[X86_SREG_GS], "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(uXcpt, "%#04x"); + IF_FIELD_DIFFERS_SET_ERROR(uErr, "%#04llx"); + IF_FIELD_DIFFERS_SET_ERROR(cr2, "%#010llx"); +#ifndef CIDET_REDUCED_CTX + IF_FIELD_DIFFERS_SET_ERROR(tr, "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(ldtr, "%#06x"); + IF_FIELD_DIFFERS_SET_ERROR(cr0, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(cr3, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(cr4, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(cr8, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(dr0, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(dr1, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(dr2, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(dr3, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(dr6, "%#010llx"); + IF_FIELD_DIFFERS_SET_ERROR(dr7, "%#010llx"); +#endif + +AssertMsgFailed(("cDiffs=%d\n", cDiffs)); + Assert(cDiffs > 0); + return cDiffs == 0; +} + + +bool CidetCoreTest_Basic(PCIDETCORE pThis) +{ + /* + * Iterate all encodings. + */ + if (!CidetCoreSetupFirstBaseEncoding(pThis)) + return CidetCoreSetError(pThis, "CidetCoreSetupFirstBaseEncoding failed"); + unsigned cExecuted = 0; + unsigned cSkipped = 0; + do + { + /* + * Iterate data buffer configurations (one iteration if none). + */ + if (CidetCoreSetupFirstMemoryOperandConfig(pThis)) + { + do + { + /* + * Iterate code buffer configurations. + */ + if (!CidetCoreSetupFirstCodeBufferConfig(pThis)) + return CidetCoreSetError(pThis, "CidetCoreSetupFirstMemoryOperandConfig failed"); + do + { + /* + * Set up inputs and expected outputs, then emit the test code. + */ + pThis->InCtx = pThis->InTemplateCtx; + pThis->InCtx.fTrickyStack = pThis->fHasStackRegInMrmRmBase || pThis->fHasStackRegInMrmReg; + pThis->ExpectedCtx = pThis->InCtx; + if ( CidetCoreReInitCodeBuf(pThis) + && CidetCoreSetupInOut(pThis) + && CidetCoreSetupCodeBuf(pThis, cSkipped + cExecuted) + ) + { + if (pThis->pfnExecute(pThis)) + { + cExecuted++; + + /* + * Check the result against our expectations. + */ + CidetCoreCheckResults(pThis); + /** @todo check result. */ + + } + else + cSkipped++; + } + else + cSkipped++; + } while (CidetCoreSetupNextCodeBufferConfig(pThis)); + } while (CidetCoreSetupNextMemoryOperandConfig(pThis)); + } + else + cSkipped++; + } while (CidetCoreSetupNextBaseEncoding(pThis)); + + CIDET_DPRINTF(("CidetCoreTest_Basic: cExecuted=%u cSkipped=%u\n" + " cSkippedSetupInOut =%u\n" + " cSkippedReInitDataBuf =%u\n" + " cSkippedSetupDataBuf =%u\n" + " cSkippedDataBufWrtRip =%u\n" + " cSkippedAssemble =%u\n" + " cSkippedReInitCodeBuf =%u\n" + " cSkippedSetupCodeBuf =%u\n" + " cSkippedSameBaseIndexRemainder =%u\n" + " cSkippedOnlyIndexRemainder =%u\n" + " cSkippedDirectAddressingOverflow =%u\n" + , + cExecuted, cSkipped, + pThis->cSkippedSetupInOut, + pThis->cSkippedReInitDataBuf, + pThis->cSkippedSetupDataBuf, + pThis->cSkippedDataBufWrtRip, + pThis->cSkippedAssemble, + pThis->cSkippedReInitCodeBuf, + pThis->cSkippedSetupCodeBuf, + pThis->cSkippedSameBaseIndexRemainder, + pThis->cSkippedOnlyIndexRemainder, + pThis->cSkippedDirectAddressingOverflow + )); + + return true; +} + + +bool CidetCoreTestInstruction(PCIDETCORE pThis, PCCIDETINSTR pInstr) +{ + AssertReleaseMsgReturn(RT_VALID_PTR(pThis), ("%p\n", pThis), false); + AssertReleaseReturn(pThis->u32Magic == CIDETCORE_MAGIC, false); + AssertReleaseReturn(pThis->cCodeBufConfigs > 0, false); + + if (!CideCoreSetInstruction(pThis, pInstr)) + return CidetCoreSetError(pThis, "CideCoreSetInstruction failed"); + + bool fResult = CidetCoreTest_Basic(pThis); + + return fResult; +} + diff --git a/src/VBox/ValidationKit/utils/cpu/cidet-instr-1.cpp b/src/VBox/ValidationKit/utils/cpu/cidet-instr-1.cpp new file mode 100644 index 00000000..4cbbd5db --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cidet-instr-1.cpp @@ -0,0 +1,297 @@ +/* $Id: cidet-instr-1.cpp $ */ +/** @file + * CPU Instruction Decoding & Execution Tests - First bunch of instructions. + */ + +/* + * Copyright (C) 2014-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include "cidet.h" +#include <VBox/err.h> + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/* + * Shorter defines for the EFLAGS to save table space. + */ +#undef CF +#undef PF +#undef AF +#undef ZF +#undef SF +#undef OF + +#define CF X86_EFL_CF +#define PF X86_EFL_PF +#define AF X86_EFL_AF +#define ZF X86_EFL_ZF +#define SF X86_EFL_SF +#define OF X86_EFL_OF + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef struct CIDET2IN1OUTWITHFLAGSU8ENTRY +{ + uint8_t uIn1; + uint8_t uIn2; + uint16_t fEFlagsIn; + uint8_t uOut; + uint16_t fEFlagsOut; +} CIDET2IN1OUTWITHFLAGSU8ENTRY; +typedef CIDET2IN1OUTWITHFLAGSU8ENTRY const *PCCIDET2IN1OUTWITHFLAGSU8ENTRY; + +typedef struct CIDET2IN1OUTWITHFLAGSU16ENTRY +{ + uint16_t uIn1; + uint16_t uIn2; + uint16_t fEFlagsIn; + uint16_t uOut; + uint16_t fEFlagsOut; +} CIDET2IN1OUTWITHFLAGSU16ENTRY; +typedef CIDET2IN1OUTWITHFLAGSU16ENTRY const *PCCIDET2IN1OUTWITHFLAGSU16ENTRY; + +typedef struct CIDET2IN1OUTWITHFLAGSU32ENTRY +{ + uint32_t uIn1; + uint32_t uIn2; + uint16_t fEFlagsIn; + uint32_t uOut; + uint16_t fEFlagsOut; +} CIDET2IN1OUTWITHFLAGSU32ENTRY; +typedef CIDET2IN1OUTWITHFLAGSU32ENTRY const *PCCIDET2IN1OUTWITHFLAGSU32ENTRY; + +typedef struct CIDET2IN1OUTWITHFLAGSU64ENTRY +{ + uint64_t uIn1; + uint64_t uIn2; + uint16_t fEFlagsIn; + uint64_t uOut; + uint16_t fEFlagsOut; +} CIDET2IN1OUTWITHFLAGSU64ENTRY; +typedef CIDET2IN1OUTWITHFLAGSU64ENTRY const *PCCIDET2IN1OUTWITHFLAGSU64ENTRY; + +typedef struct CIDET2IN1OUTWITHFLAGS +{ + PCCIDET2IN1OUTWITHFLAGSU8ENTRY pa8Entries; + PCCIDET2IN1OUTWITHFLAGSU16ENTRY pa16Entries; + PCCIDET2IN1OUTWITHFLAGSU32ENTRY pa32Entries; + PCCIDET2IN1OUTWITHFLAGSU64ENTRY pa64Entries; + uint16_t c8Entries; + uint16_t c16Entries; + uint16_t c32Entries; + uint16_t c64Entries; + uint32_t fRelevantEFlags; +} CIDET2IN1OUTWITHFLAGS; + +#define CIDET2IN1OUTWITHFLAGS_INITIALIZER(a_fRelevantEFlags) \ + { \ + &s_a8Results[0], &s_a16Results[0], &s_a32Results[0], &s_a64Results[0], \ + RT_ELEMENTS(s_a8Results), RT_ELEMENTS(s_a16Results), RT_ELEMENTS(s_a32Results), RT_ELEMENTS(s_a64Results), \ + (a_fRelevantEFlags) \ + } + + +/** + * Generic worker for a FNCIDETSETUPINOUT function with two GPR/MEM registers, + * storing result in the first and flags. + * + * @returns See FNCIDETSETUPINOUT. + * @param pThis The core CIDET state structure. The InCtx + * and ExpectedCtx members will be modified. + * @param fInvalid When set, get the next invalid operands that will + * cause exceptions/faults. + * @param pResults The result collection. + */ +static int CidetGenericIn2Out1WithFlags(PCIDETCORE pThis, bool fInvalid, CIDET2IN1OUTWITHFLAGS const *pResults) +{ + int rc; + + Assert(pThis->idxMrmRegOp < 2); + Assert(pThis->idxMrmRmOp < 2); + Assert(pThis->idxMrmRmOp != pThis->idxMrmRegOp); + AssertCompile(RT_ELEMENTS(pThis->aiInOut) >= 4); + + if (!fInvalid) + { + if ( !pThis->fHasRegCollisionDirect + && !pThis->fHasRegCollisionMem) + { + pThis->InCtx.rfl &= ~(uint64_t)pResults->fRelevantEFlags; + pThis->ExpectedCtx.rfl &= ~(uint64_t)pResults->fRelevantEFlags; + switch (pThis->aOperands[0].cb) + { + case 1: + { + uint16_t idx = ++pThis->aiInOut[0] % pResults->c8Entries; + PCCIDET2IN1OUTWITHFLAGSU8ENTRY pEntry = &pResults->pa8Entries[idx]; + rc = idx ? VINF_SUCCESS : VINF_EOF; + + *pThis->aOperands[0].In.pu8 = pEntry->uIn1; + *pThis->aOperands[1].In.pu8 = pEntry->uIn2; + pThis->InCtx.rfl |= pEntry->fEFlagsIn; + + *pThis->aOperands[0].Expected.pu8 = pEntry->uOut; + *pThis->aOperands[1].Expected.pu8 = pEntry->uIn2; + pThis->ExpectedCtx.rfl |= pEntry->fEFlagsOut; + break; + } + + case 2: + { + uint16_t idx = ++pThis->aiInOut[1] % pResults->c16Entries; + PCCIDET2IN1OUTWITHFLAGSU16ENTRY pEntry = &pResults->pa16Entries[idx]; + rc = idx ? VINF_SUCCESS : VINF_EOF; + + *pThis->aOperands[0].In.pu16 = pEntry->uIn1; + *pThis->aOperands[1].In.pu16 = pEntry->uIn2; + pThis->InCtx.rfl |= pEntry->fEFlagsIn; + + *pThis->aOperands[0].Expected.pu16 = pEntry->uOut; + *pThis->aOperands[1].Expected.pu16 = pEntry->uIn2; + pThis->ExpectedCtx.rfl |= pEntry->fEFlagsOut; + break; + } + + case 4: + { + uint16_t idx = ++pThis->aiInOut[2] % pResults->c32Entries; + PCCIDET2IN1OUTWITHFLAGSU32ENTRY pEntry = &pResults->pa32Entries[idx]; + rc = idx ? VINF_SUCCESS : VINF_EOF; + + *pThis->aOperands[0].In.pu32 = pEntry->uIn1; + *pThis->aOperands[1].In.pu32 = pEntry->uIn2; + pThis->InCtx.rfl |= pEntry->fEFlagsIn; + + *pThis->aOperands[0].Expected.pu32 = pEntry->uOut; + if (!pThis->aOperands[0].fIsMem) + pThis->aOperands[0].Expected.pu32[1] = 0; + *pThis->aOperands[1].Expected.pu32 = pEntry->uIn2; + pThis->ExpectedCtx.rfl |= pEntry->fEFlagsOut; + break; + } + + case 8: + { + uint16_t idx = ++pThis->aiInOut[3] % pResults->c64Entries; + PCCIDET2IN1OUTWITHFLAGSU64ENTRY pEntry = &pResults->pa64Entries[idx]; + rc = idx ? VINF_SUCCESS : VINF_EOF; + + *pThis->aOperands[0].In.pu64 = pEntry->uIn1; + *pThis->aOperands[1].In.pu64 = pEntry->uIn2; + pThis->InCtx.rfl |= pEntry->fEFlagsIn; + + *pThis->aOperands[0].Expected.pu64 = pEntry->uOut; + *pThis->aOperands[1].Expected.pu64 = pEntry->uIn2; + pThis->ExpectedCtx.rfl |= pEntry->fEFlagsOut; + break; + } + + default: + AssertFailed(); + rc = VERR_INTERNAL_ERROR_3; + } + } + else + rc = VERR_NOT_SUPPORTED; + } + else + rc = VERR_NO_DATA; + return rc; +} + + +static DECLCALLBACK(int) cidetInOutAdd(PCIDETCORE pThis, bool fInvalid) +{ + static const CIDET2IN1OUTWITHFLAGSU8ENTRY s_a8Results[] = + { + { UINT8_C(0x00), UINT8_C(0x00), 0, UINT8_C(0x00), ZF | PF }, + { UINT8_C(0xff), UINT8_C(0x01), 0, UINT8_C(0x00), CF | ZF | AF | PF }, + { UINT8_C(0x7f), UINT8_C(0x80), 0, UINT8_C(0xff), SF | PF }, + { UINT8_C(0x01), UINT8_C(0x01), 0, UINT8_C(0x02), 0 }, + }; + static const CIDET2IN1OUTWITHFLAGSU16ENTRY s_a16Results[] = + { + { UINT16_C(0x0000), UINT16_C(0x0000), 0, UINT16_C(0x0000), ZF | PF }, + { UINT16_C(0xfefd), UINT16_C(0x0103), 0, UINT16_C(0x0000), CF | ZF | AF | PF }, + { UINT16_C(0x8e7d), UINT16_C(0x7182), 0, UINT16_C(0xffff), SF | PF }, + { UINT16_C(0x0001), UINT16_C(0x0001), 0, UINT16_C(0x0002), 0 }, + }; + static const CIDET2IN1OUTWITHFLAGSU32ENTRY s_a32Results[] = + { + { UINT32_C(0x00000000), UINT32_C(0x00000000), 0, UINT32_C(0x00000000), ZF | PF }, + { UINT32_C(0xfefdfcfb), UINT32_C(0x01020305), 0, UINT32_C(0x00000000), CF | ZF | AF | PF }, + { UINT32_C(0x8efdfcfb), UINT32_C(0x71020304), 0, UINT32_C(0xffffffff), SF | PF }, + { UINT32_C(0x00000001), UINT32_C(0x00000001), 0, UINT32_C(0x00000002), 0 }, + }; + static const CIDET2IN1OUTWITHFLAGSU64ENTRY s_a64Results[] = + { + { UINT64_C(0x0000000000000000), UINT64_C(0x0000000000000000), 0, UINT64_C(0x0000000000000000), ZF | PF }, + { UINT64_C(0xfefdfcfbfaf9f8f7), UINT64_C(0x0102030405060709), 0, UINT64_C(0x0000000000000000), CF | ZF | AF | PF }, + { UINT64_C(0x7efdfcfbfaf9f8f7), UINT64_C(0x8102030405060708), 0, UINT64_C(0xffffffffffffffff), SF | PF }, + { UINT64_C(0x0000000000000001), UINT64_C(0x0000000000000001), 0, UINT64_C(0x0000000000000002), 0 }, + }; + static const CIDET2IN1OUTWITHFLAGS s_Results = CIDET2IN1OUTWITHFLAGS_INITIALIZER(CF | PF | AF | SF | OF); + return CidetGenericIn2Out1WithFlags(pThis, fInvalid, &s_Results); +} + + +/** First bunch of instructions. */ +const CIDETINSTR g_aCidetInstructions1[] = +{ +#if 1 + { + "add Eb,Gb", cidetInOutAdd, 1, {0x00, 0, 0}, 0, 2, + { CIDET_OF_K_GPR | CIDET_OF_Z_BYTE | CIDET_OF_M_RM | CIDET_OF_A_RW, + CIDET_OF_K_GPR | CIDET_OF_Z_BYTE | CIDET_OF_M_REG | CIDET_OF_A_R, + 0, 0 }, CIDET_IF_MODRM + }, +#endif +#if 1 + { + "add Ev,Gv", cidetInOutAdd, 1, {0x01, 0, 0}, 0, 2, + { CIDET_OF_K_GPR | CIDET_OF_Z_VAR_WDQ | CIDET_OF_M_RM | CIDET_OF_A_RW, + CIDET_OF_K_GPR | CIDET_OF_Z_VAR_WDQ | CIDET_OF_M_REG | CIDET_OF_A_R, + 0, 0 }, CIDET_IF_MODRM + }, +#endif +}; +/** Number of instruction in the g_aInstructions1 array. */ +const uint32_t g_cCidetInstructions1 = RT_ELEMENTS(g_aCidetInstructions1); + diff --git a/src/VBox/ValidationKit/utils/cpu/cidet.h b/src/VBox/ValidationKit/utils/cpu/cidet.h new file mode 100644 index 00000000..2273b7b0 --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cidet.h @@ -0,0 +1,1092 @@ +/* $Id: cidet.h $ */ +/** @file + * CPU Instruction Decoding & Execution Tests - C/C++ Header. + */ + +/* + * Copyright (C) 2014-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_cpu_cidet_h +#define VBOX_INCLUDED_SRC_cpu_cidet_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/types.h> +#include <iprt/x86.h> + + +/** @name CIDET - Operand flags. + * @{ */ +#define CIDET_OF_FIXED_MASK UINT32_C(0x0000001f) /**< Fixed register/whatever mask. */ + +#define CIDET_OF_Z_SHIFT 8 /**< Size shift. */ +#define CIDET_OF_Z_MASK UINT32_C(0x00000f00) /**< Size mask. */ +#define CIDET_OF_Z_NONE UINT32_C(0x00000000) /**< Unused zero value. */ +#define CIDET_OF_Z_BYTE UINT32_C(0x00000100) /**< Byte size. */ +#define CIDET_OF_Z_WORD UINT32_C(0x00000200) /**< Word (2 bytes) size. */ +#define CIDET_OF_Z_DWORD UINT32_C(0x00000300) /**< Double word (4 bytes) size. */ +#define CIDET_OF_Z_QWORD UINT32_C(0x00000400) /**< Quad word (8 bytes) size. */ +#define CIDET_OF_Z_TBYTE UINT32_C(0x00000500) /**< Ten byte (10 bytes) size - aka TWORD. */ +#define CIDET_OF_Z_OWORD UINT32_C(0x00000600) /**< Octa word (16 bytes) size - aka DQWORD. */ +#define CIDET_OF_Z_YWORD UINT32_C(0x00000700) /**< Yxx sized, i.e. 32 bytes. */ +#define CIDET_OF_Z_ZWORD UINT32_C(0x00000800) /**< Zxx sized, i.e. 64 bytes. */ +#define CIDET_OF_Z_VAR_WDQ UINT32_C(0x00000900) /**< Variable size depending on size prefix (2, 4, or 8 bytes). */ +#define CIDET_OF_Z_SPECIAL UINT32_C(0x00000f00) /**< Special size, see instruction flags or smth. */ + +#define CIDET_OF_K_MASK UINT32_C(0x0000f000) /**< Kind of operand. */ +#define CIDET_OF_K_NONE UINT32_C(0x00000000) /**< Unused zero value. */ +#define CIDET_OF_K_GPR UINT32_C(0x00001000) /**< General purpose register. Includes memory when used with CIDET_OF_M_RM. */ +#define CIDET_OF_K_SREG UINT32_C(0x00002000) /**< Segment register. */ +#define CIDET_OF_K_CR UINT32_C(0x00003000) /**< Control register. */ +#define CIDET_OF_K_SSE UINT32_C(0x00004000) /**< SSE register. */ +#define CIDET_OF_K_AVX UINT32_C(0x00005000) /**< AVX register. */ +#define CIDET_OF_K_AVX512 UINT32_C(0x00006000) /**< AVX-512 register. */ +#define CIDET_OF_K_AVXFUTURE UINT32_C(0x00007000) /**< Reserved for future AVX register set. */ +#define CIDET_OF_K_VRX_TST_MASK UINT32_C(0x0000c000) /**< Used for testing for VRX register kind, see CIDET_OF_K_IS_VRX. */ +#define CIDET_OF_K_VRX_TST_RES UINT32_C(0x00004000) /**< Used for testing for VRX register kind, see CIDET_OF_K_IS_VRX. */ +#define CIDET_OF_K_FPU UINT32_C(0x00008000) /**< FPU register. */ +#define CIDET_OF_K_MMX UINT32_C(0x00009000) /**< MMX register. */ +#define CIDET_OF_K_TEST UINT32_C(0x0000a000) /**< Test register. */ +#define CIDET_OF_K_IMM UINT32_C(0x0000d000) /**< Immediate. */ +#define CIDET_OF_K_MEM UINT32_C(0x0000e000) /**< Memory. */ +#define CIDET_OF_K_SPECIAL UINT32_C(0x0000f000) /**< Special. */ +/** Check if @a a_fOp is a general purpose register. */ +#define CIDET_OF_K_IS_GPR(a_fOp) ( ((a_fOp) & CIDET_OF_K_MASK) == CIDET_OF_K_GPR ) +/** Check if @a a_fOp is a XMM (SSE), YMM (AVX), ZMM (AVX-512) or similar register. */ +#define CIDET_OF_K_IS_VRX(a_fOp) ( ((a_fOp) & CIDET_OF_K_VRX_TST_MASK) == CIDET_OF_K_VRX_TST_RES ) +/** Check if @a a_fOp1 and @a a_fOp2 specify the same kind of register, + * treating SSE, AVX, AVX-512 and AVX-future as the same kind and ignoring the + * special register kind. */ +#define CIDET_OF_K_IS_SAME(a_fOp1, a_fOp2) \ + ( ((a_fOp1) & CIDET_OF_K_MASK) == ((a_fOp2) & CIDET_OF_K_MASK) \ + ? ((a_fOp1) & CIDET_OF_K_MASK) != CIDET_OF_K_SPECIAL \ + : (CIDET_OF_K_IS_VRX(a_fOp1) && CIDET_OF_K_IS_VRX(a_fOp2)) ) + +#define CIDET_OF_M_RM_ONLY_R UINT32_C(0x00010000) +#define CIDET_OF_M_RM_ONLY_M UINT32_C(0x00020000) +#define CIDET_OF_M_RM (CIDET_OF_M_RM_ONLY_R | CIDET_OF_M_RM_ONLY_M) +#define CIDET_OF_M_REG UINT32_C(0x00040000) + +#define CIDET_OF_A_R UINT32_C(0x00080000) /**< Read access. */ +#define CIDET_OF_A_W UINT32_C(0x00100000) /**< Write access. */ +#define CIDET_OF_A_RW UINT32_C(0x00180000) /**< Read & write access. */ + +/** The operand defaults to 64-bit width in 64-bit mode, making 32-bit width + * inaccessible. */ +#define CIDET_OF_DEFAULT_64BIT UINT32_C(0x40000000) +/** Operand always uses the ES segment for memory accesses. */ +#define CIDET_OF_ALWAYS_SEG_ES UINT32_C(0x80000000) +/** @} */ + + +/** @name CIDET - Instruction flags. + * @{ */ +#define CIDET_IF_MODRM RT_BIT_64(0) /**< ModR/M encoded. */ +#define CIDET_IF_PRIVILEGED RT_BIT_64(1) /**< Privileged. */ +/** @} */ + + +/** + * Callback function for setting up the input and expected output CPU contexts. + * + * @returns VBox status code. + * @retval VINF_EOF when static test data wraps (first entry is returned). + * @retval VERR_NO_DATA if @a fInvalid is set and there are no invalid operand + * values for this instruction. + * @retval VERR_NOT_SUPPORTED if something in the setup prevents us from + * comming up with working set of inputs and outputs. + * + * @param pThis The core CIDET state structure. The InCtx + * and ExpectedCtx members will be modified. + * @param fInvalid When set, get the next invalid operands that will + * cause exceptions/faults. + */ +typedef DECLCALLBACKTYPE(int, FNCIDETSETUPINOUT,(struct CIDETCORE *pThis, bool fInvalid)); +/** Pointer to a FNCIDETSETUPINOUT function. */ +typedef FNCIDETSETUPINOUT *PFNCIDETSETUPINOUT; + + +/** + * Instruction test descriptor. + */ +typedef struct CIDETINSTR +{ + /** The mnemonic (kind of). */ + const char *pszMnemonic; + /** Setup input and outputs. */ + PFNCIDETSETUPINOUT pfnSetupInOut; + /** Number of opcode bytes. */ + uint8_t cbOpcode; + /** Opcode byte(s). */ + uint8_t abOpcode[3]; + /** Mandatory prefix (zero if not applicable). */ + uint8_t bMandatoryPrefix; + /** Number of operands. */ + uint8_t cOperands; + /** Operand flags. */ + uint32_t afOperands[4]; + /** Flags. */ + uint64_t fFlags; +} CIDETINSTR; +/** Pointer to an instruction test descriptor. */ +typedef CIDETINSTR const *PCCIDETINSTR; + + +/** + * CPU Context with a few extra bits for expectations and results. + */ +typedef struct CIDETCPUCTX +{ + uint64_t rip; + uint64_t rfl; + uint64_t aGRegs[16]; + uint16_t aSRegs[6]; + +#ifndef CIDET_REDUCED_CTX + uint16_t tr; + uint16_t ldtr; + uint64_t cr0; +#else + uint16_t au16Padding[2]; +#endif + uint64_t cr2; +#ifndef CIDET_REDUCED_CTX + uint64_t cr3; + uint64_t cr4; + uint64_t cr8; + uint64_t dr0; + uint64_t dr1; + uint64_t dr2; + uint64_t dr3; + uint64_t dr6; + uint64_t dr7; +#endif + + uint64_t uErr; /**< Exception error code. UINT64_MAX if not applicable. (Not for input context.) */ + uint32_t uXcpt; /**< Exception number. UINT32_MAX if no exception. (Not for input context.) */ + + uint32_t fIgnoredRFlags; /**< Only for expected result. */ + bool fTrickyStack; /**< Set if the stack might be bad. May come at the cost of accurate flags (32-bit). */ +} CIDETCPUCTX; +typedef CIDETCPUCTX *PCIDETCPUCTX; +typedef CIDETCPUCTX const *PCCIDETCPUCTX; + +/** Number of bytes of CIDETCPUCTX that can be compared quickly using memcmp. + * Anything following these bytes are not relevant to the compare. */ +#define CIDETCPUCTX_COMPARE_SIZE RT_UOFFSETOF(CIDETCPUCTX, fIgnoredRFlags) + + +/** @name CPU mode + bits + environment. + * @{ */ +#define CIDETMODE_BIT_MASK UINT8_C(0x0e) /**< The instruction bit count. Results in byte size when masked. */ +#define CIDETMODE_BIT_16 UINT8_C(0x02) /**< 16-bit instructions. */ +#define CIDETMODE_BIT_32 UINT8_C(0x04) /**< 32-bit instructions. */ +#define CIDETMODE_BIT_64 UINT8_C(0x08) /**< 64-bit instructions. */ +#define CIDETMODE_MODE_MASK UINT8_C(0x70) /**< CPU mode mask. */ +#define CIDETMODE_MODE_RM UINT8_C(0x00) /**< Real mode. */ +#define CIDETMODE_MODE_PE UINT8_C(0x10) /**< Protected mode without paging. */ +#define CIDETMODE_MODE_PP UINT8_C(0x20) /**< Paged protected mode. */ +#define CIDETMODE_MODE_PAE UINT8_C(0x30) /**< PAE protected mode (paged). */ +#define CIDETMODE_MODE_LM UINT8_C(0x40) /**< Long mode (paged). */ +#define CIDETMODE_ENV_MASK UINT8_C(0x81) /**< Execution environment. */ +#define CIDETMODE_ENV_NORMAL UINT8_C(0x01) /**< Normal environment. */ +#define CIDETMODE_ENV_V86 UINT8_C(0x80) /**< V8086 environment. */ +#define CIDETMODE_RM (CIDETMODE_MODE_RM | CIDETMODE_BIT_16 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_PE_16 (CIDETMODE_MODE_PE | CIDETMODE_BIT_16 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_PE_32 (CIDETMODE_MODE_PE | CIDETMODE_BIT_32 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_PE_V86 (CIDETMODE_MODE_PE | CIDETMODE_BIT_16 | CIDETMODE_ENV_V86) +#define CIDETMODE_PP_16 (CIDETMODE_MODE_PP | CIDETMODE_BIT_16 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_PP_32 (CIDETMODE_MODE_PP | CIDETMODE_BIT_32 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_PP_V86 (CIDETMODE_MODE_PP | CIDETMODE_BIT_16 | CIDETMODE_ENV_V86) +#define CIDETMODE_PAE_16 (CIDETMODE_MODE_PAE | CIDETMODE_BIT_16 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_PAE_32 (CIDETMODE_MODE_PAE | CIDETMODE_BIT_32 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_PAE_V86 (CIDETMODE_MODE_PAE | CIDETMODE_BIT_16 | CIDETMODE_ENV_V86) +#define CIDETMODE_LM_16 (CIDETMODE_MODE_LM | CIDETMODE_BIT_16 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_LM_32 (CIDETMODE_MODE_LM | CIDETMODE_BIT_32 | CIDETMODE_ENV_NORMAL) +#define CIDETMODE_LM_64 (CIDETMODE_MODE_LM | CIDETMODE_BIT_64 | CIDETMODE_ENV_NORMAL) +/** Test if @a a_bMode is a 16-bit mode. */ +#define CIDETMODE_IS_16BIT(a_bMode) ( ((a_bMode) & CIDETMODE_BIT_MASK) == CIDETMODE_BIT_16 ) +/** Test if @a a_bMode is a 32-bit mode. */ +#define CIDETMODE_IS_32BIT(a_bMode) ( ((a_bMode) & CIDETMODE_BIT_MASK) == CIDETMODE_BIT_32 ) +/** Test if @a a_bMode is a 64-bit mode. */ +#define CIDETMODE_IS_64BIT(a_bMode) ( ((a_bMode) & CIDETMODE_BIT_MASK) == CIDETMODE_BIT_64 ) +/** Get the instruction bit count. */ +#define CIDETMODE_GET_BIT_COUNT(a_bMode) ( CIDETMODE_GET_BYTE_COUNT(a_bMode) * 8 ) +/** Get the instruction byte count. */ +#define CIDETMODE_GET_BYTE_COUNT(a_bMode) ( (a_bMode) & CIDETMODE_BIT_MASK ) +/** Test if @a a_bMode long mode. */ +#define CIDETMODE_IS_LM(a_bMode) ( ((a_bMode) & CIDETMODE_MODE_MASK) == CIDETMODE_MODE_LM ) +/** Test if @a a_bMode some kind of protected mode. */ +#define CIDETMODE_IS_PROT(a_bMode) ( ((a_bMode) & CIDETMODE_MODE_MASK) >= CIDETMODE_MODE_PE ) + +/** @} */ + + +/** @name Test Configuration Flags. + * @{ */ +#define CIDET_TESTCFG_SEG_PRF_CS UINT64_C(0x0000000000000001) +#define CIDET_TESTCFG_SEG_PRF_SS UINT64_C(0x0000000000000002) +#define CIDET_TESTCFG_SEG_PRF_DS UINT64_C(0x0000000000000004) +#define CIDET_TESTCFG_SEG_PRF_ES UINT64_C(0x0000000000000008) +#define CIDET_TESTCFG_SEG_PRF_FS UINT64_C(0x0000000000000010) +#define CIDET_TESTCFG_SEG_PRF_GS UINT64_C(0x0000000000000020) +#define CIDET_TESTCFG_SEG_PRF_MASK UINT64_C(0x000000000000003f) +/** @} */ + +/** */ +typedef enum CIDETREG +{ + kCidetReg_Gpr_Invalid = 0, + + kCidetReg_Gpr_al, + kCidetReg_Gpr_cl, + kCidetReg_Gpr_dl, + kCidetReg_Gpr_bl, + kCidetReg_Gpr_spl, + kCidetReg_Gpr_bpl, + kCidetReg_Gpr_sil, + kCidetReg_Gpr_dil, + kCidetReg_Gpr_r8b, + kCidetReg_Gpr_r9b, + kCidetReg_Gpr_r10b, + kCidetReg_Gpr_r11b, + kCidetReg_Gpr_r12b, + kCidetReg_Gpr_r13b, + kCidetReg_Gpr_r14b, + kCidetReg_Gpr_r15b, + kCidetReg_Gpr_ah, + kCidetReg_Gpr_ch, + kCidetReg_Gpr_dh, + kCidetReg_Gpr_bh, +#define kCidetReg_Gpr_Byte_First kCidetReg_Gpr_al +#define kCidetReg_Gpr_Byte_First_Upper kCidetReg_Gpr_ah +#define kCidetReg_Gpr_Byte_Last kCidetReg_Gpr_bh + + kCidetReg_Gpr_ax, + kCidetReg_Gpr_cx, + kCidetReg_Gpr_dx, + kCidetReg_Gpr_bx, + kCidetReg_Gpr_sp, + kCidetReg_Gpr_bp, + kCidetReg_Gpr_si, + kCidetReg_Gpr_di, + kCidetReg_Gpr_r8w, + kCidetReg_Gpr_r9w, + kCidetReg_Gpr_r10w, + kCidetReg_Gpr_r11w, + kCidetReg_Gpr_r12w, + kCidetReg_Gpr_r13w, + kCidetReg_Gpr_r14w, + kCidetReg_Gpr_r15w, +#define kCidetReg_Gpr_Word_First kCidetReg_Gpr_ax +#define kCidetReg_Gpr_Word_Last kCidetReg_Gpr_r15w + + kCidetReg_Gpr_eax, + kCidetReg_Gpr_ecx, + kCidetReg_Gpr_edx, + kCidetReg_Gpr_ebx, + kCidetReg_Gpr_esp, + kCidetReg_Gpr_ebp, + kCidetReg_Gpr_esi, + kCidetReg_Gpr_edi, + kCidetReg_Gpr_r8d, + kCidetReg_Gpr_r9d, + kCidetReg_Gpr_r10d, + kCidetReg_Gpr_r11d, + kCidetReg_Gpr_r12d, + kCidetReg_Gpr_r13d, + kCidetReg_Gpr_r14d, + kCidetReg_Gpr_r15d, +#define kCidetReg_Gpr_DWord_First kCidetReg_Gpr_eax +#define kCidetReg_Gpr_DWord_Last kCidetReg_Gpr_r15d + + kCidetReg_Gpr_rax, + kCidetReg_Gpr_rcx, + kCidetReg_Gpr_rdx, + kCidetReg_Gpr_rbx, + kCidetReg_Gpr_rsp, + kCidetReg_Gpr_rbp, + kCidetReg_Gpr_rsi, + kCidetReg_Gpr_rdi, + kCidetReg_Gpr_r8, + kCidetReg_Gpr_r9, + kCidetReg_Gpr_r10, + kCidetReg_Gpr_r11, + kCidetReg_Gpr_r12, + kCidetReg_Gpr_r13, + kCidetReg_Gpr_r14, + kCidetReg_Gpr_r15, +#define kCidetReg_Gpr_QWord_First kCidetReg_Gpr_rax +#define kCidetReg_Gpr_QWord_Last kCidetReg_Gpr_r15 + + kCidetReg_Seg_es, + kCidetReg_Seg_cs, + kCidetReg_Seg_ss, + kCidetReg_Seg_ds, + kCidetReg_Seg_fs, + kCidetReg_Seg_gs, + kCidetReg_Seg_Inv6, + kCidetReg_Seg_Inv7, +#define kCidetReg_Seg_First kCidetReg_Seg_es +#define kCidetReg_Seg_Last kCidetReg_Seg_gs +#define kCidetReg_Seg_Last_Inv kCidetReg_Seg_Inv7 + + kCidetReg_Misc_ip, + kCidetReg_Misc_eip, + kCidetReg_Misc_rip, + kCidetReg_Misc_flags, + kCidetReg_Misc_eflags, + kCidetReg_Misc_rflags, + kCidetReg_Misc_tr, + kCidetReg_Misc_ldtr, + kCidetReg_Misc_gdtr, + kCidetReg_Misc_idtr, + + kCidetReg_Ctrl_cr0, + kCidetReg_Ctrl_cr1, + kCidetReg_Ctrl_cr2, + kCidetReg_Ctrl_cr3, + kCidetReg_Ctrl_cr4, + kCidetReg_Ctrl_cr5, + kCidetReg_Ctrl_cr6, + kCidetReg_Ctrl_cr7, + kCidetReg_Ctrl_cr8, + kCidetReg_Ctrl_cr9, + kCidetReg_Ctrl_cr10, + kCidetReg_Ctrl_cr11, + kCidetReg_Ctrl_cr12, + kCidetReg_Ctrl_cr13, + kCidetReg_Ctrl_cr14, + kCidetReg_Ctrl_cr15, +#define kCidetReg_Ctrl_First kCidetReg_Ctrl_cr0 +#define kCidetReg_Ctrl_Last kCidetReg_Ctrl_cr15 +#define CIDETREG_CTRL_IS_VALID(a_iReg) ( (a_iReg) == kCidetReg_Ctrl_cr0 \ + && (a_iReg) == kCidetReg_Ctrl_cr2 \ + && (a_iReg) == kCidetReg_Ctrl_cr3 \ + && (a_iReg) == kCidetReg_Ctrl_cr4 \ + && (a_iReg) == kCidetReg_Ctrl_cr8 ) + + kCidetReg_Dbg_dr0, + kCidetReg_Dbg_dr1, + kCidetReg_Dbg_dr2, + kCidetReg_Dbg_dr3, + kCidetReg_Dbg_dr4, + kCidetReg_Dbg_dr5, + kCidetReg_Dbg_dr6, + kCidetReg_Dbg_dr7, + kCidetReg_Dbg_dr8, + kCidetReg_Dbg_dr9, + kCidetReg_Dbg_dr10, + kCidetReg_Dbg_dr11, + kCidetReg_Dbg_dr12, + kCidetReg_Dbg_dr13, + kCidetReg_Dbg_dr14, + kCidetReg_Dbg_dr15, +#define kCidetReg_Dbg_First kCidetReg_Dbg_dr0 +#define kCidetReg_Dbg_Last kCidetReg_Dbg_dr15 +#define CIDETREG_DBG_IS_VALID(a_iReg) ((a_iReg) < kCidetReg_Dbg_dr8 && (a_iReg) >= kCidetReg_Dbg_First) + + kCidetReg_Test_tr0, + kCidetReg_Test_tr1, + kCidetReg_Test_tr2, + kCidetReg_Test_tr3, + kCidetReg_Test_tr4, + kCidetReg_Test_tr5, + kCidetReg_Test_tr6, + kCidetReg_Test_tr7, + kCidetReg_Test_tr8, + kCidetReg_Test_tr9, + kCidetReg_Test_tr10, + kCidetReg_Test_tr11, + kCidetReg_Test_tr12, + kCidetReg_Test_tr13, + kCidetReg_Test_tr14, + kCidetReg_Test_tr15, +#define kCidetReg_Test_First kCidetReg_Test_tr0 +#define kCidetReg_Test_Last kCidetReg_Test_tr15 + + kCidetReg_Fpu_st0, + kCidetReg_Fpu_st1, + kCidetReg_Fpu_st2, + kCidetReg_Fpu_st3, + kCidetReg_Fpu_st4, + kCidetReg_Fpu_st5, + kCidetReg_Fpu_st6, + kCidetReg_Fpu_st7, +#define kCidetReg_Fpu_First kCidetReg_Mmx_st0 +#define kCidetReg_Fpu_Last kCidetReg_Mmx_st7 + + kCidetReg_FpuMisc_cs, + kCidetReg_FpuMisc_ip, + kCidetReg_FpuMisc_ds, + kCidetReg_FpuMisc_dp, + kCidetReg_FpuMisc_fop, + kCidetReg_FpuMisc_ftw, + kCidetReg_FpuMisc_fsw, + kCidetReg_FpuMisc_fcw, + kCidetReg_FpuMisc_mxcsr_mask, + kCidetReg_FpuMisc_mxcsr, + + kCidetReg_Mmx_mm0, + kCidetReg_Mmx_mm1, + kCidetReg_Mmx_mm2, + kCidetReg_Mmx_mm3, + kCidetReg_Mmx_mm4, + kCidetReg_Mmx_mm5, + kCidetReg_Mmx_mm6, + kCidetReg_Mmx_mm7, +#define kCidetReg_Mmx_First kCidetReg_Mmx_mm0 +#define kCidetReg_Mmx_Last kCidetReg_Mmx_mm7 + + kCidetReg_Sse_xmm0, + kCidetReg_Sse_xmm1, + kCidetReg_Sse_xmm2, + kCidetReg_Sse_xmm3, + kCidetReg_Sse_xmm4, + kCidetReg_Sse_xmm5, + kCidetReg_Sse_xmm6, + kCidetReg_Sse_xmm7, + kCidetReg_Sse_xmm8, + kCidetReg_Sse_xmm9, + kCidetReg_Sse_xmm10, + kCidetReg_Sse_xmm11, + kCidetReg_Sse_xmm12, + kCidetReg_Sse_xmm13, + kCidetReg_Sse_xmm14, + kCidetReg_Sse_xmm15, + kCidetReg_Sse_xmm16, + kCidetReg_Sse_xmm17, + kCidetReg_Sse_xmm18, + kCidetReg_Sse_xmm19, + kCidetReg_Sse_xmm20, + kCidetReg_Sse_xmm21, + kCidetReg_Sse_xmm22, + kCidetReg_Sse_xmm23, + kCidetReg_Sse_xmm24, + kCidetReg_Sse_xmm25, + kCidetReg_Sse_xmm26, + kCidetReg_Sse_xmm27, + kCidetReg_Sse_xmm28, + kCidetReg_Sse_xmm29, + kCidetReg_Sse_xmm30, + kCidetReg_Sse_xmm31, +#define kCidetReg_Sse_First kCidetReg_Mmx_Xmm0 +#define kCidetReg_Sse_Last kCidetReg_Mmx_Xmm15 +#define kCidetReg_Sse_Last_Avx512 kCidetReg_Mmx_Xmm31 + + kCidetReg_Avx_Ymm0, + kCidetReg_Avx_Ymm1, + kCidetReg_Avx_Ymm2, + kCidetReg_Avx_Ymm3, + kCidetReg_Avx_Ymm4, + kCidetReg_Avx_Ymm5, + kCidetReg_Avx_Ymm6, + kCidetReg_Avx_Ymm7, + kCidetReg_Avx_Ymm8, + kCidetReg_Avx_Ymm9, + kCidetReg_Avx_Ymm10, + kCidetReg_Avx_Ymm11, + kCidetReg_Avx_Ymm12, + kCidetReg_Avx_Ymm13, + kCidetReg_Avx_Ymm14, + kCidetReg_Avx_Ymm15, + kCidetReg_Avx_Ymm16, + kCidetReg_Avx_Ymm17, + kCidetReg_Avx_Ymm18, + kCidetReg_Avx_Ymm19, + kCidetReg_Avx_Ymm20, + kCidetReg_Avx_Ymm21, + kCidetReg_Avx_Ymm22, + kCidetReg_Avx_Ymm23, + kCidetReg_Avx_Ymm24, + kCidetReg_Avx_Ymm25, + kCidetReg_Avx_Ymm26, + kCidetReg_Avx_Ymm27, + kCidetReg_Avx_Ymm28, + kCidetReg_Avx_Ymm29, + kCidetReg_Avx_Ymm30, + kCidetReg_Avx_Ymm31, +#define kCidetReg_Avx_First kCidetReg_Avx_Ymm0 +#define kCidetReg_Avx_Last kCidetReg_Avx_Ymm15 +#define kCidetReg_Avx_Last_Avx512 kCidetReg_Avx_Ymm31 + + kCidetReg_Avx512_Zmm0, + kCidetReg_Avx512_Zmm1, + kCidetReg_Avx512_Zmm2, + kCidetReg_Avx512_Zmm3, + kCidetReg_Avx512_Zmm4, + kCidetReg_Avx512_Zmm5, + kCidetReg_Avx512_Zmm6, + kCidetReg_Avx512_Zmm7, + kCidetReg_Avx512_Zmm8, + kCidetReg_Avx512_Zmm9, + kCidetReg_Avx512_Zmm10, + kCidetReg_Avx512_Zmm11, + kCidetReg_Avx512_Zmm12, + kCidetReg_Avx512_Zmm13, + kCidetReg_Avx512_Zmm14, + kCidetReg_Avx512_Zmm15, + kCidetReg_Avx512_Zmm16, + kCidetReg_Avx512_Zmm17, + kCidetReg_Avx512_Zmm18, + kCidetReg_Avx512_Zmm19, + kCidetReg_Avx512_Zmm20, + kCidetReg_Avx512_Zmm21, + kCidetReg_Avx512_Zmm22, + kCidetReg_Avx512_Zmm23, + kCidetReg_Avx512_Zmm24, + kCidetReg_Avx512_Zmm25, + kCidetReg_Avx512_Zmm26, + kCidetReg_Avx512_Zmm27, + kCidetReg_Avx512_Zmm28, + kCidetReg_Avx512_Zmm29, + kCidetReg_Avx512_Zmm30, + kCidetReg_Avx512_Zmm31, +#define kCidetReg_Avx512_First kCidetReg_Avx512_Zmm0 +#define kCidetReg_Avx512_Last kCidetReg_Avx512_Zmm31 + + kCidetReg_End +} CIDETREG; + + +/** @name CIDETBUF_XXX - buffer flags. + * @{ */ +#define CIDETBUF_PROT_MASK UINT32_C(0x0000000f) /**< Page protection mask. */ +#define CIDETBUF_PROT_RWX UINT32_C(0x00000001) /**< Read + write + execute. */ +#define CIDETBUF_PROT_RWNX UINT32_C(0x00000002) /**< Read + write + no execute. */ +#define CIDETBUF_PROT_RX UINT32_C(0x00000003) /**< Read + execute. */ +#define CIDETBUF_PROT_RNX UINT32_C(0x00000004) /**< Read + no execute. */ +#define CIDETBUF_PROT_RWX_1NP UINT32_C(0x00000005) /**< Read + write + execute; 1 page not present. */ +#define CIDETBUF_PROT_RWX_1RWNX UINT32_C(0x00000006) /**< Read + write + execute; 1 page read + write + no execute. */ +#define CIDETBUF_PROT_RWX_1RNX UINT32_C(0x00000007) /**< Read + write + execute; 1 page read + no execute. */ +#define CIDETBUF_PROT_RWX_1RWXS UINT32_C(0x00000008) /**< Read + write + execute; 1 page read + execute + supervisor. */ + +#define CIDETBUF_LOC_MASK UINT32_C(0x000000f0) /**< Location mask. */ +/** Buffer located at top and start of the 32-bit address space. */ +#define CIDETBUF_LOC_32BIT_WRAP UINT32_C(0x00000010) +/** Buffer located at the low canonical boundrary (AMD64). */ +#define CIDETBUF_LOC_CANON_LO UINT32_C(0x00000020) +/** Buffer located at the high canonical boundrary (AMD64). */ +#define CIDETBUF_LOC_CANON_HI UINT32_C(0x00000030) + +/** Segment protection mask. */ +#define CIDETBUF_SEG_MASK UINT32_C(0x00000f00) +#define CIDETBUF_SEG_EO UINT32_C(0x00000100) /**< Execute only */ +#define CIDETBUF_SEG_ER UINT32_C(0x00000200) /**< Execute + read */ +#define CIDETBUF_SEG_EO_CONF UINT32_C(0x00000300) /**< Execute only + conforming. */ +#define CIDETBUF_SEG_ER_CONF UINT32_C(0x00000400) /**< Execute + read + conforming. */ +#define CIDETBUF_SEG_RO UINT32_C(0x00000500) /**< Read only. */ +#define CIDETBUF_SEG_RW UINT32_C(0x00000600) /**< Read + write. */ +#define CIDETBUF_SEG_RO_DOWN UINT32_C(0x00000700) /**< Read only + expand down. */ +#define CIDETBUF_SEG_RW_DOWN UINT32_C(0x00000800) /**< Read + write + expand down. */ + +#define CIDETBUF_DPL_MASK UINT32_C(0x00003000) /**< DPL mask. */ +#define CIDETBUF_DPL_0 UINT32_C(0x00000000) /**< DPL=0. */ +#define CIDETBUF_DPL_1 UINT32_C(0x00001000) /**< DPL=1. */ +#define CIDETBUF_DPL_2 UINT32_C(0x00002000) /**< DPL=2. */ +#define CIDETBUF_DPL_3 UINT32_C(0x00003000) /**< DPL=3. */ +#define CIDETBUF_DPL_SAME UINT32_C(0x00004000) /**< Same DPL as the execution environment. */ + +#define CIDETBUF_SEG_LIMIT_BASE_CAP UINT32_C(0x00008000) /**< Capability to change segment limit and base. */ + +#define CIDETBUF_KIND_DATA UINT32_C(0x00000000) /**< Data buffer. */ +#define CIDETBUF_KIND_CODE UINT32_C(0x80000000) /**< Code buffer. */ +/** Checks if @a a_fFlags describes a code buffer. */ +#define CIDETBUF_IS_CODE(a_fFlags) (((a_fFlags) & CIDETBUF_KIND_CODE) != 0) +/** Checks if @a a_fFlags describes a data buffer. */ +#define CIDETBUF_IS_DATA(a_fFlags) (((a_fFlags) & CIDETBUF_KIND_CODE) == 0) +/** @} */ + +/** Code buffer size. (At least two pages.) */ +#define CIDET_CODE_BUF_SIZE (PAGE_SIZE * 2) +/** Data buffer size. (At least two pages.) */ +#define CIDET_DATA_BUF_SIZE (PAGE_SIZE * 3) + + +/** + * Detailed expected exception. + * + * This is used to internally in the core to calculate the expected exception + * considering all the things that may cause exceptions. + */ +typedef enum CIDETEXPECTXCPT +{ + kCidetExpectXcpt_Invalid = 0, + /** No exception expected. */ + kCidetExpectXcpt_None, + + /** Page not present. */ + kCidetExpectXcpt_PageNotPresent, + /** Write access to a non-writable page. */ + kCidetExpectXcpt_PageNotWritable, + /** Executable access to a non-executable page. */ + kCidetExpectXcpt_PageNotExecutable, + /** Access to supervisor page from user mode code. */ + kCidetExpectXcpt_PagePrivileged, +#define kCidetExpectXcpt_First_PageFault kCidetExpectXcpt_PageNotPresent +#define kCidetExpectXcpt_Last_PageFault kCidetExpectXcpt_PagePrivileged + + /** Read or write access to an execute only segment. */ + kCidetExpectXcpt_SegExecuteOnly, + /** Write to a read only or execute+read segment. */ + kCidetExpectXcpt_SegNotWritable, + /** Exceeded the limit of a non-stack access. */ + kCidetExpectXcpt_SegExceededLimit, + /** Non-canonical address via any segment other than the stack. */ + kCidetExpectXcpt_AddrNotCanonical, + /** Misaligned 16 or 32 byte SSE or AVX operand. */ + kCidetExpectXcpt_MisalignedSseAvx, + /** Privileged instruction. */ + kCidetExpectXcpt_PrivilegedInstruction, +#define kCidetExpectXcpt_First_GeneralProtectionFault kCidetExpectXcpt_SegExecuteOnly +#define kCidetExpectXcpt_Last_GeneralProtectionFault kCidetExpectXcpt_PrivilegedInstruction + + /** Exceeded the limit of a stack access. */ + kCidetExpectXcpt_StackExceededLimit, + /** Non-canonical stack address. */ + kCidetExpectXcpt_StackAddrNotCanonical, +#define kCidetExpectXcpt_First_StackFault kCidetExpectXcpt_StackExceededLimit +#define kCidetExpectXcpt_Last_StackFault kCidetExpectXcpt_StackAddrNotCanonical + + /** Misaligned memory operand (and alignment checking is in effect) if AC is + * enabled (executing in ring-3). */ + kCidetExpectXcpt_MisalignedIfAcEnabled, + /** Misaligned 16 byte memory operand resulting in \#AC if ring-3 and + * enable, otherwise \#GP(0). */ + kCidetExpectXcpt_Misaligned16ByteAcEnabledOrGp, +#define kCidetExpectXcpt_First_AlignmentCheckFault kCidetExpectXcpt_MisalignedIfAcEnabled +#define kCidetExpectXcpt_Last_AlignmentCheckFault kCidetExpectXcpt_Misaligned16ByteAcEnabledOrGp + + kCidetExpectXcpt_End +} CIDETEXPECTXCPT; + + +/** + * Buffer configuration. + */ +typedef struct CIDETBUFCFG +{ + /** The name of this buffer configuration. */ + const char *pszName; + /** The buffer flags (CIDETBUF_XXX) */ + uint32_t fFlags; +} CIDETBUFCFG; +/** Pointer to a constant buffer configuration. */ +typedef CIDETBUFCFG const *PCCIDETBUFCFG; + + +/** + * CIDET buffer for code or data. + * + * ASSUMES page aligned buffers. + */ +typedef struct CIDETBUF +{ + /** @name Owned & modified by the front end. + * @{ */ + /** Effective buffer address. */ + uint64_t uEffBufAddr; + /** The segment base address. */ + uint64_t uSegBase; + /** The active segment limit (see also cbSegLimit). UINT64_MAX if flat. */ + uint64_t cbActiveSegLimit; + /** This specifies the selector to use if a non-flat segment limit or special + * segment flags was requested via pfnSetupBuf. UINT32_MAX if any segment is + * selector works. */ + uint32_t uSeg; + /** The off value at the last pfnReinitBuf call. */ + uint16_t offActive; + /** The cb value at the last pfnReinitBuf call. */ + uint16_t cbActive; + /** Prologue (or front fence) size. */ + uint16_t cbPrologue; + /** Epilogue (or tail fence) size. */ + uint16_t cbEpilogue; + /** @} */ + + /** @name Set by the core before pfnReinitBuf call. + * @{ */ + /** Pointer to the buffer config. */ + PCCIDETBUFCFG pCfg; + /** The configuration index. */ + uint32_t idxCfg; + /** The offset into the buffer of the data / code. */ + uint16_t off; + /** The number of bytes of data / code. */ + uint16_t cb; + /** The segment limit relative to the start of the buffer (last byte included + * in count). UINT16_MAX if maximum segment size should be used. */ + uint16_t cbSegLimit; + /** Desired segment base offset. + * This is for checking where the alignment checks are performed. */ + uint8_t offSegBase; + + /** Set if this buffer is actively being used. */ + bool fActive : 1; + /** The operand index (if data), 7 if not active. */ + uint8_t idxOp : 3; + /** Code: Set if the expected exception is supposed to occur on the + * following insturction, not the instruction unter test. */ + bool fXcptAfterInstruction : 1; + /** Set if the instruction will read from the buffer. */ + bool fRead : 1; + /** Set if the instruction will write to the buffer. */ + bool fWrite : 1; + /** The expected exception. */ + CIDETEXPECTXCPT enmExpectXcpt; + /** @} */ +} CIDETBUF; +/** Pointer to a CIDET buffer for code or data. */ +typedef CIDETBUF *PCIDETBUF; + + +/** + * CPU Instruction Decoding & Execution Testing (CIDET) state. + */ +typedef struct CIDETCORE +{ + /** Magic number (CIDETCORE_MAGIC). */ + uint32_t u32Magic; + + /** The target CPU mode / environment. */ + uint8_t bMode; + /** The target ring. */ + uint8_t iRing; + /** Unused padding bytes. */ + uint8_t abPadding1[2]; + + /** Test configuration. */ + uint64_t fTestCfg; + + /** Code buffer configurations to test. + * The first buffer must be a normal buffer that does not cause any problems. */ + PCCIDETBUFCFG paCodeBufConfigs; + /** The number of code buffer configurations to test (pafCodeBufConfigs). */ + uint32_t cCodeBufConfigs; + /** The number of data buffer configurations to test (pafDataBufConfigs). */ + uint32_t cDataBufConfigs; + /** Data buffer configurations to test. + * The first buffer must be a normal buffer that does not cause any problems. */ + PCCIDETBUFCFG paDataBufConfigs; + + /** The instruction currently under testing. */ + PCCIDETINSTR pCurInstr; + + /** Primary data buffer. */ + CIDETBUF DataBuf; + /** Secondary data buffer. */ + CIDETBUF DataBuf2; + + /** Handle to the random number source. */ + RTRAND hRand; + + /** + * Re-initializes one of the data buffers. + * + * @returns true on succes, false if the request cannot be satisfied. + * @param pThis The core state. + * @param pBuf Pointer to the buffer structure. + */ + DECLCALLBACKMEMBER(bool, pfnReInitDataBuf,(struct CIDETCORE *pThis, PCIDETBUF pBuf)); + + /** + * Copies bytes into the data buffer and sets it up for execution. + * + * @returns true on succes, false if the request cannot be satisfied. + * @param pThis The core state. + * @param pBuf Pointer to the buffer structure. + * @param pvSrc The source bytes (size and destination offset + * given in pfnReinitBuf call). + */ + DECLCALLBACKMEMBER(bool, pfnSetupDataBuf,(struct CIDETCORE *pThis, PCIDETBUF pBuf, void const *pvSrc)); + + /** + * Compares buffer content after test execution. + * + * This also checks any fill bytes in the buffer that the front end may + * have put up. The front end will double buffer the content of supposedly + * inaccessible pages as well as non-existing pages to simplify things for + * the core code. + * + * @returns true if equal, false if not. + * @param pThis The core state. + * @param pBuf Pointer to the buffer structure. + * @param pvExpected Pointer to the expected source bytes (size and + * buffer offset given in pfnReinitBuf call). + */ + DECLCALLBACKMEMBER(bool, pfnIsBufEqual,(struct CIDETCORE *pThis, struct CIDETBUF *pBuf, void const *pvExpected)); + + /** + * Re-initializes the code buffer. + * + * @returns true on succes, false if the request cannot be satisfied. + * @param pThis The core state. + * @param pBuf Pointer to the CodeBuf member. The off and cb + * members represent what the core wants to + * execute. + */ + DECLCALLBACKMEMBER(bool, pfnReInitCodeBuf,(struct CIDETCORE *pThis, PCIDETBUF pBuf)); + + /** + * Emit code into the code buffer, making everything ready for pfnExecute. + * + * @returns VBox status code. + * @param pThis Pointer to the core structure. + * @param pBuf Pointer to the CodeBuf member. + * @param pvInstr Pointer to the encoded instruction bytes. + */ + DECLCALLBACKMEMBER(bool, pfnSetupCodeBuf,(struct CIDETCORE *pThis, PCIDETBUF pBuf, void const *pvInstr)); + + /** + * Executes the code indicated by InCtx, returning the result in ActualCtx. + * + * @returns true if execute, false if skipped. + * @param pThis Pointer to the core structure. + */ + DECLCALLBACKMEMBER(bool, pfnExecute,(struct CIDETCORE *pThis)); + + /** + * Report a test failure. + * + * @param pThis Pointer to the core structure. + * @param pszFormat Format string containing failure details. + * @param va Arguments referenced in @a pszFormat. + */ + DECLCALLBACKMEMBER(void, pfnFailure,(struct CIDETCORE *pThis, const char *pszFormat, va_list va)); + + /** Array of indexes for use by FNCIDETSETUPINOUT. + * Reset when changing instruction or switching between valid and invalid + * inputs. */ + uint32_t aiInOut[4]; + + /** @name Copyied and extracted instruction information. + * @{ */ + /** The flags (CIDET_OF_XXX) for the MODRM.REG operand, 0 if not applicable. */ + uint32_t fMrmRegOp; + /** The flags (CIDET_OF_XXX) for the MODRM.RM operand, 0 if not applicable. */ + uint32_t fMrmRmOp; + /** Instruction flags (CIDETINSTR::fFlags). */ + uint64_t fInstrFlags; + /** Number of operands (CIDETINSTR::cOperands). */ + uint8_t cOperands; + /** Number of memory operands (set by CidetCoreSetupFirstMemoryOperandConfig). */ + uint8_t cMemoryOperands : 3; + /** Set if we're working on a MOD R/M byte. */ + bool fUsesModRm : 1; + /** The index of the MODRM.REG operand, 7 if not applicable. */ + uint8_t idxMrmRegOp : 3; + /** The index of the MODRM.RM operand, 7 if not applicable. */ + uint8_t idxMrmRmOp : 3; + /** Set if the SIB byte uses VEX registers for indexing. */ + bool fUsesVexIndexRegs : 1; + /** @} */ + + /** @name Basic encoding knobs, wheels and indicators. + * @{ */ + /** Set if we're working on a SIB byte. */ + bool fSib : 1; + /** Required segment prefix (X86_SREG_XXX), X86_SREG_COUNT if not. */ + uint8_t uSegPrf : 3; + /** The address size prefix. */ + bool fAddrSizePrf : 1; + /** The operand size prefix. */ + bool fOpSizePrf : 1; + /** The REX.W prefix value. */ + bool fRexW : 1; + /** The REX.R prefix value. */ + bool fRexR : 1; + /** The REX.X prefix value. */ + bool fRexX : 1; + /** The REX.B prefix value. */ + bool fRexB : 1; + /** Set if a REX prefix is required with or without flags (for byte regs). */ + bool fRex : 1; + /** Use VEX encoding. */ + bool fVex : 1; + /** Use EVEX encoding. */ + bool fEvex : 1; + /** Indicator: Effective addressing mode in bytes (2, 4, 8). */ + uint8_t cbAddrMode : 4; + /** Indicator: Set if there is an operand accessing memory. */ + bool fHasMemoryOperand : 1; + /** Indicator: Set if a register is used in two or more operands, and one of + * them being for addressing. */ + bool fHasRegCollisionMem : 1; + /** Indicator: Helper indicator for tracking SIB.BASE collision. */ + bool fHasRegCollisionMemBase : 1; + /** Indicator: Helper indicator for tracking SIB.INDEX collision. */ + bool fHasRegCollisionMemIndex : 1; + /** Indicator: Set if a register is used directly in more than one operand. */ + bool fHasRegCollisionDirect : 1; + + /** Indicator: Set if MODRM.REG is the stack register. */ + bool fHasStackRegInMrmReg : 1; + /** Indicator: Set if MODRM.RM or SIB.BASE is the stack register. */ + bool fHasStackRegInMrmRmBase: 1; + + /** Indicator: High byte-register specified by MODRM.REG. */ + bool fHasHighByteRegInMrmReg : 1; + /** Indicator: High byte-register specified by MODRM.RM. */ + bool fHasHighByteRegInMrmRm : 1; + /** Indicator: Set if REX prefixes are incompatible with the byte-register + * specified by MODRM.REG. */ + bool fNoRexPrefixMrmReg : 1; + /** Indicator: Set if REX prefixes are incompatible with the byte-register + * specified by MODRM.RM. */ + bool fNoRexPrefixMrmRm : 1; + /** Indicator: fNoRexPrefixMrmReg || fNoRexPrefixMrmMr. */ + bool fNoRexPrefix : 1; + /** The MOD R/M byte we're working on (if fUsesModRm is set). */ + uint8_t bModRm; + /** The SIB/VSIB byte we're working on (if fSib is set). */ + uint8_t bSib; + /** @} */ + + /** The effective instruction address. (See InCtx.rip and InCtx.cs for the + * rest of the instruction addressing stuff.) */ + uint64_t uInstrEffAddr; + + /** Operand information, mainly for the FNCIDETSETUPINOUT and similar. */ + struct + { + /** The operand flags copied from (CIDETINSTR::afOperands). */ + uint32_t fFlags; + /** The encoded register number, if register, UINT8_MAX if not. */ + uint8_t iReg; + /** The actual operand size (encoded). */ + uint8_t cb; + /** Set if immediate value. */ + bool fIsImmediate : 1; + /** Set if memory access. */ + bool fIsMem : 1; + /** Set if addressing is relative to RIP. */ + bool fIsRipRelative : 1; + /** Set if it's a high byte register. */ + bool fIsHighByteRegister : 1; + /** Size of the disposition, 0 if none. */ + uint8_t cbMemDisp; + /** Base register, UINT8_MAX if not applicable. */ + uint8_t iMemBaseReg; + /** Index register, UINT8_MAX if not applicable. */ + uint8_t iMemIndexReg; + /** Index register, 1 if not applicable. */ + uint8_t uMemScale; + /** Effective segment register, UINT8_MAX if not memory access. */ + uint8_t iEffSeg; + /** Segment offset if memory access. Undefined if not memory access. */ + uint64_t offSeg; + /** The effective address if memory access. */ + uint64_t uEffAddr; + /** Immediate or displacement value. */ + uint64_t uImmDispValue; + /** Base register value, undefined if irrelevant. */ + uint64_t uMemBaseRegValue; + /** Index register value, undefined if irrelevant. */ + uint64_t uMemIndexRegValue; + /** Points to where the input data for this operand should be placed, + * when possible. In the fIsMem = true case, it either points directly + * to the input buffer or to a temporary one. While in the other case, + * it'll point into InCtx when possible. */ + RTPTRUNION In; + /** Points to where the expected output data for this operand should be + * stored, when possible. In the fIsMem = false case, it'll point into + * ExpectedCtx when possible. */ + RTPTRUNION Expected; + /** Pointer to the data buffer for this operand. */ + PCIDETBUF pDataBuf; + } aOperands[4]; + + /** Buffer where we assemble the instruction. */ + uint8_t abInstr[45]; + /** The size of the instruction in abInstr. */ + uint8_t cbInstr; + /** Offset of the instruction into the buffer. */ + uint16_t offInstr; + /** Current code buffer. */ + CIDETBUF CodeBuf; + + /** The input context. Initalized by driver and FNCIDETSETUPINOUT. */ + CIDETCPUCTX InCtx; + /** The expected output context. */ + CIDETCPUCTX ExpectedCtx; + /** The actual output context. */ + CIDETCPUCTX ActualCtx; + /** Template input context, initialized when setting the mode. */ + CIDETCPUCTX InTemplateCtx; + + /** Input and expected output temporary memory buffers. */ + uint8_t abBuf[0x2000]; + + + /** Number of skipped tests because of pfnSetupInOut failures. */ + uint32_t cSkippedSetupInOut; + /** Number of skipped tests because of pfnReInitDataBuf failures. */ + uint32_t cSkippedReInitDataBuf; + /** Number of skipped tests because of pfnSetupDataBuf failures. */ + uint32_t cSkippedSetupDataBuf; + /** Number of skipped tests because RIP relative addressing constraints. */ + uint32_t cSkippedDataBufWrtRip; + /** Number of skipped tests because of assemble failures. */ + uint32_t cSkippedAssemble; + /** Number of skipped tests because of pfnReInitCodeBuf failures. */ + uint32_t cSkippedReInitCodeBuf; + /** Number of skipped tests because of pfnSetupCodeBuf failures. */ + uint32_t cSkippedSetupCodeBuf; + /** Number of skipped tests because the base and index registers are the same + * one and there was a remainder when trying to point to the data buffer. */ + uint32_t cSkippedSameBaseIndexRemainder; + /** Number of skipped tests because index-only addressing left a remainder. */ + uint32_t cSkippedOnlyIndexRemainder; + /** Number of skipped tests because of direct addressing overflowed. */ + uint32_t cSkippedDirectAddressingOverflow; + + +} CIDETCORE; +/** Pointer to the CIDET core state. */ +typedef CIDETCORE *PCIDETCORE; + +/** Magic number for CIDETCORE (Lee Konitz). */ +#define CIDETCORE_MAGIC UINT32_C(0x19271013) + + +int CidetCoreInit(PCIDETCORE pThis, RTRAND hRand); +void CidetCoreDelete(PCIDETCORE pThis); +int CidetCoreSetTargetMode(PCIDETCORE pThis, uint8_t bMode); +uint32_t CidetCoreGetOperandSize(PCIDETCORE pThis, uint8_t iOp); +bool CidetCoreTestInstruction(PCIDETCORE pThis, PCCIDETINSTR pInstr); + + +extern const CIDETINSTR g_aCidetInstructions1[]; +extern const uint32_t g_cCidetInstructions1; + +#endif /* !VBOX_INCLUDED_SRC_cpu_cidet_h */ + diff --git a/src/VBox/ValidationKit/utils/cpu/cidet.mac b/src/VBox/ValidationKit/utils/cpu/cidet.mac new file mode 100644 index 00000000..d55333df --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cidet.mac @@ -0,0 +1,75 @@ +; $Id: cidet.mac $ ; +;; @file +; CPU Instruction Decoding & Execution Tests - Assembly Header. +; + +; +; Copyright (C) 2014-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 <https://www.gnu.org/licenses>. +; +; The contents of this file may alternatively be used under the terms +; of the Common Development and Distribution License Version 1.0 +; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +; in the VirtualBox distribution, in which case the provisions of the +; CDDL are applicable instead of those of the GPL. +; +; You may elect to license modified versions of this file under the +; terms and conditions of either the GPL or the CDDL or both. +; +; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +; + + +%ifndef ___cidet_mac___ +%define ___cidet_mac___ + +struc CIDETCPUCTX + .rip resq 1 + .rfl resq 1 + .aGRegs resq 16 + .aSRegs resw 6 + +%ifndef CIDET_REDUCED_CTX + .tr resw 1 + .ldtr resw 1 + .cr0 resq 1 +%else + .au16Padding resw 2 +%endif + .cr2 resq 1 +%ifndef CIDET_REDUCED_CTX + .cr3 resq 1 + .cr4 resq 1 + .cr8 resq 1 + .dr0 resq 1 + .dr1 resq 1 + .dr2 resq 1 + .dr3 resq 1 + .dr6 resq 1 + .dr7 resq 1 +%endif + + .uErr resq 1 + .uXcpt resd 1 + + .fIgnoredRFlags resd 1 + .fTrickyStack resb 1 +endstruc + +%endif + diff --git a/src/VBox/ValidationKit/utils/cpu/cpu-alloc-all-mem.cpp b/src/VBox/ValidationKit/utils/cpu/cpu-alloc-all-mem.cpp new file mode 100644 index 00000000..889cb600 --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cpu-alloc-all-mem.cpp @@ -0,0 +1,223 @@ +/* $Id: cpu-alloc-all-mem.cpp $ */ +/** @file + * Allocate all memory we can get and then quit. + */ + +/* + * Copyright (C) 2011-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/test.h> + +#include <iprt/asm.h> +#include <iprt/list.h> +#include <iprt/mem.h> +#include <iprt/param.h> +#include <iprt/string.h> +#include <iprt/time.h> + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef struct TSTALLOC +{ + /** The page sequence number. */ + size_t iPageSeq; + /** The allocation sequence number. */ + size_t iAllocSeq; + /** The allocation size. */ + size_t cb; + /** Pointer to the ourselves (paranoid). */ + void *pv; + /** Linked list node. */ + RTLISTNODE Node; + +} TSTALLOC; +typedef TSTALLOC *PTSTALLOC; + + +static bool checkList(PRTLISTNODE pHead) +{ + size_t iPageSeq = 0; + size_t iAllocSeq = 0; + PTSTALLOC pCur; + RTListForEach(pHead, pCur, TSTALLOC, Node) + { + RTTESTI_CHECK_RET(pCur->iAllocSeq == iAllocSeq, false); + RTTESTI_CHECK_RET(pCur->pv == pCur, false); + + size_t const *pu = (size_t const *)pCur; + size_t const *puEnd = pu + pCur->cb / sizeof(size_t); + while (pu != puEnd) + { + RTTESTI_CHECK_RET(*pu == iPageSeq, false); + iPageSeq++; + pu += PAGE_SIZE / sizeof(size_t); + } + iAllocSeq++; + } + return true; +} + + +static void doTest(RTTEST hTest) +{ + RTTestSub(hTest, "Allocate all memory"); + + RTLISTANCHOR AllocHead; + PTSTALLOC pCur; + uint64_t cNsElapsed = 0; + size_t cbPrint = 0; + uint64_t uPrintTS = 0; + size_t cbTotal = 0; +#if ARCH_BITS == 64 + size_t const cbOneStart = 64 * _1M; + size_t const cbOneMin = 4 * _1M; +#else + size_t const cbOneStart = 16 * _1M; + size_t const cbOneMin = 4 * _1M; +#endif + size_t cbOne = cbOneStart; + size_t cAllocs = 0; + uint32_t iPageSeq = 0; + RTListInit(&AllocHead); + + for (;;) + { + /* + * Allocate a chunk and make sure all the pages are there. + */ + uint64_t const uStartTS = RTTimeNanoTS(); + pCur = (PTSTALLOC)RTMemPageAlloc(cbOne); + if (pCur) + { + size_t *pu = (size_t *)pCur; + size_t *puEnd = pu + cbOne / sizeof(size_t); + while (pu != puEnd) + { + *pu = iPageSeq++; + pu += PAGE_SIZE / sizeof(size_t); + } + uint64_t const uEndTS = RTTimeNanoTS(); + uint64_t const cNsThis = uEndTS - uStartTS; + + /* + * Update the statistics. + */ + cNsElapsed += cNsThis; + cbTotal += cbOne; + cAllocs++; + + /* + * Link the allocation. + */ + pCur->iAllocSeq = cAllocs - 1; + pCur->pv = pCur; + pCur->cb = cbOne; + RTListAppend(&AllocHead, &pCur->Node); + + /* + * Print progress info? + */ + if ( uEndTS - uPrintTS >= RT_NS_1SEC_64*10 +#if ARCH_BITS == 64 + || cbTotal - cbPrint >= _4G +#else + || cbTotal - cbPrint >= _2G +#endif + ) + { + cbPrint = cbTotal; + uPrintTS = uEndTS; + + uint32_t cMBPerSec = (uint32_t)((long double)cbTotal / ((long double)cNsElapsed / RT_NS_1SEC) / _1M); + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "%'zu bytes in %'llu ns - %'u MB/s\n", + cbTotal, cNsElapsed, cMBPerSec); + RTTESTI_CHECK_RETV(checkList(&AllocHead)); + } + } + else + { + /* + * Try again with a smaller request. + */ + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Failed to allocate %'zu bytes (after %'zu bytes)\n", cbOne, cbTotal); + if (cbOne <= cbOneMin) + break; + cbOne = cbOneMin; + } + } + + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "Verifying...\n"); + RTTESTI_CHECK_RETV(checkList(&AllocHead)); + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "... detected no corruption.\n"); + + /* + * Free up some memory before displaying the results. + */ + size_t i = 0; + PTSTALLOC pPrev; + RTListForEachReverseSafe(&AllocHead, pCur, pPrev, TSTALLOC, Node) + { + RTMemPageFree(pCur->pv, pCur->cb); + if (++i > 32) + break; + } + + RTTestValue(hTest, "amount", cbTotal, RTTESTUNIT_BYTES); + RTTestValue(hTest, "time", cNsElapsed, RTTESTUNIT_NS); + uint32_t cMBPerSec = (uint32_t)((long double)cbTotal / ((long double)cNsElapsed / RT_NS_1SEC) / _1M); + RTTestValue(hTest, "speed", cMBPerSec, RTTESTUNIT_MEGABYTES_PER_SEC); + RTTestSubDone(hTest); +} + + +int main(int argc, char **argv) +{ + RTTEST hTest; + RTEXITCODE rcExit = RTTestInitAndCreate("memallocall", &hTest); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + RTTestBanner(hTest); + + NOREF(argv); + if (argc == 1) + doTest(hTest); + else + RTTestFailed(hTest, "This test takes no arguments!"); + + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/ValidationKit/utils/cpu/cpu-numa.cpp b/src/VBox/ValidationKit/utils/cpu/cpu-numa.cpp new file mode 100644 index 00000000..1331c3ce --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/cpu-numa.cpp @@ -0,0 +1,205 @@ +/* $Id: cpu-numa.cpp $ */ +/** @file + * numa - NUMA / memory benchmark. + */ + +/* + * Copyright (C) 2011-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/test.h> + +#include <iprt/asm.h> +//#if defined(RT_ARCH_X86) || defined(RT_ARCH_AMD64) +//# include <iprt/asm-amd64-x86.h> +//#endif +#include <iprt/mem.h> +#include <iprt/mp.h> +#include <iprt/string.h> +#include <iprt/thread.h> +#include <iprt/time.h> + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** The number of threads to skip when testing. */ +static uint32_t g_cThreadsToSkip = 1; + +/** + * Gets the next online CPU. + * + * @returns Next CPU index or RTCPUSET_MAX_CPUS. + * @param iCurCpu The current CPU (index). + */ +static int getNextCpu(unsigned iCurCpu) +{ + /* Skip to the next chip. */ + iCurCpu = (iCurCpu / g_cThreadsToSkip) * g_cThreadsToSkip; + iCurCpu += g_cThreadsToSkip; + + /* Skip offline cpus. */ + while ( iCurCpu < RTCPUSET_MAX_CPUS + && !RTMpIsCpuOnline(iCurCpu) ) + iCurCpu++; + + /* Make sure we're within bounds (in case of bad input). */ + if (iCurCpu > RTCPUSET_MAX_CPUS) + iCurCpu = RTCPUSET_MAX_CPUS; + return iCurCpu; +} + + +static void doTest(RTTEST hTest) +{ + NOREF(hTest); + uint32_t iAllocCpu = 0; + while (iAllocCpu < RTCPUSET_MAX_CPUS) + { + const uint32_t cbTestSet = _1M * 32; + const uint32_t cIterations = 384; + + /* + * Change CPU and allocate a chunk of memory. + */ + RTTESTI_CHECK_RC_OK_RETV(RTThreadSetAffinityToCpu(RTMpCpuIdFromSetIndex(iAllocCpu))); + + void *pvTest = RTMemPageAlloc(cbTestSet); /* may be leaked, who cares */ + RTTESTI_CHECK_RETV(pvTest != NULL); + memset(pvTest, 0xef, cbTestSet); + + /* + * Do the tests. + */ + uint32_t iAccessCpu = 0; + while (iAccessCpu < RTCPUSET_MAX_CPUS) + { + RTTESTI_CHECK_RC_OK_RETV(RTThreadSetAffinityToCpu(RTMpCpuIdFromSetIndex(iAccessCpu))); + + /* + * The write test. + */ + RTTimeNanoTS(); RTThreadYield(); + uint64_t u64StartTS = RTTimeNanoTS(); + for (uint32_t i = 0; i < cIterations; i++) + { + ASMCompilerBarrier(); /* paranoia */ + memset(pvTest, i, cbTestSet); + } + uint64_t const cNsElapsedWrite = RTTimeNanoTS() - u64StartTS; + uint64_t cMBPerSec = (uint64_t)( ((uint64_t)cIterations * cbTestSet) /* bytes */ + / ((long double)cNsElapsedWrite / RT_NS_1SEC_64) /* seconds */ + / _1M /* MB */ ); + RTTestIValueF(cMBPerSec, RTTESTUNIT_MEGABYTES_PER_SEC, "cpu%02u-mem%02u-write", iAllocCpu, iAccessCpu); + + /* + * The read test. + */ + memset(pvTest, 0, cbTestSet); + RTTimeNanoTS(); RTThreadYield(); + u64StartTS = RTTimeNanoTS(); + for (uint32_t i = 0; i < cIterations; i++) + { +#if 1 + size_t u = 0; + size_t volatile *puCur = (size_t volatile *)pvTest; + size_t volatile *puEnd = puCur + cbTestSet / sizeof(size_t); + while (puCur != puEnd) + u += *puCur++; +#else + ASMCompilerBarrier(); /* paranoia */ + void *pvFound = memchr(pvTest, (i & 127) + 1, cbTestSet); + RTTESTI_CHECK(pvFound == NULL); +#endif + } + uint64_t const cNsElapsedRead = RTTimeNanoTS() - u64StartTS; + cMBPerSec = (uint64_t)( ((uint64_t)cIterations * cbTestSet) /* bytes */ + / ((long double)cNsElapsedRead / RT_NS_1SEC_64) /* seconds */ + / _1M /* MB */ ); + RTTestIValueF(cMBPerSec, RTTESTUNIT_MEGABYTES_PER_SEC, "cpu%02u-mem%02u-read", iAllocCpu, iAccessCpu); + + /* + * The read/write test. + */ + RTTimeNanoTS(); RTThreadYield(); + u64StartTS = RTTimeNanoTS(); + for (uint32_t i = 0; i < cIterations; i++) + { + ASMCompilerBarrier(); /* paranoia */ + memcpy(pvTest, (uint8_t *)pvTest + cbTestSet / 2, cbTestSet / 2); + } + uint64_t const cNsElapsedRW = RTTimeNanoTS() - u64StartTS; + cMBPerSec = (uint64_t)( ((uint64_t)cIterations * cbTestSet) /* bytes */ + / ((long double)cNsElapsedRW / RT_NS_1SEC_64) /* seconds */ + / _1M /* MB */ ); + RTTestIValueF(cMBPerSec, RTTESTUNIT_MEGABYTES_PER_SEC, "cpu%02u-mem%02u-read-write", iAllocCpu, iAccessCpu); + + /* + * Total time. + */ + RTTestIValueF(cNsElapsedRead + cNsElapsedWrite + cNsElapsedRW, RTTESTUNIT_NS, + "cpu%02u-mem%02u-time", iAllocCpu, iAccessCpu); + + /* advance */ + iAccessCpu = getNextCpu(iAccessCpu); + } + + /* + * Clean up and advance to the next CPU. + */ + RTMemPageFree(pvTest, cbTestSet); + iAllocCpu = getNextCpu(iAllocCpu); + } +} + + +int main(int argc, char **argv) +{ + RTTEST hTest; + RTEXITCODE rcExit = RTTestInitAndCreate("numa-1", &hTest); + if (rcExit != RTEXITCODE_SUCCESS) + return rcExit; + RTTestBanner(hTest); + +#if defined(RT_ARCH_X86) || defined(RT_ARCH_AMD64) + /** @todo figure basic topology. */ +#endif + if (argc == 2) + g_cThreadsToSkip = RTStrToUInt8(argv[1]); + + doTest(hTest); + + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/ValidationKit/utils/cpu/exceptionsR3-asm.asm b/src/VBox/ValidationKit/utils/cpu/exceptionsR3-asm.asm new file mode 100644 index 00000000..d96194ad --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/exceptionsR3-asm.asm @@ -0,0 +1,160 @@ +; $Id: exceptionsR3-asm.asm $ +;; @file +; exceptionsR3-asm.asm - assembly helpers. +; + +; +; Copyright (C) 2009-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 <https://www.gnu.org/licenses>. +; +; The contents of this file may alternatively be used under the terms +; of the Common Development and Distribution License Version 1.0 +; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +; in the VirtualBox distribution, in which case the provisions of the +; CDDL are applicable instead of those of the GPL. +; +; You may elect to license modified versions of this file under the +; terms and conditions of either the GPL or the CDDL or both. +; +; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +; + + +;******************************************************************************* +;* Header Files * +;******************************************************************************* +%include "iprt/asmdefs.mac" + + +;******************************************************************************* +;* Defined Constants And Macros * +;******************************************************************************* +%ifdef RT_ARCH_AMD64 + %define TST_XCPT_MAGIC 0123456789abcdef0h +%else + %define TST_XCPT_MAGIC 012345678h +%endif + +%macro tstXcptAsmProlog 0 + push xBP + push xDI + push xSI + push xBX + %ifdef RT_ARCH_X86 + push gs + push fs + push es + push ds + %endif + %ifdef RT_ARCH_AMD64 + push r10 + push r11 + push r12 + push r13 + push r14 + push r15 + %endif + + mov xAX, TST_XCPT_MAGIC + mov xBX, xAX + mov xCX, xAX + mov xDX, xAX + mov xDI, xAX + mov xSI, xAX + mov xBP, xAX + %ifdef RT_ARCH_AMD64 + mov r8, xAX + mov r9, xAX + mov r10, xAX + mov r11, xAX + mov r12, xAX + mov r13, xAX + mov r14, xAX + mov r15, xAX + %endif +%endmacro + +%macro tstXcptAsmEpilog 0 + %ifdef RT_ARCH_AMD64 + pop r15 + pop r14 + pop r13 + pop r12 + pop r11 + pop r10 + %endif + %ifdef RT_ARCH_X86 + pop ds + pop es + pop fs + pop gs + %endif + pop xBX + pop xSI + pop xDI + pop xBP +%endmacro + + +BEGINCODE + +;; +BEGINPROC tstXcptAsmNullPtrRead +; tstXcptAsmProlog + xor eax, eax +GLOBALNAME tstXcptAsmNullPtrRead_PC + mov al, [xAX] +; tstXcptAsmEpilog + ret +ENDPROC tstXcptAsmNullPtrRead + + +;; +BEGINPROC tstXcptAsmNullPtrWrite + tstXcptAsmProlog + xor eax, eax +GLOBALNAME tstXcptAsmNullPtrWrite_PC + mov [xAX], al + tstXcptAsmEpilog + ret +ENDPROC tstXcptAsmNullPtrWrite + + +;; +BEGINPROC tstXcptAsmSysCall + tstXcptAsmProlog +GLOBALNAME tstXcptAsmSysCall_PC + syscall + tstXcptAsmEpilog + ret +ENDPROC tstXcptAsmSysCall + + +;; +BEGINPROC tstXcptAsmSysEnter + tstXcptAsmProlog +GLOBALNAME tstXcptAsmSysEnter_PC +%ifdef RT_ARCH_AMD64 + db 00fh, 034h ; test this on 64-bit, yasm complains... +%else + sysenter +%endif + tstXcptAsmEpilog + ret +ENDPROC tstXcptAsmSysEnter + diff --git a/src/VBox/ValidationKit/utils/cpu/exceptionsR3.cpp b/src/VBox/ValidationKit/utils/cpu/exceptionsR3.cpp new file mode 100644 index 00000000..474e0eeb --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/exceptionsR3.cpp @@ -0,0 +1,272 @@ +/* $Id: exceptionsR3.cpp $ */ +/** @file + * exceptionsR3 - Tests various ring-3 CPU exceptions. + */ + +/* + * Copyright (C) 2009-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/cdefs.h> +#include <iprt/ctype.h> +#include <iprt/getopt.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> +#include <iprt/x86.h> + +#include <setjmp.h> + +#ifndef RT_OS_WINDOWS +# define USE_SIGNALS +# include <signal.h> +# include <stdlib.h> +#endif + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Executes a simple test. */ +#define TST_XCPT(Trapper, iTrap, uErr) \ + do \ + { \ + RTTestISub(#Trapper); \ + tstXcptReset(); \ + if (!setjmp(g_JmpBuf)) \ + { \ + tstXcptAsm##Trapper(); \ + RTTestIFailed("%s didn't trap (line no %u)", #Trapper, __LINE__); \ + } \ + else if ( (iTrap) != tstXcptCurTrap() \ + || (uErr) != tstXcptCurErr() ) \ + RTTestIFailed("%s trapped with %#x/%#x, expected %#x/%#x (line no %u)", \ + #Trapper, tstXcptCurTrap(), tstXcptCurErr(), (iTrap), (uErr), __LINE__); \ + else \ + RTTestISubDone(); \ + } while (0) + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Where to longjmp to when getting a signal/exception. */ +jmp_buf g_JmpBuf; +#ifdef USE_SIGNALS +/** Pending signal. + * -1 if no signal is pending. */ +int32_t volatile g_iSignal; +/** Pending signal info. */ +siginfo_t volatile g_SigInfo; +#endif + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +DECLASM(void) tstXcptAsmNullPtrRead(void); +DECLASM(void) tstXcptAsmNullPtrWrite(void); +DECLASM(void) tstXcptAsmSysEnter(void); +DECLASM(void) tstXcptAsmSysCall(void); + + + +#ifdef USE_SIGNALS +/** + * Generic signal handler. + */ +static void tstXcptSigHandler(int iSignal, siginfo_t *pSigInfo, void *pvCtx) +{ +#if 1 + RTStrmPrintf(g_pStdErr, "signal %d pSigInfo=%p pvCtx=%p", iSignal, pSigInfo, pvCtx); + if (pSigInfo) + RTStrmPrintf(g_pStdErr, " si_addr=%p si_code=%#x sival_ptr=%p sival_int=%d", + pSigInfo->si_addr, pSigInfo->si_code, pSigInfo->si_value.sival_ptr, pSigInfo->si_value.sival_int); + RTStrmPrintf(g_pStdErr, "\n"); +#endif + if (g_iSignal == -1) + { + g_iSignal = iSignal; + if (pSigInfo) + memcpy((void *)&g_SigInfo, pSigInfo, sizeof(g_SigInfo)); + longjmp(g_JmpBuf, 1); + } + else + { + /* we're up the infamous creek... */ + _Exit(2); + } +} + +#elif defined(RT_OS_WINDOWS) +/** @todo */ +//# error "PORTME" + +#else +# error "PORTME" +#endif + + +/** Reset the current exception state and get ready for a new trap. */ +static void tstXcptReset(void) +{ +#ifdef USE_SIGNALS + g_iSignal = -1; + memset((void *)&g_SigInfo, 0, sizeof(g_SigInfo)); +#endif +} + + + +/** Get the current intel trap number. Returns -1 if none. */ +static int tstXcptCurTrap(void) +{ +#ifdef USE_SIGNALS + /** @todo this is just a quick sketch. */ + switch (g_iSignal) + { + case SIGBUS: +# ifdef RT_OS_DARWIN + if (g_SigInfo.si_code == 2 /*KERN_PROTECTION_FAILURE*/) + return X86_XCPT_PF; +# endif + return X86_XCPT_GP; + + case SIGSEGV: + return X86_XCPT_GP; + } +#endif + return -1; +} + + +/** Get the exception error code if applicable. */ +static uint32_t tstXcptCurErr(void) +{ +#ifdef USE_SIGNALS + /** @todo this is just a quick sketch. */ + switch (g_iSignal) + { + case SIGBUS: +# ifdef RT_OS_DARWIN + if (g_SigInfo.si_code == 2 /*KERN_PROTECTION_FAILURE*/) + return 0; +# endif + break; + + case SIGSEGV: + break; + } +#endif + return UINT32_MAX; +} + + +int main(int argc, char **argv) +{ + /* + * Prolog. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("exceptionsR3", &hTest); + if (rc) + return rc; + + /* + * Parse options. + */ + bool volatile fRawMode = false; + static const RTGETOPTDEF s_aOptions[] = + { + { "--raw-mode", 'r', RTGETOPT_REQ_NOTHING }, + }; + + RTGETOPTUNION ValUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0); + while ((rc = RTGetOpt(&GetState, &ValUnion))) + { + switch (rc) + { + case 'r': + fRawMode = true; + break; + + default: + return RTGetOptPrintError(rc, &ValUnion); + } + } + + /* + * Test setup. + */ +#ifdef USE_SIGNALS + struct sigaction Act; + RT_ZERO(Act); + Act.sa_sigaction = tstXcptSigHandler; + Act.sa_flags = SA_SIGINFO; + sigfillset(&Act.sa_mask); + + sigaction(SIGILL, &Act, NULL); + sigaction(SIGTRAP, &Act, NULL); +# ifdef SIGEMT + sigaction(SIGEMT, &Act, NULL); +# endif + sigaction(SIGFPE, &Act, NULL); + sigaction(SIGBUS, &Act, NULL); + sigaction(SIGSEGV, &Act, NULL); + +#else + /** @todo Implement this using structured exception handling on Windows and + * OS/2. */ +#endif + + /* + * The tests. + */ + RTTestBanner(hTest); + TST_XCPT(NullPtrRead, X86_XCPT_PF, 0); + TST_XCPT(NullPtrWrite, X86_XCPT_PF, 0); + if (fRawMode) + { + TST_XCPT(SysEnter, X86_XCPT_GP, 0); + TST_XCPT(SysCall, X86_XCPT_UD, 0); + } + + /* + * Epilog. + */ + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/ValidationKit/utils/cpu/rdtsc-asm.asm b/src/VBox/ValidationKit/utils/cpu/rdtsc-asm.asm new file mode 100644 index 00000000..751c8808 --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/rdtsc-asm.asm @@ -0,0 +1,162 @@ +; $Id: rdtsc-asm.asm $ +;; @file +; RDTSC test, assembly code +; + +; +; Copyright (C) 2009-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 <https://www.gnu.org/licenses>. +; +; The contents of this file may alternatively be used under the terms +; of the Common Development and Distribution License Version 1.0 +; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +; in the VirtualBox distribution, in which case the provisions of the +; CDDL are applicable instead of those of the GPL. +; +; You may elect to license modified versions of this file under the +; terms and conditions of either the GPL or the CDDL or both. +; +; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +; + + +;********************************************************************************************************************************* +;* Header Files * +;********************************************************************************************************************************* +%include "iprt/asmdefs.mac" +%include "iprt/x86.mac" + + +;********************************************************************************************************************************* +;* Global Variables * +;********************************************************************************************************************************* +BEGINDATA +;; +; Where DoTscReads() returns the rdtsc values. +; +; @note The results are 32-bit value pairs in x86 mode and 64-bit pairs in +; AMD64 mode. +GLOBALNAME g_aRdTscResults +%ifdef RT_ARCH_AMD64 + dq 0, 0 + dq 0, 0 ; first value stored + dq 0, 0 + dq 0, 0 + dq 0, 0 + dq 0, 0 + dq 0, 0 +%else + dq 0, 0 + dd 0, 0 ; first value stored + dd 0, 0 + dd 0, 0 +%endif + + +BEGINCODE + +;; Takes no arguments, returns number of values read into g_aRdTscResults. +BEGINPROC DoTscReads + push xBP + mov xBP, xSP +%ifdef RT_ARCH_AMD64 + mov rax, 0feedfacecafebabeh + mov rdx, 0cafebabefeedfaceh + mov r8, 0deadbeefdeadbeefh + mov r9, 0deadbeefdeadbeefh + mov r10, 0deadbeefdeadbeefh + mov r11, 0deadbeefdeadbeefh + push rbx + push r12 + push r13 + push r14 + push r15 + + ; Read 6x TSC into registers. + rdtsc + mov r8, rax + mov r9, rdx + rdtsc + mov r10, rax + mov r11, rdx + rdtsc + mov r12, rax + mov r13, rdx + rdtsc + mov r14, rax + mov r15, rdx + rdtsc + mov rbx, rax + mov rcx, rdx + rdtsc + + ; Store the values (64-bit). + mov [NAME(g_aRdTscResults) + 10h xWrtRIP], r8 + mov [NAME(g_aRdTscResults) + 18h xWrtRIP], r9 + mov [NAME(g_aRdTscResults) + 20h xWrtRIP], r10 + mov [NAME(g_aRdTscResults) + 28h xWrtRIP], r11 + mov [NAME(g_aRdTscResults) + 30h xWrtRIP], r12 + mov [NAME(g_aRdTscResults) + 38h xWrtRIP], r13 + mov [NAME(g_aRdTscResults) + 40h xWrtRIP], r14 + mov [NAME(g_aRdTscResults) + 48h xWrtRIP], r15 + mov [NAME(g_aRdTscResults) + 50h xWrtRIP], rbx + mov [NAME(g_aRdTscResults) + 58h xWrtRIP], rcx + mov [NAME(g_aRdTscResults) + 60h xWrtRIP], rax + mov [NAME(g_aRdTscResults) + 68h xWrtRIP], rdx + + pop r15 + pop r14 + pop r13 + pop r12 + pop rbx + + mov eax, 6 +%else + mov eax, 0feedfaceh + mov edx, 0cafebabeh + push esi + push edi + push ebx + + ; Read 3x TSC into registers. + rdtsc + mov ebx, eax + mov ecx, edx + rdtsc + mov esi, eax + mov edi, edx + rdtsc + + ; Store values. + mov [NAME(g_aRdTscResults) + 08h], ebx + mov [NAME(g_aRdTscResults) + 0ch], ecx + mov [NAME(g_aRdTscResults) + 10h], esi + mov [NAME(g_aRdTscResults) + 14h], edi + mov [NAME(g_aRdTscResults) + 18h], eax + mov [NAME(g_aRdTscResults) + 1ch], edx + + pop ebx + pop edi + pop esi + + mov eax, 3 +%endif + leave + ret +ENDPROC DoTscReads + diff --git a/src/VBox/ValidationKit/utils/cpu/rdtsc.cpp b/src/VBox/ValidationKit/utils/cpu/rdtsc.cpp new file mode 100644 index 00000000..81405f3d --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/rdtsc.cpp @@ -0,0 +1,294 @@ +/* $Id: rdtsc.cpp $ */ +/** @file + * rdtsc - Test if three consecutive rdtsc instructions return different values. + */ + +/* + * Copyright (C) 2009-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/errcore.h> +#include <iprt/initterm.h> +#include <iprt/message.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/time.h> + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef struct RDTSCRESULT +{ + RTCCUINTREG uLow, uHigh; +} RDTSCRESULT; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +extern "C" RDTSCRESULT g_aRdTscResults[]; /* rdtsc-asm.asm */ + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +/** + * Does 3 (32-bit) or 6 (64-bit) fast TSC reads and stores the result + * in g_aRdTscResults, starting with the 2nd entry. + * + * Starting the result storing at g_aRdTscResults[1] make it easy to do the + * comparisons in a loop. + * + * @returns Number of results read into g_aRdTscResults[1] and onwards. + */ +DECLASM(uint32_t) DoTscReads(void); + + + + +int main(int argc, char **argv) +{ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Tunables. + */ + uint64_t offJumpThreshold = _4G * 2; + unsigned cMaxLoops = 10000000; + unsigned cStatusEvery = 2000000; + unsigned cMinSeconds = 0; + + for (int i = 1; i < argc; i++) + { + const char *psz = argv[i]; + if (*psz == '-') + { + psz++; + char chOpt; + while ((chOpt = *psz++) != '\0') + { + /* Option value. */ + const char *pszValue = NULL; + uint64_t uValue = 0; + switch (chOpt) + { + case 'l': + case 's': + case 'm': + if (*psz == '\0') + { + if (i + 1 >= argc) + return RTMsgSyntax("The %c option requires a value", chOpt); + pszValue = argv[++i]; + } + else + pszValue = psz + (*psz == ':' || *psz == '='); + switch (chOpt) + { + case 'l': + case 's': + case 'm': + { + char *pszNext = NULL; + rc = RTStrToUInt64Ex(pszValue, &pszNext, 0, &uValue); + if (RT_FAILURE(rc)) + return RTMsgSyntax("Bad number: %s (%Rrc)", pszValue, rc); + if (pszNext && *pszNext != '\0') + { + if (*pszNext == 'M'&& pszNext[1] == '\0') + uValue *= _1M; + else if (*pszNext == 'K' && pszNext[1] == '\0') + uValue *= _1K; + else if (*pszNext == 'G' && pszNext[1] == '\0') + uValue *= _1G; + else + return RTMsgSyntax("Bad value format for option %c: %s", chOpt, pszValue); + } + break; + } + } + break; + } + + /* handle the option. */ + switch (chOpt) + { + case 'l': + cMaxLoops = uValue; + break; + + case 'm': + cMinSeconds = uValue; + break; + + case 's': + cStatusEvery = uValue; + break; + + case 'h': + case '?': + RTPrintf("usage: rdtsc [-l <loops>] [-s <loops-between-status>]\n" + " [-m <minimum-seconds-to-run>]\n"); + return RTEXITCODE_SUCCESS; + + default: + return RTMsgSyntax("Unknown option %c (argument %d)\n", chOpt, i); + } + } + } + else + return RTMsgSyntax("argument %d (%s): not an option\n", i, psz); + } + + /* + * Do the job. + */ + uint64_t const nsTsStart = RTTimeNanoTS(); + unsigned cOuterLoops = 0; + unsigned cLoopsToNextStatus = cStatusEvery; + unsigned cRdTscInstructions = 0; + unsigned cBackwards = 0; + unsigned cSame = 0; + unsigned cBadValues = 0; + unsigned cJumps = 0; + uint64_t offMaxJump = 0; + uint64_t offMinIncr = UINT64_MAX; + uint64_t offMaxIncr = 0; + + g_aRdTscResults[0] = g_aRdTscResults[DoTscReads() - 1]; + + for (;;) + { + for (unsigned iLoop = 0; iLoop < cMaxLoops; iLoop++) + { + uint32_t const cResults = DoTscReads(); + cRdTscInstructions += cResults; + + for (uint32_t i = 0; i < cResults; i++) + { + uint64_t uPrev = RT_MAKE_U64((uint32_t)g_aRdTscResults[i ].uLow, (uint32_t)g_aRdTscResults[i ].uHigh); + uint64_t uCur = RT_MAKE_U64((uint32_t)g_aRdTscResults[i + 1].uLow, (uint32_t)g_aRdTscResults[i + 1].uHigh); + if (RT_LIKELY(uCur != uPrev)) + { + int64_t offDelta = uCur - uPrev; + if (RT_LIKELY(offDelta >= 0)) + { + if (RT_LIKELY((uint64_t)offDelta < offJumpThreshold)) + { + if ((uint64_t)offDelta < offMinIncr) + offMinIncr = offDelta; + if ((uint64_t)offDelta > offMaxIncr && i != 0) + offMaxIncr = offDelta; + } + else + { + cJumps++; + if ((uint64_t)offDelta > offMaxJump) + offMaxJump = offDelta; + RTPrintf("%u/%u: Jump: %#010x`%08x -> %#010x`%08x\n", cOuterLoops, iLoop, + (unsigned)g_aRdTscResults[i].uHigh, (unsigned)g_aRdTscResults[i].uLow, + (unsigned)g_aRdTscResults[i + 1].uHigh, (unsigned)g_aRdTscResults[i + 1].uLow); + } + } + else + { + cBackwards++; + RTPrintf("%u/%u: Back: %#010x`%08x -> %#010x`%08x\n", cOuterLoops, iLoop, + (unsigned)g_aRdTscResults[i].uHigh, (unsigned)g_aRdTscResults[i].uLow, + (unsigned)g_aRdTscResults[i + 1].uHigh, (unsigned)g_aRdTscResults[i + 1].uLow); + } + } + else + { + cSame++; + RTPrintf("%u/%u: Same: %#010x`%08x -> %#010x`%08x\n", cOuterLoops, iLoop, + (unsigned)g_aRdTscResults[i].uHigh, (unsigned)g_aRdTscResults[i].uLow, + (unsigned)g_aRdTscResults[i + 1].uHigh, (unsigned)g_aRdTscResults[i + 1].uLow); + } +#if ARCH_BITS == 64 + if ((g_aRdTscResults[i + 1].uLow >> 32) || (g_aRdTscResults[i + 1].uHigh >> 32)) + cBadValues++; +#endif + } + + /* Copy the last value for the next iteration. */ + g_aRdTscResults[0] = g_aRdTscResults[cResults]; + + /* Display status. */ + if (RT_LIKELY(--cLoopsToNextStatus > 0)) + { /* likely */ } + else + { + cLoopsToNextStatus = cStatusEvery; + RTPrintf("%u/%u: %#010x`%08x\n", cOuterLoops, iLoop, + (unsigned)g_aRdTscResults[cResults].uHigh, (unsigned)g_aRdTscResults[cResults].uLow); + } + } + + /* + * Check minimum number of seconds. + */ + cOuterLoops++; + if (!cMinSeconds) + break; + uint64_t nsElapsed = RTTimeNanoTS() - nsTsStart; + if (nsElapsed >= cMinSeconds * RT_NS_1SEC_64) + break; + } + + /* + * Summary. + */ + if (cBackwards == 0 && cSame == 0 && cJumps == 0 && cBadValues == 0) + { + RTPrintf("rdtsc: Success (%u RDTSC over %u*%u loops, deltas: %#x`%08x..%#x`%08x)\n", + cRdTscInstructions, cOuterLoops, cMaxLoops, + (unsigned)(offMinIncr >> 32), (unsigned)offMinIncr, (unsigned)(offMaxIncr >> 32), (unsigned)offMaxIncr); + return RTEXITCODE_SUCCESS; + } + RTPrintf("RDTSC instructions: %u\n", cRdTscInstructions); + RTPrintf("Loops: %u * %u => %u\n", cMaxLoops, cOuterLoops, cOuterLoops * cMaxLoops); + RTPrintf("Backwards: %u\n", cBackwards); + RTPrintf("Jumps: %u\n", cJumps); + RTPrintf("Max jumps: %#010x`%08x\n", (unsigned)(offMaxJump >> 32), (unsigned)offMaxJump); + RTPrintf("Same value: %u\n", cSame); + RTPrintf("Bad values: %u\n", cBadValues); + RTPrintf("Min increment: %#010x`%08x\n", (unsigned)(offMinIncr >> 32), (unsigned)offMinIncr); + RTPrintf("Max increment: %#010x`%08x\n", (unsigned)(offMaxIncr >> 32), (unsigned)offMaxIncr); + return RTEXITCODE_FAILURE; +} + diff --git a/src/VBox/ValidationKit/utils/cpu/xmmsaving-asm.asm b/src/VBox/ValidationKit/utils/cpu/xmmsaving-asm.asm new file mode 100644 index 00000000..49a29e7a --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/xmmsaving-asm.asm @@ -0,0 +1,162 @@ +; $Id: xmmsaving-asm.asm $ +;; @file +; xmmsaving - assembly helpers. +; + +; +; Copyright (C) 2009-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 <https://www.gnu.org/licenses>. +; +; The contents of this file may alternatively be used under the terms +; of the Common Development and Distribution License Version 1.0 +; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +; in the VirtualBox distribution, in which case the provisions of the +; CDDL are applicable instead of those of the GPL. +; +; You may elect to license modified versions of this file under the +; terms and conditions of either the GPL or the CDDL or both. +; +; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +; + + +%include "iprt/asmdefs.mac" +%include "VBox/vmm/stam.mac" + + +BEGINCODE + + +;; +; DECLASM(int) XmmSavingTestLoadSet(const MYXMMREGSET *pSet, const MYXMMREGSET *pPrevSet, PRTUINT128U pBadVal); +; +; @returns 0 on success, 1-based register number on failure. +; @param pSet The new set. +; @param pPrevSet The previous set. Can be NULL. +; @param pBadVal Where to store the actual register value on failure. +; +BEGINPROC XmmSavingTestLoadSet + push xBP + mov xBP, xSP + sub xSP, 32 ; Space for storing an XMM register (in TEST_REG). + and xSP, ~31 ; Align it. + + ; Unify register/arguments. +%ifdef ASM_CALL64_GCC + mov r8, rdx ; pBadVal + mov xCX, rdi ; pSet + mov xDX, rsi ; pPrevSet +%endif +%ifdef RT_ARCH_X86 + mov xCX, [ebp + 8] ; pSet + mov xDX, [ebp + 12] ; pPrevSet +%endif + + test xDX, xDX + jz near .just_load + + ; Check that the old set is still correct. +%macro TEST_REG 1, + movdqa [xSP], xmm %+ %1 + mov xAX, [xDX + %1 * 8] + cmp [xSP], xAX + jne %%bad + mov xAX, [xDX + %1 * 8 + xCB] + cmp [xSP + xCB], xAX +%ifdef RT_ARCH_X86 + jne %%bad + mov xAX, [xDX + %1 * 8 + xCB*2] + cmp [xSP + xCB*2], xAX + jne %%bad + mov xAX, [xDX + %1 * 8 + xCB*3] + cmp [xSP + xCB*3], xAX +%endif + je %%next +%%bad: + mov eax, %1 + 1 + jmp .return_copy_badval +%%next: +%endmacro + + TEST_REG 0 + TEST_REG 1 + TEST_REG 2 + TEST_REG 3 + TEST_REG 4 + TEST_REG 5 + TEST_REG 6 + TEST_REG 7 +%ifdef RT_ARCH_AMD64 + TEST_REG 8 + TEST_REG 9 + TEST_REG 10 + TEST_REG 11 + TEST_REG 12 + TEST_REG 13 + TEST_REG 14 + TEST_REG 15 +%endif + + ; Load the new state. +.just_load: + movdqu xmm0, [xCX + 0*8] + movdqu xmm1, [xCX + 1*8] + movdqu xmm2, [xCX + 2*8] + movdqu xmm3, [xCX + 3*8] + movdqu xmm4, [xCX + 4*8] + movdqu xmm5, [xCX + 5*8] + movdqu xmm6, [xCX + 6*8] + movdqu xmm7, [xCX + 7*8] +%ifdef RT_ARCH_AMD64 + movdqu xmm8, [xCX + 8*8] + movdqu xmm9, [xCX + 9*8] + movdqu xmm10, [xCX + 10*8] + movdqu xmm11, [xCX + 11*8] + movdqu xmm12, [xCX + 12*8] + movdqu xmm13, [xCX + 13*8] + movdqu xmm14, [xCX + 14*8] + movdqu xmm15, [xCX + 15*8] +%endif + xor eax, eax + jmp .return + +.return_copy_badval: + ; don't touch eax here. +%ifdef RT_ARCH_X86 + mov edx, [ebp + 16] + mov ecx, [esp] + mov [edx ], ecx + mov ecx, [esp + 4] + mov [edx + 4], ecx + mov ecx, [esp + 8] + mov [edx + 8], ecx + mov ecx, [esp + 12] + mov [edx + 12], ecx +%else + mov rdx, [rsp] + mov rcx, [rsp + 8] + mov [r8], rdx + mov [r8 + 8], rcx +%endif + jmp .return + +.return: + leave + ret +ENDPROC XmmSavingTestLoadSet + diff --git a/src/VBox/ValidationKit/utils/cpu/xmmsaving.cpp b/src/VBox/ValidationKit/utils/cpu/xmmsaving.cpp new file mode 100644 index 00000000..a8d377f8 --- /dev/null +++ b/src/VBox/ValidationKit/utils/cpu/xmmsaving.cpp @@ -0,0 +1,130 @@ +/* $Id: xmmsaving.cpp $ */ +/** @file + * xmmsaving - Test that all XMM register state is handled correctly and + * not corrupted the VMM. + */ + +/* + * Copyright (C) 2009-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/test.h> +#include <iprt/x86.h> + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef struct MYXMMREGSET +{ + RTUINT128U aRegs[16]; +} MYXMMREGSET; + + +DECLASM(int) XmmSavingTestLoadSet(const MYXMMREGSET *pSet, const MYXMMREGSET *pPrevSet, PRTUINT128U pBadVal); + + +static void XmmSavingTest(void) +{ + RTTestISub("xmm saving and restoring"); + + /* Create the test sets. */ + static MYXMMREGSET s_aSets[256]; + for (unsigned s = 0; s < RT_ELEMENTS(s_aSets); s++) + { + for (unsigned r = 0; r < RT_ELEMENTS(s_aSets[s].aRegs); r++) + { + unsigned x = (s << 4) | r; + s_aSets[s].aRegs[r].au32[0] = x | UINT32_C(0x12345000); + s_aSets[s].aRegs[r].au32[1] = (x << 8) | UINT32_C(0x88700011); + s_aSets[s].aRegs[r].au32[2] = (x << 16) | UINT32_C(0xe000dcba); + s_aSets[s].aRegs[r].au32[3] = (x << 20) | UINT32_C(0x00087654); + } + } + + /* Do the actual testing. */ + const MYXMMREGSET *pPrev2 = NULL; + const MYXMMREGSET *pPrev = NULL; + for (int i = 0; i < 1000000; i++) + { + if ((i % 50000) == 0) + { + RTTestIPrintf(RTTESTLVL_ALWAYS, "."); + pPrev = pPrev2 = NULL; /* May be trashed by the above call. */ + } + for (unsigned s = 0; s < RT_ELEMENTS(s_aSets); s++) + { + RTUINT128U BadVal; + const MYXMMREGSET *pSet = &s_aSets[s]; + int r = XmmSavingTestLoadSet(pSet, pPrev, &BadVal); + if (r-- != 0) + { + RTTestIFailed("i=%d s=%d r=%d", i, s, r); + RTTestIFailureDetails("XMM%-2d = %08x,%08x,%08x,%08x\n", + r, + BadVal.au32[0], + BadVal.au32[1], + BadVal.au32[2], + BadVal.au32[3]); + RTTestIFailureDetails("Expected %08x,%08x,%08x,%08x\n", + pPrev->aRegs[r].au32[0], + pPrev->aRegs[r].au32[1], + pPrev->aRegs[r].au32[2], + pPrev->aRegs[r].au32[3]); + if (pPrev2) + RTTestIFailureDetails("PrevPrev %08x,%08x,%08x,%08x\n", + pPrev2->aRegs[r].au32[0], + pPrev2->aRegs[r].au32[1], + pPrev2->aRegs[r].au32[2], + pPrev2->aRegs[r].au32[3]); + return; + } + pPrev2 = pPrev; + pPrev = pSet; + } + } + RTTestISubDone(); +} + + +int main() +{ + RTTEST hTest; + int rc = RTTestInitAndCreate("xmmsaving", &hTest); + if (rc) + return rc; + XmmSavingTest(); + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/ValidationKit/utils/dos/DosSleep.c b/src/VBox/ValidationKit/utils/dos/DosSleep.c new file mode 100644 index 00000000..f18c5fe9 --- /dev/null +++ b/src/VBox/ValidationKit/utils/dos/DosSleep.c @@ -0,0 +1,67 @@ +/* $Id: DosSleep.c $ */ +/** @file + * 16-bit DOS sleep program. + * + * Build: wcl -I%WATCOM%\h\win -l=dos -k4096 -fm -W4 DosSleep.c + */ + +/* + * Copyright (C) 2018-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + + + +int main(int argc, char **argv) +{ + int iExit; + + if (argc == 2) + { + int cSeconds = atoi(argv[1]); + sleep(cSeconds); + iExit = 0; + } + else + { + fprintf(stderr, "syntax error: only expected a number of seconds\n"); + iExit = 4; + } + + return iExit; +} + diff --git a/src/VBox/ValidationKit/utils/dos/DosVmOff.asm b/src/VBox/ValidationKit/utils/dos/DosVmOff.asm new file mode 100644 index 00000000..1958f9ce --- /dev/null +++ b/src/VBox/ValidationKit/utils/dos/DosVmOff.asm @@ -0,0 +1,79 @@ +; $Id: DosVmOff.asm $ +;; @file +; 16-bit DOS COM program that powers off the VM. +; +; Build: yasm -f bin -i../../../../../include/ DosVmOff.asm -o DosVmOff.com +; + +; +; Copyright (C) 2018-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 <https://www.gnu.org/licenses>. +; +; The contents of this file may alternatively be used under the terms +; of the Common Development and Distribution License Version 1.0 +; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +; in the VirtualBox distribution, in which case the provisions of the +; CDDL are applicable instead of those of the GPL. +; +; You may elect to license modified versions of this file under the +; terms and conditions of either the GPL or the CDDL or both. +; +; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +; + + + +%include "VBox/bios.mac" + + org 100h + +segment text +main: +%if 0 + ; Setup stack. + mov ax, stack + mov ss, ax + mov sp, top_of_stack +%endif + + ; Do the shutdown thing. + mov ax, cs + mov ds, ax + + mov bl, 64 + mov dx, VBOX_BIOS_SHUTDOWN_PORT + mov ax, VBOX_BIOS_OLD_SHUTDOWN_PORT +.retry: + mov cx, 8 + mov si, .s_szShutdown + rep outsb + xchg ax, dx ; alternate between the new (VBox) and old (Bochs) ports. + dec bl + jnz .retry + + + ; Probably not a VBox VM, exit the program with errorlevel 1. +.whatever: + mov ax, 04c01h + int 021h + hlt + jmp .whatever + +.s_szShutdown: + db 'Shutdown', 0 + diff --git a/src/VBox/ValidationKit/utils/dos/WinExit.asm b/src/VBox/ValidationKit/utils/dos/WinExit.asm new file mode 100644 index 00000000..1705d261 --- /dev/null +++ b/src/VBox/ValidationKit/utils/dos/WinExit.asm @@ -0,0 +1,95 @@ +; $Id: WinExit.asm $ +;; @file +; 16-bit windows program that exits windows. +; +; Build: wcl -I%WATCOM%\h\win -l=windows -k4096 -fm WinExit.asm +; + +; +; Copyright (C) 2018-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 <https://www.gnu.org/licenses>. +; +; The contents of this file may alternatively be used under the terms +; of the Common Development and Distribution License Version 1.0 +; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +; in the VirtualBox distribution, in which case the provisions of the +; CDDL are applicable instead of those of the GPL. +; +; You may elect to license modified versions of this file under the +; terms and conditions of either the GPL or the CDDL or both. +; +; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +; + + + +;.stack 4096 +STACK segment para stack 'STACK' +STACK ends + + +extrn INITTASK:FAR +extrn INITAPP:FAR +extrn EXITWINDOWS:FAR +extrn WAITEVENT:FAR + +_TEXT segment word public 'CODE' +start: + push bp + mov bp, sp + + ; + ; Initialize the windows app. + ; + call INITTASK + + xor ax, ax + push ax + call WAITEVENT + + push di ; hInstance + push di + call INITAPP + + ; + ; Do what we're here for, exitting windows. + ; + xor ax, ax + xor cx, cx + xor dx, dx + push ax + push ax + push ax + push ax + call EXITWINDOWS + + ; + ; Exit via DOS interrupt. + ; + xor al, al + mov ah,04cH + int 021h + + mov sp, bp + pop bp + ret + +_TEXT ends + +end start + diff --git a/src/VBox/ValidationKit/utils/fs/FsPerf.cpp b/src/VBox/ValidationKit/utils/fs/FsPerf.cpp new file mode 100644 index 00000000..ae8b7d91 --- /dev/null +++ b/src/VBox/ValidationKit/utils/fs/FsPerf.cpp @@ -0,0 +1,6834 @@ +/* $Id: FsPerf.cpp $ */ +/** @file + * FsPerf - File System (Shared Folders) Performance Benchmark. + */ + +/* + * Copyright (C) 2019-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#ifdef RT_OS_OS2 +# define INCL_BASE +# include <os2.h> +# undef RT_MAX +#endif +#include <iprt/alloca.h> +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/err.h> +#include <iprt/dir.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/list.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/param.h> +#include <iprt/path.h> +#ifdef RT_OS_LINUX +# include <iprt/pipe.h> +#endif +#include <iprt/process.h> +#include <iprt/rand.h> +#include <iprt/string.h> +#include <iprt/stream.h> +#include <iprt/system.h> +#include <iprt/tcp.h> +#include <iprt/test.h> +#include <iprt/time.h> +#include <iprt/thread.h> +#include <iprt/zero.h> + +#ifdef RT_OS_WINDOWS +# include <iprt/nt/nt-and-windows.h> +#else +# include <errno.h> +# include <unistd.h> +# include <limits.h> +# include <sys/types.h> +# include <sys/fcntl.h> +# ifndef RT_OS_OS2 +# include <sys/mman.h> +# include <sys/uio.h> +# endif +# include <sys/socket.h> +# include <signal.h> +# ifdef RT_OS_LINUX +# include <sys/sendfile.h> +# include <sys/syscall.h> +# endif +# ifdef RT_OS_DARWIN +# include <sys/uio.h> +# endif +#endif + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Used for cutting the the -d parameter value short and avoid a number of buffer overflow checks. */ +#define FSPERF_MAX_NEEDED_PATH 224 +/** The max path used by this code. + * It greatly exceeds the RTPATH_MAX so we can push the limits on windows. */ +#define FSPERF_MAX_PATH (_32K) + +/** EOF marker character used by the master/slave comms. */ +#define FSPERF_EOF 0x1a +/** EOF marker character used by the master/slave comms, string version. */ +#define FSPERF_EOF_STR "\x1a" + +/** @def FSPERF_TEST_SENDFILE + * Whether to enable the sendfile() tests. */ +#if defined(RT_OS_LINUX) || defined(RT_OS_DARWIN) +# define FSPERF_TEST_SENDFILE +#endif + +/** + * Macro for profiling @a a_fnCall (typically forced inline) for about @a a_cNsTarget ns. + * + * Always does an even number of iterations. + */ +#define PROFILE_FN(a_fnCall, a_cNsTarget, a_szDesc) \ + do { \ + /* Estimate how many iterations we need to fill up the given timeslot: */ \ + fsPerfYield(); \ + uint64_t nsStart = RTTimeNanoTS(); \ + uint64_t nsPrf; \ + do \ + nsPrf = RTTimeNanoTS(); \ + while (nsPrf == nsStart); \ + nsStart = nsPrf; \ + \ + uint64_t iIteration = 0; \ + do \ + { \ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + iIteration++; \ + nsPrf = RTTimeNanoTS() - nsStart; \ + } while (nsPrf < RT_NS_10MS || (iIteration & 1)); \ + nsPrf /= iIteration; \ + if (nsPrf > g_nsPerNanoTSCall + 32) \ + nsPrf -= g_nsPerNanoTSCall; \ + \ + uint64_t cIterations = (a_cNsTarget) / nsPrf; \ + if (cIterations <= 1) \ + cIterations = 2; \ + else if (cIterations & 1) \ + cIterations++; \ + \ + /* Do the actual profiling: */ \ + fsPerfYield(); \ + iIteration = 0; \ + nsStart = RTTimeNanoTS(); \ + for (; iIteration < cIterations; iIteration++) \ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + nsPrf = RTTimeNanoTS() - nsStart; \ + RTTestIValue(a_szDesc, nsPrf / cIterations, RTTESTUNIT_NS_PER_OCCURRENCE); \ + if (g_fShowDuration) \ + RTTestIValueF(nsPrf, RTTESTUNIT_NS, "%s duration", a_szDesc); \ + if (g_fShowIterations) \ + RTTestIValueF(iIteration, RTTESTUNIT_OCCURRENCES, "%s iterations", a_szDesc); \ + } while (0) + + +/** + * Macro for profiling an operation on each file in the manytree directory tree. + * + * Always does an even number of tree iterations. + */ +#define PROFILE_MANYTREE_FN(a_szPath, a_fnCall, a_cEstimationIterations, a_cNsTarget, a_szDesc) \ + do { \ + if (!g_fManyFiles) \ + break; \ + \ + /* Estimate how many iterations we need to fill up the given timeslot: */ \ + fsPerfYield(); \ + uint64_t nsStart = RTTimeNanoTS(); \ + uint64_t ns; \ + do \ + ns = RTTimeNanoTS(); \ + while (ns == nsStart); \ + nsStart = ns; \ + \ + PFSPERFNAMEENTRY pCur; \ + uint64_t iIteration = 0; \ + do \ + { \ + RTListForEach(&g_ManyTreeHead, pCur, FSPERFNAMEENTRY, Entry) \ + { \ + memcpy(a_szPath, pCur->szName, pCur->cchName); \ + for (uint32_t i = 0; i < g_cManyTreeFilesPerDir; i++) \ + { \ + RTStrFormatU32(&a_szPath[pCur->cchName], sizeof(a_szPath) - pCur->cchName, i, 10, 5, 5, RTSTR_F_ZEROPAD); \ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + } \ + } \ + iIteration++; \ + ns = RTTimeNanoTS() - nsStart; \ + } while (ns < RT_NS_10MS || (iIteration & 1)); \ + ns /= iIteration; \ + if (ns > g_nsPerNanoTSCall + 32) \ + ns -= g_nsPerNanoTSCall; \ + \ + uint32_t cIterations = (a_cNsTarget) / ns; \ + if (cIterations <= 1) \ + cIterations = 2; \ + else if (cIterations & 1) \ + cIterations++; \ + \ + /* Do the actual profiling: */ \ + fsPerfYield(); \ + uint32_t cCalls = 0; \ + nsStart = RTTimeNanoTS(); \ + for (iIteration = 0; iIteration < cIterations; iIteration++) \ + { \ + RTListForEach(&g_ManyTreeHead, pCur, FSPERFNAMEENTRY, Entry) \ + { \ + memcpy(a_szPath, pCur->szName, pCur->cchName); \ + for (uint32_t i = 0; i < g_cManyTreeFilesPerDir; i++) \ + { \ + RTStrFormatU32(&a_szPath[pCur->cchName], sizeof(a_szPath) - pCur->cchName, i, 10, 5, 5, RTSTR_F_ZEROPAD); \ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + cCalls++; \ + } \ + } \ + } \ + ns = RTTimeNanoTS() - nsStart; \ + RTTestIValueF(ns / cCalls, RTTESTUNIT_NS_PER_OCCURRENCE, a_szDesc); \ + if (g_fShowDuration) \ + RTTestIValueF(ns, RTTESTUNIT_NS, "%s duration", a_szDesc); \ + if (g_fShowIterations) \ + RTTestIValueF(iIteration, RTTESTUNIT_OCCURRENCES, "%s iterations", a_szDesc); \ + } while (0) + + +/** + * Execute a_fnCall for each file in the manytree. + */ +#define DO_MANYTREE_FN(a_szPath, a_fnCall) \ + do { \ + PFSPERFNAMEENTRY pCur; \ + RTListForEach(&g_ManyTreeHead, pCur, FSPERFNAMEENTRY, Entry) \ + { \ + memcpy(a_szPath, pCur->szName, pCur->cchName); \ + for (uint32_t i = 0; i < g_cManyTreeFilesPerDir; i++) \ + { \ + RTStrFormatU32(&a_szPath[pCur->cchName], sizeof(a_szPath) - pCur->cchName, i, 10, 5, 5, RTSTR_F_ZEROPAD); \ + a_fnCall; \ + } \ + } \ + } while (0) + + +/** @def FSPERF_VERR_PATH_NOT_FOUND + * Hides the fact that we only get VERR_PATH_NOT_FOUND on non-unix systems. */ +#if defined(RT_OS_WINDOWS) //|| defined(RT_OS_OS2) - using posix APIs IIRC, so lost in translation. +# define FSPERF_VERR_PATH_NOT_FOUND VERR_PATH_NOT_FOUND +#else +# define FSPERF_VERR_PATH_NOT_FOUND VERR_FILE_NOT_FOUND +#endif + +#ifdef RT_OS_WINDOWS +/** @def CHECK_WINAPI + * Checks a windows API call, reporting the last error on failure. */ +# define CHECK_WINAPI_CALL(a_CallAndTestExpr) \ + if (!(a_CallAndTestExpr)) { \ + RTTestIFailed("line %u: %s failed - last error %u, last status %#x", \ + __LINE__, #a_CallAndTestExpr, GetLastError(), RTNtLastStatusValue()); \ + } else do {} while (0) +#endif + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef struct FSPERFNAMEENTRY +{ + RTLISTNODE Entry; + uint16_t cchName; + RT_FLEXIBLE_ARRAY_EXTENSION + char szName[RT_FLEXIBLE_ARRAY]; +} FSPERFNAMEENTRY; +typedef FSPERFNAMEENTRY *PFSPERFNAMEENTRY; + + +enum +{ + kCmdOpt_First = 128, + + kCmdOpt_ManyFiles = kCmdOpt_First, + kCmdOpt_NoManyFiles, + kCmdOpt_Open, + kCmdOpt_NoOpen, + kCmdOpt_FStat, + kCmdOpt_NoFStat, +#ifdef RT_OS_WINDOWS + kCmdOpt_NtQueryInfoFile, + kCmdOpt_NoNtQueryInfoFile, + kCmdOpt_NtQueryVolInfoFile, + kCmdOpt_NoNtQueryVolInfoFile, +#endif + kCmdOpt_FChMod, + kCmdOpt_NoFChMod, + kCmdOpt_FUtimes, + kCmdOpt_NoFUtimes, + kCmdOpt_Stat, + kCmdOpt_NoStat, + kCmdOpt_ChMod, + kCmdOpt_NoChMod, + kCmdOpt_Utimes, + kCmdOpt_NoUtimes, + kCmdOpt_Rename, + kCmdOpt_NoRename, + kCmdOpt_DirOpen, + kCmdOpt_NoDirOpen, + kCmdOpt_DirEnum, + kCmdOpt_NoDirEnum, + kCmdOpt_MkRmDir, + kCmdOpt_NoMkRmDir, + kCmdOpt_StatVfs, + kCmdOpt_NoStatVfs, + kCmdOpt_Rm, + kCmdOpt_NoRm, + kCmdOpt_ChSize, + kCmdOpt_NoChSize, + kCmdOpt_ReadPerf, + kCmdOpt_NoReadPerf, + kCmdOpt_ReadTests, + kCmdOpt_NoReadTests, +#ifdef FSPERF_TEST_SENDFILE + kCmdOpt_SendFile, + kCmdOpt_NoSendFile, +#endif +#ifdef RT_OS_LINUX + kCmdOpt_Splice, + kCmdOpt_NoSplice, +#endif + kCmdOpt_WritePerf, + kCmdOpt_NoWritePerf, + kCmdOpt_WriteTests, + kCmdOpt_NoWriteTests, + kCmdOpt_Seek, + kCmdOpt_NoSeek, + kCmdOpt_FSync, + kCmdOpt_NoFSync, + kCmdOpt_MMap, + kCmdOpt_NoMMap, + kCmdOpt_MMapCoherency, + kCmdOpt_NoMMapCoherency, + kCmdOpt_MMapPlacement, + kCmdOpt_IgnoreNoCache, + kCmdOpt_NoIgnoreNoCache, + kCmdOpt_IoFileSize, + kCmdOpt_SetBlockSize, + kCmdOpt_AddBlockSize, + kCmdOpt_Copy, + kCmdOpt_NoCopy, + kCmdOpt_Remote, + kCmdOpt_NoRemote, + + kCmdOpt_ShowDuration, + kCmdOpt_NoShowDuration, + kCmdOpt_ShowIterations, + kCmdOpt_NoShowIterations, + + kCmdOpt_ManyTreeFilesPerDir, + kCmdOpt_ManyTreeSubdirsPerDir, + kCmdOpt_ManyTreeDepth, + + kCmdOpt_MaxBufferSize, + + kCmdOpt_End +}; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Command line parameters */ +static const RTGETOPTDEF g_aCmdOptions[] = +{ + { "--dir", 'd', RTGETOPT_REQ_STRING }, + { "--relative-dir", 'r', RTGETOPT_REQ_NOTHING }, + { "--comms-dir", 'c', RTGETOPT_REQ_STRING }, + { "--comms-slave", 'C', RTGETOPT_REQ_NOTHING }, + { "--seconds", 's', RTGETOPT_REQ_UINT32 }, + { "--milliseconds", 'm', RTGETOPT_REQ_UINT64 }, + + { "--enable-all", 'e', RTGETOPT_REQ_NOTHING }, + { "--disable-all", 'z', RTGETOPT_REQ_NOTHING }, + + { "--many-files", kCmdOpt_ManyFiles, RTGETOPT_REQ_UINT32 }, + { "--no-many-files", kCmdOpt_NoManyFiles, RTGETOPT_REQ_NOTHING }, + { "--files-per-dir", kCmdOpt_ManyTreeFilesPerDir, RTGETOPT_REQ_UINT32 }, + { "--subdirs-per-dir", kCmdOpt_ManyTreeSubdirsPerDir, RTGETOPT_REQ_UINT32 }, + { "--tree-depth", kCmdOpt_ManyTreeDepth, RTGETOPT_REQ_UINT32 }, + { "--max-buffer-size", kCmdOpt_MaxBufferSize, RTGETOPT_REQ_UINT32 }, + { "--mmap-placement", kCmdOpt_MMapPlacement, RTGETOPT_REQ_STRING }, + /// @todo { "--timestamp-style", kCmdOpt_TimestampStyle, RTGETOPT_REQ_STRING }, + + { "--open", kCmdOpt_Open, RTGETOPT_REQ_NOTHING }, + { "--no-open", kCmdOpt_NoOpen, RTGETOPT_REQ_NOTHING }, + { "--fstat", kCmdOpt_FStat, RTGETOPT_REQ_NOTHING }, + { "--no-fstat", kCmdOpt_NoFStat, RTGETOPT_REQ_NOTHING }, +#ifdef RT_OS_WINDOWS + { "--nt-query-info-file", kCmdOpt_NtQueryInfoFile, RTGETOPT_REQ_NOTHING }, + { "--no-nt-query-info-file", kCmdOpt_NoNtQueryInfoFile, RTGETOPT_REQ_NOTHING }, + { "--nt-query-vol-info-file", kCmdOpt_NtQueryVolInfoFile, RTGETOPT_REQ_NOTHING }, + { "--no-nt-query-vol-info-file",kCmdOpt_NoNtQueryVolInfoFile, RTGETOPT_REQ_NOTHING }, +#endif + { "--fchmod", kCmdOpt_FChMod, RTGETOPT_REQ_NOTHING }, + { "--no-fchmod", kCmdOpt_NoFChMod, RTGETOPT_REQ_NOTHING }, + { "--futimes", kCmdOpt_FUtimes, RTGETOPT_REQ_NOTHING }, + { "--no-futimes", kCmdOpt_NoFUtimes, RTGETOPT_REQ_NOTHING }, + { "--stat", kCmdOpt_Stat, RTGETOPT_REQ_NOTHING }, + { "--no-stat", kCmdOpt_NoStat, RTGETOPT_REQ_NOTHING }, + { "--chmod", kCmdOpt_ChMod, RTGETOPT_REQ_NOTHING }, + { "--no-chmod", kCmdOpt_NoChMod, RTGETOPT_REQ_NOTHING }, + { "--utimes", kCmdOpt_Utimes, RTGETOPT_REQ_NOTHING }, + { "--no-utimes", kCmdOpt_NoUtimes, RTGETOPT_REQ_NOTHING }, + { "--rename", kCmdOpt_Rename, RTGETOPT_REQ_NOTHING }, + { "--no-rename", kCmdOpt_NoRename, RTGETOPT_REQ_NOTHING }, + { "--dir-open", kCmdOpt_DirOpen, RTGETOPT_REQ_NOTHING }, + { "--no-dir-open", kCmdOpt_NoDirOpen, RTGETOPT_REQ_NOTHING }, + { "--dir-enum", kCmdOpt_DirEnum, RTGETOPT_REQ_NOTHING }, + { "--no-dir-enum", kCmdOpt_NoDirEnum, RTGETOPT_REQ_NOTHING }, + { "--mk-rm-dir", kCmdOpt_MkRmDir, RTGETOPT_REQ_NOTHING }, + { "--no-mk-rm-dir", kCmdOpt_NoMkRmDir, RTGETOPT_REQ_NOTHING }, + { "--stat-vfs", kCmdOpt_StatVfs, RTGETOPT_REQ_NOTHING }, + { "--no-stat-vfs", kCmdOpt_NoStatVfs, RTGETOPT_REQ_NOTHING }, + { "--rm", kCmdOpt_Rm, RTGETOPT_REQ_NOTHING }, + { "--no-rm", kCmdOpt_NoRm, RTGETOPT_REQ_NOTHING }, + { "--chsize", kCmdOpt_ChSize, RTGETOPT_REQ_NOTHING }, + { "--no-chsize", kCmdOpt_NoChSize, RTGETOPT_REQ_NOTHING }, + { "--read-tests", kCmdOpt_ReadTests, RTGETOPT_REQ_NOTHING }, + { "--no-read-tests", kCmdOpt_NoReadTests, RTGETOPT_REQ_NOTHING }, + { "--read-perf", kCmdOpt_ReadPerf, RTGETOPT_REQ_NOTHING }, + { "--no-read-perf", kCmdOpt_NoReadPerf, RTGETOPT_REQ_NOTHING }, +#ifdef FSPERF_TEST_SENDFILE + { "--sendfile", kCmdOpt_SendFile, RTGETOPT_REQ_NOTHING }, + { "--no-sendfile", kCmdOpt_NoSendFile, RTGETOPT_REQ_NOTHING }, +#endif +#ifdef RT_OS_LINUX + { "--splice", kCmdOpt_Splice, RTGETOPT_REQ_NOTHING }, + { "--no-splice", kCmdOpt_NoSplice, RTGETOPT_REQ_NOTHING }, +#endif + { "--write-tests", kCmdOpt_WriteTests, RTGETOPT_REQ_NOTHING }, + { "--no-write-tests", kCmdOpt_NoWriteTests, RTGETOPT_REQ_NOTHING }, + { "--write-perf", kCmdOpt_WritePerf, RTGETOPT_REQ_NOTHING }, + { "--no-write-perf", kCmdOpt_NoWritePerf, RTGETOPT_REQ_NOTHING }, + { "--seek", kCmdOpt_Seek, RTGETOPT_REQ_NOTHING }, + { "--no-seek", kCmdOpt_NoSeek, RTGETOPT_REQ_NOTHING }, + { "--fsync", kCmdOpt_FSync, RTGETOPT_REQ_NOTHING }, + { "--no-fsync", kCmdOpt_NoFSync, RTGETOPT_REQ_NOTHING }, + { "--mmap", kCmdOpt_MMap, RTGETOPT_REQ_NOTHING }, + { "--no-mmap", kCmdOpt_NoMMap, RTGETOPT_REQ_NOTHING }, + { "--mmap-coherency", kCmdOpt_MMapCoherency, RTGETOPT_REQ_NOTHING }, + { "--no-mmap-coherency", kCmdOpt_NoMMapCoherency, RTGETOPT_REQ_NOTHING }, + { "--ignore-no-cache", kCmdOpt_IgnoreNoCache, RTGETOPT_REQ_NOTHING }, + { "--no-ignore-no-cache", kCmdOpt_NoIgnoreNoCache, RTGETOPT_REQ_NOTHING }, + { "--io-file-size", kCmdOpt_IoFileSize, RTGETOPT_REQ_UINT64 }, + { "--set-block-size", kCmdOpt_SetBlockSize, RTGETOPT_REQ_UINT32 }, + { "--add-block-size", kCmdOpt_AddBlockSize, RTGETOPT_REQ_UINT32 }, + { "--copy", kCmdOpt_Copy, RTGETOPT_REQ_NOTHING }, + { "--no-copy", kCmdOpt_NoCopy, RTGETOPT_REQ_NOTHING }, + { "--remote", kCmdOpt_Remote, RTGETOPT_REQ_NOTHING }, + { "--no-remote", kCmdOpt_NoRemote, RTGETOPT_REQ_NOTHING }, + + { "--show-duration", kCmdOpt_ShowDuration, RTGETOPT_REQ_NOTHING }, + { "--no-show-duration", kCmdOpt_NoShowDuration, RTGETOPT_REQ_NOTHING }, + { "--show-iterations", kCmdOpt_ShowIterations, RTGETOPT_REQ_NOTHING }, + { "--no-show-iterations", kCmdOpt_NoShowIterations, RTGETOPT_REQ_NOTHING }, + + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + { "--version", 'V', RTGETOPT_REQ_NOTHING }, + { "--help", 'h', RTGETOPT_REQ_NOTHING } /* for Usage() */ +}; + +/** The test handle. */ +static RTTEST g_hTest; +/** The number of nanoseconds a RTTimeNanoTS call takes. + * This is used for adjusting loop count estimates. */ +static uint64_t g_nsPerNanoTSCall = 1; +/** Whether or not to display the duration of each profile run. + * This is chiefly for verify the estimate phase. */ +static bool g_fShowDuration = false; +/** Whether or not to display the iteration count for each profile run. + * This is chiefly for verify the estimate phase. */ +static bool g_fShowIterations = false; +/** Verbosity level. */ +static uint32_t g_uVerbosity = 0; +/** Max buffer size, UINT32_MAX for unlimited. + * This is for making sure we don't run into the MDL limit on windows, which + * a bit less than 64 MiB. */ +#if defined(RT_OS_WINDOWS) +static uint32_t g_cbMaxBuffer = _32M; +#else +static uint32_t g_cbMaxBuffer = UINT32_MAX; +#endif +/** When to place the mmap test. */ +static int g_iMMapPlacement = 0; + +/** @name Selected subtest + * @{ */ +static bool g_fManyFiles = true; +static bool g_fOpen = true; +static bool g_fFStat = true; +#ifdef RT_OS_WINDOWS +static bool g_fNtQueryInfoFile = true; +static bool g_fNtQueryVolInfoFile = true; +#endif +static bool g_fFChMod = true; +static bool g_fFUtimes = true; +static bool g_fStat = true; +static bool g_fChMod = true; +static bool g_fUtimes = true; +static bool g_fRename = true; +static bool g_fDirOpen = true; +static bool g_fDirEnum = true; +static bool g_fMkRmDir = true; +static bool g_fStatVfs = true; +static bool g_fRm = true; +static bool g_fChSize = true; +static bool g_fReadTests = true; +static bool g_fReadPerf = true; +#ifdef FSPERF_TEST_SENDFILE +static bool g_fSendFile = true; +#endif +#ifdef RT_OS_LINUX +static bool g_fSplice = true; +#endif +static bool g_fWriteTests = true; +static bool g_fWritePerf = true; +static bool g_fSeek = true; +static bool g_fFSync = true; +static bool g_fMMap = true; +static bool g_fMMapCoherency = true; +static bool g_fCopy = true; +static bool g_fRemote = true; +/** @} */ + +/** The length of each test run. */ +static uint64_t g_nsTestRun = RT_NS_1SEC_64 * 10; + +/** For the 'manyfiles' subdir. */ +static uint32_t g_cManyFiles = 10000; + +/** Number of files in the 'manytree' directory tree. */ +static uint32_t g_cManyTreeFiles = 640 + 16*640 /*10880*/; +/** Number of files per directory in the 'manytree' construct. */ +static uint32_t g_cManyTreeFilesPerDir = 640; +/** Number of subdirs per directory in the 'manytree' construct. */ +static uint32_t g_cManyTreeSubdirsPerDir = 16; +/** The depth of the 'manytree' directory tree. */ +static uint32_t g_cManyTreeDepth = 1; +/** List of directories in the many tree, creation order. */ +static RTLISTANCHOR g_ManyTreeHead; + +/** Number of configured I/O block sizes. */ +static uint32_t g_cIoBlocks = 8; +/** Configured I/O block sizes. */ +static uint32_t g_acbIoBlocks[16] = { 1, 512, 4096, 16384, 65536, _1M, _32M, _128M }; +/** The desired size of the test file we use for I/O. */ +static uint64_t g_cbIoFile = _512M; +/** Whether to be less strict with non-cache file handle. */ +static bool g_fIgnoreNoCache = false; + +/** Set if g_szDir and friends are path relative to CWD rather than absolute. */ +static bool g_fRelativeDir = false; +/** The length of g_szDir. */ +static size_t g_cchDir; +/** The length of g_szEmptyDir. */ +static size_t g_cchEmptyDir; +/** The length of g_szDeepDir. */ +static size_t g_cchDeepDir; + +/** The length of g_szCommsDir. */ +static size_t g_cchCommsDir; +/** The length of g_szCommsSubDir. */ +static size_t g_cchCommsSubDir; + +/** The test directory (absolute). This will always have a trailing slash. */ +static char g_szDir[FSPERF_MAX_PATH]; +/** The test directory (absolute), 2nd copy for use with InDir2(). */ +static char g_szDir2[FSPERF_MAX_PATH]; +/** The empty test directory (absolute). This will always have a trailing slash. */ +static char g_szEmptyDir[FSPERF_MAX_PATH]; +/** The deep test directory (absolute). This will always have a trailing slash. */ +static char g_szDeepDir[FSPERF_MAX_PATH + _1K]; + +/** The communcations directory. This will always have a trailing slash. */ +static char g_szCommsDir[FSPERF_MAX_PATH]; +/** The communcations subdirectory used for the actual communication. This will + * always have a trailing slash. */ +static char g_szCommsSubDir[FSPERF_MAX_PATH]; + +/** + * Yield the CPU and stuff before starting a test run. + */ +DECLINLINE(void) fsPerfYield(void) +{ + RTThreadYield(); + RTThreadYield(); +} + + +/** + * Profiles the RTTimeNanoTS call, setting g_nsPerNanoTSCall. + */ +static void fsPerfNanoTS(void) +{ + fsPerfYield(); + + /* Make sure we start off on a changing timestamp on platforms will low time resoultion. */ + uint64_t nsStart = RTTimeNanoTS(); + uint64_t ns; + do + ns = RTTimeNanoTS(); + while (ns == nsStart); + nsStart = ns; + + /* Call it for 10 ms. */ + uint32_t i = 0; + do + { + i++; + ns = RTTimeNanoTS(); + } + while (ns - nsStart < RT_NS_10MS); + + g_nsPerNanoTSCall = (ns - nsStart) / i; +} + + +/** + * Construct a path relative to the base test directory. + * + * @returns g_szDir. + * @param pszAppend What to append. + * @param cchAppend How much to append. + */ +DECLINLINE(char *) InDir(const char *pszAppend, size_t cchAppend) +{ + Assert(g_szDir[g_cchDir - 1] == RTPATH_SLASH); + memcpy(&g_szDir[g_cchDir], pszAppend, cchAppend); + g_szDir[g_cchDir + cchAppend] = '\0'; + return &g_szDir[0]; +} + + +/** + * Construct a path relative to the base test directory, 2nd copy. + * + * @returns g_szDir2. + * @param pszAppend What to append. + * @param cchAppend How much to append. + */ +DECLINLINE(char *) InDir2(const char *pszAppend, size_t cchAppend) +{ + Assert(g_szDir[g_cchDir - 1] == RTPATH_SLASH); + memcpy(g_szDir2, g_szDir, g_cchDir); + memcpy(&g_szDir2[g_cchDir], pszAppend, cchAppend); + g_szDir2[g_cchDir + cchAppend] = '\0'; + return &g_szDir2[0]; +} + + +/** + * Construct a path relative to the empty directory. + * + * @returns g_szEmptyDir. + * @param pszAppend What to append. + * @param cchAppend How much to append. + */ +DECLINLINE(char *) InEmptyDir(const char *pszAppend, size_t cchAppend) +{ + Assert(g_szEmptyDir[g_cchEmptyDir - 1] == RTPATH_SLASH); + memcpy(&g_szEmptyDir[g_cchEmptyDir], pszAppend, cchAppend); + g_szEmptyDir[g_cchEmptyDir + cchAppend] = '\0'; + return &g_szEmptyDir[0]; +} + + +/** + * Construct a path relative to the deep test directory. + * + * @returns g_szDeepDir. + * @param pszAppend What to append. + * @param cchAppend How much to append. + */ +DECLINLINE(char *) InDeepDir(const char *pszAppend, size_t cchAppend) +{ + Assert(g_szDeepDir[g_cchDeepDir - 1] == RTPATH_SLASH); + memcpy(&g_szDeepDir[g_cchDeepDir], pszAppend, cchAppend); + g_szDeepDir[g_cchDeepDir + cchAppend] = '\0'; + return &g_szDeepDir[0]; +} + + + +/********************************************************************************************************************************* +* Slave FsPerf Instance Interaction. * +*********************************************************************************************************************************/ + +/** + * Construct a path relative to the comms directory. + * + * @returns g_szCommsDir. + * @param pszAppend What to append. + * @param cchAppend How much to append. + */ +DECLINLINE(char *) InCommsDir(const char *pszAppend, size_t cchAppend) +{ + Assert(g_szCommsDir[g_cchCommsDir - 1] == RTPATH_SLASH); + memcpy(&g_szCommsDir[g_cchCommsDir], pszAppend, cchAppend); + g_szCommsDir[g_cchCommsDir + cchAppend] = '\0'; + return &g_szCommsDir[0]; +} + + +/** + * Construct a path relative to the comms sub-directory. + * + * @returns g_szCommsSubDir. + * @param pszAppend What to append. + * @param cchAppend How much to append. + */ +DECLINLINE(char *) InCommsSubDir(const char *pszAppend, size_t cchAppend) +{ + Assert(g_szCommsSubDir[g_cchCommsSubDir - 1] == RTPATH_SLASH); + memcpy(&g_szCommsSubDir[g_cchCommsSubDir], pszAppend, cchAppend); + g_szCommsSubDir[g_cchCommsSubDir + cchAppend] = '\0'; + return &g_szCommsSubDir[0]; +} + + +/** + * Creates a file under g_szCommsDir with the given content. + * + * Will modify g_szCommsDir to contain the given filename. + * + * @returns IPRT status code (fully bitched). + * @param pszFilename The filename. + * @param cchFilename The length of the filename. + * @param pszContent The file content. + * @param cchContent The length of the file content. + */ +static int FsPerfCommsWriteFile(const char *pszFilename, size_t cchFilename, const char *pszContent, size_t cchContent) +{ + RTFILE hFile; + int rc = RTFileOpen(&hFile, InCommsDir(pszFilename, cchFilename), + RTFILE_O_WRITE | RTFILE_O_DENY_NONE | RTFILE_O_CREATE_REPLACE); + if (RT_SUCCESS(rc)) + { + rc = RTFileWrite(hFile, pszContent, cchContent, NULL); + if (RT_FAILURE(rc)) + RTMsgError("Error writing %#zx bytes to '%s': %Rrc", cchContent, g_szCommsDir, rc); + + int rc2 = RTFileClose(hFile); + if (RT_FAILURE(rc2)) + { + RTMsgError("Error closing to '%s': %Rrc", g_szCommsDir, rc); + rc = rc2; + } + if (RT_SUCCESS(rc) && g_uVerbosity >= 3) + RTMsgInfo("comms: wrote '%s'\n", g_szCommsDir); + if (RT_FAILURE(rc)) + RTFileDelete(g_szCommsDir); + } + else + RTMsgError("Failed to create '%s': %Rrc", g_szCommsDir, rc); + return rc; +} + + +/** + * Creates a file under g_szCommsDir with the given content, then renames it + * into g_szCommsSubDir. + * + * Will modify g_szCommsSubDir to contain the final filename and g_szCommsDir to + * hold the temporary one. + * + * @returns IPRT status code (fully bitched). + * @param pszFilename The filename. + * @param cchFilename The length of the filename. + * @param pszContent The file content. + * @param cchContent The length of the file content. + */ +static int FsPerfCommsWriteFileAndRename(const char *pszFilename, size_t cchFilename, const char *pszContent, size_t cchContent) +{ + int rc = FsPerfCommsWriteFile(pszFilename, cchFilename, pszContent, cchContent); + if (RT_SUCCESS(rc)) + { + rc = RTFileRename(g_szCommsDir, InCommsSubDir(pszFilename, cchFilename), RTPATHRENAME_FLAGS_REPLACE); + if (RT_SUCCESS(rc) && g_uVerbosity >= 3) + RTMsgInfo("comms: placed '%s'\n", g_szCommsSubDir); + if (RT_FAILURE(rc)) + { + RTMsgError("Error renaming '%s' to '%s': %Rrc", g_szCommsDir, g_szCommsSubDir, rc); + RTFileDelete(g_szCommsDir); + } + } + return rc; +} + + +/** + * Reads the given file from the comms subdir, ensuring that it is terminated by + * an EOF (0x1a) character. + * + * @returns IPRT status code. + * @retval VERR_TRY_AGAIN if the file is incomplete. + * @retval VERR_FILE_TOO_BIG if the file is considered too big. + * @retval VERR_FILE_NOT_FOUND if not found. + * + * @param iSeqNo The sequence number. + * @param pszSuffix The filename suffix. + * @param ppszContent Where to return the content. + */ +static int FsPerfCommsReadFile(uint32_t iSeqNo, const char *pszSuffix, char **ppszContent) +{ + *ppszContent = NULL; + + RTStrPrintf(&g_szCommsSubDir[g_cchCommsSubDir], sizeof(g_szCommsSubDir) - g_cchCommsSubDir, "%u%s", iSeqNo, pszSuffix); + RTFILE hFile; + int rc = RTFileOpen(&hFile, g_szCommsSubDir, RTFILE_O_READ | RTFILE_O_DENY_NONE | RTFILE_O_OPEN); + if (RT_SUCCESS(rc)) + { + size_t cbUsed = 0; + size_t cbAlloc = 1024; + char *pszBuf = (char *)RTMemAllocZ(cbAlloc); + for (;;) + { + /* Do buffer resizing. */ + size_t cbMaxRead = cbAlloc - cbUsed - 1; + if (cbMaxRead < 8) + { + if (cbAlloc < _1M) + { + cbAlloc *= 2; + void *pvRealloced = RTMemRealloc(pszBuf, cbAlloc); + if (!pvRealloced) + { + rc = VERR_NO_MEMORY; + break; + } + pszBuf = (char *)pvRealloced; + RT_BZERO(&pszBuf[cbAlloc / 2], cbAlloc); + cbMaxRead = cbAlloc - cbUsed - 1; + } + else + { + RTMsgError("File '%s' is too big - giving up at 1MB", g_szCommsSubDir); + rc = VERR_FILE_TOO_BIG; + break; + } + } + + /* Do the reading. */ + size_t cbActual = 0; + rc = RTFileRead(hFile, &pszBuf[cbUsed], cbMaxRead, &cbActual); + if (RT_SUCCESS(rc)) + cbUsed += cbActual; + else + { + RTMsgError("Failed to read '%s': %Rrc", g_szCommsSubDir, rc); + break; + } + + /* EOF? */ + if (cbActual < cbMaxRead) + break; + } + + RTFileClose(hFile); + + /* + * Check if the file ends with the EOF marker. + */ + if ( RT_SUCCESS(rc) + && ( cbUsed == 0 + || pszBuf[cbUsed - 1] != FSPERF_EOF)) + rc = VERR_TRY_AGAIN; + + /* + * Return or free the content we've read. + */ + if (RT_SUCCESS(rc)) + *ppszContent = pszBuf; + else + RTMemFree(pszBuf); + } + else if (rc != VERR_FILE_NOT_FOUND && rc != VERR_SHARING_VIOLATION) + RTMsgError("Failed to open '%s': %Rrc", g_szCommsSubDir, rc); + return rc; +} + + +/** + * FsPerfCommsReadFile + renaming from the comms subdir to the comms dir. + * + * g_szCommsSubDir holds the original filename and g_szCommsDir the final + * filename on success. + */ +static int FsPerfCommsReadFileAndRename(uint32_t iSeqNo, const char *pszSuffix, const char *pszRenameSuffix, char **ppszContent) +{ + RTStrPrintf(&g_szCommsDir[g_cchCommsDir], sizeof(g_szCommsDir) - g_cchCommsDir, "%u%s", iSeqNo, pszRenameSuffix); + int rc = FsPerfCommsReadFile(iSeqNo, pszSuffix, ppszContent); + if (RT_SUCCESS(rc)) + { + rc = RTFileRename(g_szCommsSubDir, g_szCommsDir, RTPATHRENAME_FLAGS_REPLACE); + if (RT_FAILURE(rc)) + { + RTMsgError("Error renaming '%s' to '%s': %Rrc", g_szCommsSubDir, g_szCommsDir, rc); + RTMemFree(*ppszContent); + *ppszContent = NULL; + } + } + return rc; +} + + +/** The comms master sequence number. */ +static uint32_t g_iSeqNoMaster = 0; + + +/** + * Sends a script to the remote comms slave. + * + * @returns IPRT status code giving the scripts execution status. + * @param pszScript The script. + */ +static int FsPerfCommsSend(const char *pszScript) +{ + /* + * Make sure the script is correctly terminated with an EOF control character. + */ + size_t const cchScript = strlen(pszScript); + AssertReturn(cchScript > 0 && pszScript[cchScript - 1] == FSPERF_EOF, VERR_INVALID_PARAMETER); + + /* + * Make sure the comms slave is running. + */ + if (!RTFileExists(InCommsDir(RT_STR_TUPLE("slave.pid")))) + return VERR_PIPE_NOT_CONNECTED; + + /* + * Format all the names we might want to check for. + */ + char szSendNm[32]; + size_t const cchSendNm = RTStrPrintf(szSendNm, sizeof(szSendNm), "%u-order.send", g_iSeqNoMaster); + + char szAckNm[64]; + size_t const cchAckNm = RTStrPrintf(szAckNm, sizeof(szAckNm), "%u-order.ack", g_iSeqNoMaster); + + /* + * Produce the script file and submit it. + */ + int rc = FsPerfCommsWriteFileAndRename(szSendNm, cchSendNm, pszScript, cchScript); + if (RT_SUCCESS(rc)) + { + g_iSeqNoMaster++; + + /* + * Wait for the result. + */ + uint64_t const msTimeout = RT_MS_1MIN / 2; + uint64_t msStart = RTTimeMilliTS(); + uint32_t msSleepX4 = 4; + for (;;) + { + /* Try read the result file: */ + char *pszContent = NULL; + rc = FsPerfCommsReadFile(g_iSeqNoMaster - 1, "-order.done", &pszContent); + if (RT_SUCCESS(rc)) + { + /* Split the result content into status code and error text: */ + char *pszErrorText = strchr(pszContent, '\n'); + if (pszErrorText) + { + *pszErrorText = '\0'; + pszErrorText++; + } + else + { + char *pszEnd = strchr(pszContent, '\0'); + Assert(pszEnd[-1] == FSPERF_EOF); + pszEnd[-1] = '\0'; + } + + /* Parse the status code: */ + int32_t rcRemote = VERR_GENERAL_FAILURE; + rc = RTStrToInt32Full(pszContent, 0, &rcRemote); + if (rc != VINF_SUCCESS) + { + RTTestIFailed("FsPerfCommsSend: Failed to convert status code '%s'", pszContent); + rcRemote = VERR_GENERAL_FAILURE; + } + + /* Display or return the text? */ + if (RT_SUCCESS(rc) && g_uVerbosity >= 2) + RTMsgInfo("comms: order #%u: %Rrc%s%s\n", + g_iSeqNoMaster - 1, rcRemote, *pszErrorText ? " - " : "", pszErrorText); + + RTMemFree(pszContent); + return rcRemote; + } + + if (rc == VERR_TRY_AGAIN) + msSleepX4 = 4; + + /* Check for timeout. */ + if (RTTimeMilliTS() - msStart > msTimeout) + { + if (RT_SUCCESS(rc) && g_uVerbosity >= 2) + RTMsgInfo("comms: timed out waiting for order #%u'\n", g_iSeqNoMaster - 1); + + rc = RTFileDelete(InCommsSubDir(szSendNm, cchSendNm)); + if (RT_SUCCESS(rc)) + { + g_iSeqNoMaster--; + rc = VERR_TIMEOUT; + } + else if (RTFileExists(InCommsDir(szAckNm, cchAckNm))) + rc = VERR_PIPE_BUSY; + else + rc = VERR_PIPE_IO_ERROR; + break; + } + + /* Sleep a little while. */ + msSleepX4++; + RTThreadSleep(msSleepX4 / 4); + } + } + return rc; +} + + +/** + * Shuts down the comms slave if it exists. + */ +static void FsPerfCommsShutdownSlave(void) +{ + static bool s_fAlreadyShutdown = false; + if (g_szCommsDir[0] != '\0' && !s_fAlreadyShutdown) + { + s_fAlreadyShutdown = true; + FsPerfCommsSend("exit" FSPERF_EOF_STR); + + g_szCommsDir[g_cchCommsDir] = '\0'; + int rc = RTDirRemoveRecursive(g_szCommsDir, RTDIRRMREC_F_CONTENT_AND_DIR | (g_fRelativeDir ? RTDIRRMREC_F_NO_ABS_PATH : 0)); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "RTDirRemoveRecursive(%s,) -> %Rrc\n", g_szCommsDir, rc); + } +} + + + +/********************************************************************************************************************************* +* Comms Slave * +*********************************************************************************************************************************/ + +typedef struct FSPERFCOMMSSLAVESTATE +{ + uint32_t iSeqNo; + bool fTerminate; + RTEXITCODE rcExit; + RTFILE ahFiles[8]; + char *apszFilenames[8]; + + /** The current command. */ + const char *pszCommand; + /** The current line number. */ + uint32_t iLineNo; + /** The current line content. */ + const char *pszLine; + /** Where to return extra error info text. */ + RTERRINFOSTATIC ErrInfo; +} FSPERFCOMMSSLAVESTATE; + + +static void FsPerfSlaveStateInit(FSPERFCOMMSSLAVESTATE *pState) +{ + pState->iSeqNo = 0; + pState->fTerminate = false; + pState->rcExit = RTEXITCODE_SUCCESS; + unsigned i = RT_ELEMENTS(pState->ahFiles); + while (i-- > 0) + { + pState->ahFiles[i] = NIL_RTFILE; + pState->apszFilenames[i] = NULL; + } + RTErrInfoInitStatic(&pState->ErrInfo); +} + + +static void FsPerfSlaveStateCleanup(FSPERFCOMMSSLAVESTATE *pState) +{ + unsigned i = RT_ELEMENTS(pState->ahFiles); + while (i-- > 0) + { + if (pState->ahFiles[i] != NIL_RTFILE) + { + RTFileClose(pState->ahFiles[i]); + pState->ahFiles[i] = NIL_RTFILE; + } + if (pState->apszFilenames[i] != NULL) + { + RTStrFree(pState->apszFilenames[i]); + pState->apszFilenames[i] = NULL; + } + } +} + + +/** Helper reporting a error. */ +static int FsPerfSlaveError(FSPERFCOMMSSLAVESTATE *pState, int rc, const char *pszError, ...) +{ + va_list va; + va_start(va, pszError); + RTErrInfoSetF(&pState->ErrInfo.Core, VERR_PARSE_ERROR, "line %u: %s: error: %N", + pState->iLineNo, pState->pszCommand, pszError, &va); + va_end(va); + return rc; +} + + +/** Helper reporting a syntax error. */ +static int FsPerfSlaveSyntax(FSPERFCOMMSSLAVESTATE *pState, const char *pszError, ...) +{ + va_list va; + va_start(va, pszError); + RTErrInfoSetF(&pState->ErrInfo.Core, VERR_PARSE_ERROR, "line %u: %s: syntax error: %N", + pState->iLineNo, pState->pszCommand, pszError, &va); + va_end(va); + return VERR_PARSE_ERROR; +} + + +/** Helper for parsing an unsigned 64-bit integer argument. */ +static int FsPerfSlaveParseU64(FSPERFCOMMSSLAVESTATE *pState, const char *pszArg, const char *pszName, + unsigned uBase, uint64_t uMin, uint64_t uLast, uint64_t *puValue) +{ + *puValue = uMin; + uint64_t uValue; + int rc = RTStrToUInt64Full(pszArg, uBase, &uValue); + if (RT_FAILURE(rc)) + return FsPerfSlaveSyntax(pState, "invalid %s: %s (RTStrToUInt64Full -> %Rrc)", pszName, pszArg, rc); + if (uValue < uMin || uValue > uLast) + return FsPerfSlaveSyntax(pState, "%s is out of range: %u, valid range %u..%u", pszName, uValue, uMin, uLast); + *puValue = uValue; + return VINF_SUCCESS; +} + + +/** Helper for parsing an unsigned 32-bit integer argument. */ +static int FsPerfSlaveParseU32(FSPERFCOMMSSLAVESTATE *pState, const char *pszArg, const char *pszName, + unsigned uBase, uint32_t uMin, uint32_t uLast, uint32_t *puValue) +{ + *puValue = uMin; + uint32_t uValue; + int rc = RTStrToUInt32Full(pszArg, uBase, &uValue); + if (RT_FAILURE(rc)) + return FsPerfSlaveSyntax(pState, "invalid %s: %s (RTStrToUInt32Full -> %Rrc)", pszName, pszArg, rc); + if (uValue < uMin || uValue > uLast) + return FsPerfSlaveSyntax(pState, "%s is out of range: %u, valid range %u..%u", pszName, uValue, uMin, uLast); + *puValue = uValue; + return VINF_SUCCESS; +} + + +/** Helper for parsing a file handle index argument. */ +static int FsPerfSlaveParseFileIdx(FSPERFCOMMSSLAVESTATE *pState, const char *pszArg, uint32_t *pidxFile) +{ + return FsPerfSlaveParseU32(pState, pszArg, "file index", 0, 0, RT_ELEMENTS(pState->ahFiles) - 1, pidxFile); +} + + +/** + * 'open {idxFile} {filename} {access} {disposition} [sharing] [mode]' + */ +static int FsPerfSlaveHandleOpen(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs > 1 + 6 || cArgs < 1 + 4) + return FsPerfSlaveSyntax(pState, "takes four to six arguments, not %u", cArgs); + + uint32_t idxFile; + int rc = FsPerfSlaveParseFileIdx(pState, papszArgs[1], &idxFile); + if (RT_FAILURE(rc)) + return rc; + + const char *pszFilename = papszArgs[2]; + + uint64_t fOpen = 0; + rc = RTFileModeToFlagsEx(papszArgs[3], papszArgs[4], papszArgs[5], &fOpen); + if (RT_FAILURE(rc)) + return FsPerfSlaveSyntax(pState, "failed to parse access (%s), disposition (%s) and sharing (%s): %Rrc", + papszArgs[3], papszArgs[4], papszArgs[5] ? papszArgs[5] : "", rc); + + uint32_t uMode = 0660; + if (cArgs >= 1 + 6) + { + rc = FsPerfSlaveParseU32(pState, papszArgs[6], "mode", 8, 0, 0777, &uMode); + if (RT_FAILURE(rc)) + return rc; + fOpen |= uMode << RTFILE_O_CREATE_MODE_SHIFT; + } + + /* + * Is there already a file assigned to the file handle index? + */ + if (pState->ahFiles[idxFile] != NIL_RTFILE) + return FsPerfSlaveError(pState, VERR_RESOURCE_BUSY, "handle #%u is already in use for '%s'", + idxFile, pState->apszFilenames[idxFile]); + + /* + * Check the filename length. + */ + size_t const cchFilename = strlen(pszFilename); + if (g_cchDir + cchFilename >= sizeof(g_szDir)) + return FsPerfSlaveError(pState, VERR_FILENAME_TOO_LONG, "'%.*s%s'", g_cchDir, g_szDir, pszFilename); + + /* + * Duplicate the name and execute the command. + */ + char *pszDup = RTStrDup(pszFilename); + if (!pszDup) + return FsPerfSlaveError(pState, VERR_NO_STR_MEMORY, "out of memory"); + + RTFILE hFile = NIL_RTFILE; + rc = RTFileOpen(&hFile, InDir(pszFilename, cchFilename), fOpen); + if (RT_SUCCESS(rc)) + { + pState->ahFiles[idxFile] = hFile; + pState->apszFilenames[idxFile] = pszDup; + } + else + { + RTStrFree(pszDup); + rc = FsPerfSlaveError(pState, rc, "%s: %Rrc", pszFilename, rc); + } + return rc; +} + + +/** + * 'close {idxFile}' + */ +static int FsPerfSlaveHandleClose(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs > 1 + 1) + return FsPerfSlaveSyntax(pState, "takes exactly one argument, not %u", cArgs); + + uint32_t idxFile; + int rc = FsPerfSlaveParseFileIdx(pState, papszArgs[1], &idxFile); + if (RT_SUCCESS(rc)) + { + /* + * Do it. + */ + rc = RTFileClose(pState->ahFiles[idxFile]); + if (RT_SUCCESS(rc)) + { + pState->ahFiles[idxFile] = NIL_RTFILE; + RTStrFree(pState->apszFilenames[idxFile]); + pState->apszFilenames[idxFile] = NULL; + } + } + return rc; +} + +/** @name Patterns for 'writepattern' + * @{ */ +static uint8_t const g_abPattern0[] = { 0xf0 }; +static uint8_t const g_abPattern1[] = { 0xf1 }; +static uint8_t const g_abPattern2[] = { 0xf2 }; +static uint8_t const g_abPattern3[] = { 0xf3 }; +static uint8_t const g_abPattern4[] = { 0xf4 }; +static uint8_t const g_abPattern5[] = { 0xf5 }; +static uint8_t const g_abPattern6[] = { 0xf6 }; +static uint8_t const g_abPattern7[] = { 0xf7 }; +static uint8_t const g_abPattern8[] = { 0xf8 }; +static uint8_t const g_abPattern9[] = { 0xf9 }; +static uint8_t const g_abPattern10[] = { 0x1f, 0x4e, 0x99, 0xec, 0x71, 0x71, 0x48, 0x0f, 0xa7, 0x5c, 0xb4, 0x5a, 0x1f, 0xc7, 0xd0, 0x93 }; +static struct +{ + uint8_t const *pb; + uint32_t cb; +} const g_aPatterns[] = +{ + { g_abPattern0, sizeof(g_abPattern0) }, + { g_abPattern1, sizeof(g_abPattern1) }, + { g_abPattern2, sizeof(g_abPattern2) }, + { g_abPattern3, sizeof(g_abPattern3) }, + { g_abPattern4, sizeof(g_abPattern4) }, + { g_abPattern5, sizeof(g_abPattern5) }, + { g_abPattern6, sizeof(g_abPattern6) }, + { g_abPattern7, sizeof(g_abPattern7) }, + { g_abPattern8, sizeof(g_abPattern8) }, + { g_abPattern9, sizeof(g_abPattern9) }, + { g_abPattern10, sizeof(g_abPattern10) }, +}; +/** @} */ + +/** + * 'writepattern {idxFile} {offFile} {idxPattern} {cbToWrite}' + */ +static int FsPerfSlaveHandleWritePattern(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs > 1 + 4) + return FsPerfSlaveSyntax(pState, "takes exactly four arguments, not %u", cArgs); + + uint32_t idxFile; + int rc = FsPerfSlaveParseFileIdx(pState, papszArgs[1], &idxFile); + if (RT_FAILURE(rc)) + return rc; + + uint64_t offFile; + rc = FsPerfSlaveParseU64(pState, papszArgs[2], "file offset", 0, 0, UINT64_MAX / 4, &offFile); + if (RT_FAILURE(rc)) + return rc; + + uint32_t idxPattern; + rc = FsPerfSlaveParseU32(pState, papszArgs[3], "pattern index", 0, 0, RT_ELEMENTS(g_aPatterns) - 1, &idxPattern); + if (RT_FAILURE(rc)) + return rc; + + uint64_t cbToWrite; + rc = FsPerfSlaveParseU64(pState, papszArgs[4], "number of bytes to write", 0, 0, _1G, &cbToWrite); + if (RT_FAILURE(rc)) + return rc; + + if (pState->ahFiles[idxFile] == NIL_RTFILE) + return FsPerfSlaveError(pState, VERR_INVALID_HANDLE, "no open file at index #%u", idxFile); + + /* + * Allocate a suitable buffer. + */ + size_t cbMaxBuf = RT_MIN(_2M, g_cbMaxBuffer); + size_t cbBuf = cbToWrite >= cbMaxBuf ? cbMaxBuf : RT_ALIGN_Z((size_t)cbToWrite, 512); + uint8_t *pbBuf = (uint8_t *)RTMemTmpAlloc(cbBuf); + if (!pbBuf) + { + cbBuf = _4K; + pbBuf = (uint8_t *)RTMemTmpAlloc(cbBuf); + if (!pbBuf) + return FsPerfSlaveError(pState, VERR_NO_TMP_MEMORY, "failed to allocate 4KB for buffers"); + } + + /* + * Fill 1 byte patterns before we start looping. + */ + if (g_aPatterns[idxPattern].cb == 1) + memset(pbBuf, g_aPatterns[idxPattern].pb[0], cbBuf); + + /* + * The write loop. + */ + uint32_t offPattern = 0; + while (cbToWrite > 0) + { + /* + * Fill the buffer if multi-byte pattern (single byte patterns are handled before the loop): + */ + if (g_aPatterns[idxPattern].cb > 1) + { + uint32_t const cbSrc = g_aPatterns[idxPattern].cb; + uint8_t const * const pbSrc = g_aPatterns[idxPattern].pb; + size_t cbDst = cbBuf; + uint8_t *pbDst = pbBuf; + + /* first iteration, potential partial pattern. */ + if (offPattern >= cbSrc) + offPattern = 0; + size_t cbThis1 = RT_MIN(g_aPatterns[idxPattern].cb - offPattern, cbToWrite); + memcpy(pbDst, &pbSrc[offPattern], cbThis1); + cbDst -= cbThis1; + if (cbDst > 0) + { + pbDst += cbThis1; + offPattern = 0; + + /* full patterns */ + while (cbDst >= cbSrc) + { + memcpy(pbDst, pbSrc, cbSrc); + pbDst += cbSrc; + cbDst -= cbSrc; + } + + /* partial final copy */ + if (cbDst > 0) + { + memcpy(pbDst, pbSrc, cbDst); + offPattern = (uint32_t)cbDst; + } + } + } + + /* + * Write. + */ + size_t const cbThisWrite = (size_t)RT_MIN(cbToWrite, cbBuf); + rc = RTFileWriteAt(pState->ahFiles[idxFile], offFile, pbBuf, cbThisWrite, NULL); + if (RT_FAILURE(rc)) + { + FsPerfSlaveError(pState, rc, "error writing %#zx bytes at %#RX64: %Rrc (file: %s)", + cbThisWrite, offFile, rc, pState->apszFilenames[idxFile]); + break; + } + + offFile += cbThisWrite; + cbToWrite -= cbThisWrite; + } + + RTMemTmpFree(pbBuf); + return rc; +} + + +/** + * 'truncate {idxFile} {cbFile}' + */ +static int FsPerfSlaveHandleTruncate(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs != 1 + 2) + return FsPerfSlaveSyntax(pState, "takes exactly two arguments, not %u", cArgs); + + uint32_t idxFile; + int rc = FsPerfSlaveParseFileIdx(pState, papszArgs[1], &idxFile); + if (RT_FAILURE(rc)) + return rc; + + uint64_t cbFile; + rc = FsPerfSlaveParseU64(pState, papszArgs[2], "new file size", 0, 0, UINT64_MAX / 4, &cbFile); + if (RT_FAILURE(rc)) + return rc; + + if (pState->ahFiles[idxFile] == NIL_RTFILE) + return FsPerfSlaveError(pState, VERR_INVALID_HANDLE, "no open file at index #%u", idxFile); + + /* + * Execute. + */ + rc = RTFileSetSize(pState->ahFiles[idxFile], cbFile); + if (RT_FAILURE(rc)) + return FsPerfSlaveError(pState, rc, "failed to set file size to %#RX64: %Rrc (file: %s)", + cbFile, rc, pState->apszFilenames[idxFile]); + return VINF_SUCCESS; +} + + +/** + * 'futimes {idxFile} {modified|0} [access|0] [change|0] [birth|0]' + */ +static int FsPerfSlaveHandleFUTimes(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs < 1 + 2 || cArgs > 1 + 5) + return FsPerfSlaveSyntax(pState, "takes between two and five arguments, not %u", cArgs); + + uint32_t idxFile; + int rc = FsPerfSlaveParseFileIdx(pState, papszArgs[1], &idxFile); + if (RT_FAILURE(rc)) + return rc; + + uint64_t nsModifiedTime; + rc = FsPerfSlaveParseU64(pState, papszArgs[2], "modified time", 0, 0, UINT64_MAX, &nsModifiedTime); + if (RT_FAILURE(rc)) + return rc; + + uint64_t nsAccessTime = 0; + if (cArgs >= 1 + 3) + { + rc = FsPerfSlaveParseU64(pState, papszArgs[3], "access time", 0, 0, UINT64_MAX, &nsAccessTime); + if (RT_FAILURE(rc)) + return rc; + } + + uint64_t nsChangeTime = 0; + if (cArgs >= 1 + 4) + { + rc = FsPerfSlaveParseU64(pState, papszArgs[4], "change time", 0, 0, UINT64_MAX, &nsChangeTime); + if (RT_FAILURE(rc)) + return rc; + } + + uint64_t nsBirthTime = 0; + if (cArgs >= 1 + 5) + { + rc = FsPerfSlaveParseU64(pState, papszArgs[4], "birth time", 0, 0, UINT64_MAX, &nsBirthTime); + if (RT_FAILURE(rc)) + return rc; + } + + if (pState->ahFiles[idxFile] == NIL_RTFILE) + return FsPerfSlaveError(pState, VERR_INVALID_HANDLE, "no open file at index #%u", idxFile); + + /* + * Execute. + */ + RTTIMESPEC ModifiedTime; + RTTIMESPEC AccessTime; + RTTIMESPEC ChangeTime; + RTTIMESPEC BirthTime; + rc = RTFileSetTimes(pState->ahFiles[idxFile], + nsAccessTime ? RTTimeSpecSetNano(&AccessTime, nsAccessTime) : NULL, + nsModifiedTime ? RTTimeSpecSetNano(&ModifiedTime, nsModifiedTime) : NULL, + nsChangeTime ? RTTimeSpecSetNano(&ChangeTime, nsChangeTime) : NULL, + nsBirthTime ? RTTimeSpecSetNano(&BirthTime, nsBirthTime) : NULL); + if (RT_FAILURE(rc)) + return FsPerfSlaveError(pState, rc, "failed to set file times to %RI64, %RI64, %RI64, %RI64: %Rrc (file: %s)", + nsModifiedTime, nsAccessTime, nsChangeTime, nsBirthTime, rc, pState->apszFilenames[idxFile]); + return VINF_SUCCESS; +} + + +/** + * 'fchmod {idxFile} {cbFile}' + */ +static int FsPerfSlaveHandleFChMod(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs != 1 + 2) + return FsPerfSlaveSyntax(pState, "takes exactly two arguments, not %u", cArgs); + + uint32_t idxFile; + int rc = FsPerfSlaveParseFileIdx(pState, papszArgs[1], &idxFile); + if (RT_FAILURE(rc)) + return rc; + + uint32_t fAttribs; + rc = FsPerfSlaveParseU32(pState, papszArgs[2], "new file attributes", 0, 0, UINT32_MAX, &fAttribs); + if (RT_FAILURE(rc)) + return rc; + + if (pState->ahFiles[idxFile] == NIL_RTFILE) + return FsPerfSlaveError(pState, VERR_INVALID_HANDLE, "no open file at index #%u", idxFile); + + /* + * Execute. + */ + rc = RTFileSetMode(pState->ahFiles[idxFile], fAttribs); + if (RT_FAILURE(rc)) + return FsPerfSlaveError(pState, rc, "failed to set file mode to %#RX32: %Rrc (file: %s)", + fAttribs, rc, pState->apszFilenames[idxFile]); + return VINF_SUCCESS; +} + + +/** + * 'reset' + */ +static int FsPerfSlaveHandleReset(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs > 1) + return FsPerfSlaveSyntax(pState, "takes zero arguments, not %u", cArgs); + RT_NOREF(papszArgs); + + /* + * Execute the command. + */ + FsPerfSlaveStateCleanup(pState); + return VINF_SUCCESS; +} + + +/** + * 'exit [exitcode]' + */ +static int FsPerfSlaveHandleExit(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs) +{ + /* + * Parse parameters. + */ + if (cArgs > 1 + 1) + return FsPerfSlaveSyntax(pState, "takes zero or one argument, not %u", cArgs); + + if (cArgs >= 1 + 1) + { + uint32_t uExitCode; + int rc = FsPerfSlaveParseU32(pState, papszArgs[1], "exit code", 0, 0, 127, &uExitCode); + if (RT_FAILURE(rc)) + return rc; + + /* + * Execute the command. + */ + pState->rcExit = (RTEXITCODE)uExitCode; + } + pState->fTerminate = true; + return VINF_SUCCESS; +} + + +/** + * Executes a script line. + */ +static int FsPerfSlaveExecuteLine(FSPERFCOMMSSLAVESTATE *pState, char *pszLine) +{ + /* + * Parse the command line using bourne shell quoting style. + */ + char **papszArgs; + int cArgs; + int rc = RTGetOptArgvFromString(&papszArgs, &cArgs, pszLine, RTGETOPTARGV_CNV_QUOTE_BOURNE_SH, NULL); + if (RT_FAILURE(rc)) + return RTErrInfoSetF(&pState->ErrInfo.Core, rc, "Failed to parse line %u: %s", pState->iLineNo, pszLine); + if (cArgs <= 0) + { + RTGetOptArgvFree(papszArgs); + return RTErrInfoSetF(&pState->ErrInfo.Core, rc, "No command found on line %u: %s", pState->iLineNo, pszLine); + } + + /* + * Execute the command. + */ + static const struct + { + const char *pszCmd; + size_t cchCmd; + int (*pfnHandler)(FSPERFCOMMSSLAVESTATE *pState, char **papszArgs, int cArgs); + } s_aHandlers[] = + { + { RT_STR_TUPLE("open"), FsPerfSlaveHandleOpen }, + { RT_STR_TUPLE("close"), FsPerfSlaveHandleClose }, + { RT_STR_TUPLE("writepattern"), FsPerfSlaveHandleWritePattern }, + { RT_STR_TUPLE("truncate"), FsPerfSlaveHandleTruncate }, + { RT_STR_TUPLE("futimes"), FsPerfSlaveHandleFUTimes}, + { RT_STR_TUPLE("fchmod"), FsPerfSlaveHandleFChMod }, + { RT_STR_TUPLE("reset"), FsPerfSlaveHandleReset }, + { RT_STR_TUPLE("exit"), FsPerfSlaveHandleExit }, + }; + const char * const pszCmd = papszArgs[0]; + size_t const cchCmd = strlen(pszCmd); + for (size_t i = 0; i < RT_ELEMENTS(s_aHandlers); i++) + if ( s_aHandlers[i].cchCmd == cchCmd + && memcmp(pszCmd, s_aHandlers[i].pszCmd, cchCmd) == 0) + { + pState->pszCommand = s_aHandlers[i].pszCmd; + rc = s_aHandlers[i].pfnHandler(pState, papszArgs, cArgs); + RTGetOptArgvFree(papszArgs); + return rc; + } + + rc = RTErrInfoSetF(&pState->ErrInfo.Core, VERR_NOT_FOUND, "Command on line %u not found: %s", pState->iLineNo, pszLine); + RTGetOptArgvFree(papszArgs); + return rc; +} + + +/** + * Executes a script. + */ +static int FsPerfSlaveExecuteScript(FSPERFCOMMSSLAVESTATE *pState, char *pszContent) +{ + /* + * Validate the encoding. + */ + int rc = RTStrValidateEncoding(pszContent); + if (RT_FAILURE(rc)) + return RTErrInfoSetF(&pState->ErrInfo.Core, rc, "Invalid UTF-8 encoding"); + + /* + * Work the script content line by line. + */ + pState->iLineNo = 0; + while (*pszContent != FSPERF_EOF && *pszContent != '\0') + { + pState->iLineNo++; + + /* Figure the current line and move pszContent ahead: */ + char *pszLine = RTStrStripL(pszContent); + char *pszEol = strchr(pszLine, '\n'); + if (pszEol) + pszContent = pszEol + 1; + else + { + pszEol = strchr(pszLine, FSPERF_EOF); + AssertStmt(pszEol, pszEol = strchr(pszLine, '\0')); + pszContent = pszEol; + } + + /* Terminate and strip it: */ + *pszEol = '\0'; + pszLine = RTStrStrip(pszLine); + + /* Skip empty lines and comment lines: */ + if (*pszLine == '\0' || *pszLine == '#') + continue; + + /* Execute the line: */ + pState->pszLine = pszLine; + rc = FsPerfSlaveExecuteLine(pState, pszLine); + if (RT_FAILURE(rc)) + break; + } + return rc; +} + + +/** + * Communication slave. + * + * @returns exit code. + */ +static int FsPerfCommsSlave(void) +{ + /* + * Make sure we've got a directory and create it and it's subdir. + */ + if (g_cchCommsDir == 0) + return RTMsgError("no communcation directory was specified (-C)"); + + int rc = RTDirCreateFullPath(g_szCommsSubDir, 0775); + if (RT_FAILURE(rc)) + return RTMsgError("Failed to create '%s': %Rrc", g_szCommsSubDir, rc); + + /* + * Signal that we're here. + */ + char szTmp[_4K]; + rc = FsPerfCommsWriteFile(RT_STR_TUPLE("slave.pid"), szTmp, RTStrPrintf(szTmp, sizeof(szTmp), + "%u" FSPERF_EOF_STR, RTProcSelf())); + if (RT_FAILURE(rc)) + return RTEXITCODE_FAILURE; + + /* + * Processing loop. + */ + FSPERFCOMMSSLAVESTATE State; + FsPerfSlaveStateInit(&State); + uint32_t msSleep = 1; + while (!State.fTerminate) + { + /* + * Try read the next command script. + */ + char *pszContent = NULL; + rc = FsPerfCommsReadFileAndRename(State.iSeqNo, "-order.send", "-order.ack", &pszContent); + if (RT_SUCCESS(rc)) + { + /* + * Execute it. + */ + RTErrInfoInitStatic(&State.ErrInfo); + rc = FsPerfSlaveExecuteScript(&State, pszContent); + + /* + * Write the result. + */ + char szResult[64]; + size_t cchResult = RTStrPrintf(szResult, sizeof(szResult), "%u-order.done", State.iSeqNo); + size_t cchTmp = RTStrPrintf(szTmp, sizeof(szTmp), "%d\n%s" FSPERF_EOF_STR, + rc, RTErrInfoIsSet(&State.ErrInfo.Core) ? State.ErrInfo.Core.pszMsg : ""); + FsPerfCommsWriteFileAndRename(szResult, cchResult, szTmp, cchTmp); + State.iSeqNo++; + + msSleep = 1; + } + + /* + * Wait a little and check again. + */ + RTThreadSleep(msSleep); + if (msSleep < 128) + msSleep++; + } + + /* + * Remove the we're here indicator and quit. + */ + RTFileDelete(InCommsDir(RT_STR_TUPLE("slave.pid"))); + FsPerfSlaveStateCleanup(&State); + return State.rcExit; +} + + + +/********************************************************************************************************************************* +* Tests * +*********************************************************************************************************************************/ + +/** + * Prepares the test area. + * @returns VBox status code. + */ +static int fsPrepTestArea(void) +{ + /* The empty subdir and associated globals: */ + static char s_szEmpty[] = "empty"; + memcpy(g_szEmptyDir, g_szDir, g_cchDir); + memcpy(&g_szEmptyDir[g_cchDir], s_szEmpty, sizeof(s_szEmpty)); + g_cchEmptyDir = g_cchDir + sizeof(s_szEmpty) - 1; + RTTESTI_CHECK_RC_RET(RTDirCreate(g_szEmptyDir, 0755, 0), VINF_SUCCESS, rcCheck); + g_szEmptyDir[g_cchEmptyDir++] = RTPATH_SLASH; + g_szEmptyDir[g_cchEmptyDir] = '\0'; + RTTestIPrintf(RTTESTLVL_ALWAYS, "Empty dir: %s\n", g_szEmptyDir); + + /* Deep directory: */ + memcpy(g_szDeepDir, g_szDir, g_cchDir); + g_cchDeepDir = g_cchDir; + do + { + static char const s_szSub[] = "d" RTPATH_SLASH_STR; + memcpy(&g_szDeepDir[g_cchDeepDir], s_szSub, sizeof(s_szSub)); + g_cchDeepDir += sizeof(s_szSub) - 1; + int rc = RTDirCreate(g_szDeepDir, 0755, 0); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTDirCreate(g_szDeepDir=%s) -> %Rrc\n", g_szDeepDir, rc); + return rc; + } + } while (g_cchDeepDir < 176); + RTTestIPrintf(RTTESTLVL_ALWAYS, "Deep dir: %s\n", g_szDeepDir); + + /* Create known file in both deep and shallow dirs: */ + RTFILE hKnownFile; + RTTESTI_CHECK_RC_RET(RTFileOpen(&hKnownFile, InDir(RT_STR_TUPLE("known-file")), + RTFILE_O_CREATE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), + VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RC_RET(RTFileClose(hKnownFile), VINF_SUCCESS, rcCheck); + + RTTESTI_CHECK_RC_RET(RTFileOpen(&hKnownFile, InDeepDir(RT_STR_TUPLE("known-file")), + RTFILE_O_CREATE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), + VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RC_RET(RTFileClose(hKnownFile), VINF_SUCCESS, rcCheck); + + return VINF_SUCCESS; +} + + +/** + * Create a name list entry. + * @returns Pointer to the entry, NULL if out of memory. + * @param pchName The name. + * @param cchName The name length. + */ +PFSPERFNAMEENTRY fsPerfCreateNameEntry(const char *pchName, size_t cchName) +{ + PFSPERFNAMEENTRY pEntry = (PFSPERFNAMEENTRY)RTMemAllocVar(RT_UOFFSETOF_DYN(FSPERFNAMEENTRY, szName[cchName + 1])); + if (pEntry) + { + RTListInit(&pEntry->Entry); + pEntry->cchName = (uint16_t)cchName; + memcpy(pEntry->szName, pchName, cchName); + pEntry->szName[cchName] = '\0'; + } + return pEntry; +} + + +static int fsPerfManyTreeRecursiveDirCreator(size_t cchDir, uint32_t iDepth) +{ + PFSPERFNAMEENTRY pEntry = fsPerfCreateNameEntry(g_szDir, cchDir); + RTTESTI_CHECK_RET(pEntry, VERR_NO_MEMORY); + RTListAppend(&g_ManyTreeHead, &pEntry->Entry); + + RTTESTI_CHECK_RC_RET(RTDirCreate(g_szDir, 0755, RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_DONT_SET | RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_NOT_CRITICAL), + VINF_SUCCESS, rcCheck); + + if (iDepth < g_cManyTreeDepth) + for (uint32_t i = 0; i < g_cManyTreeSubdirsPerDir; i++) + { + size_t cchSubDir = RTStrPrintf(&g_szDir[cchDir], sizeof(g_szDir) - cchDir, "d%02u" RTPATH_SLASH_STR, i); + RTTESTI_CHECK_RC_RET(fsPerfManyTreeRecursiveDirCreator(cchDir + cchSubDir, iDepth + 1), VINF_SUCCESS, rcCheck); + } + + return VINF_SUCCESS; +} + + +void fsPerfManyFiles(void) +{ + RTTestISub("manyfiles"); + + /* + * Create a sub-directory with like 10000 files in it. + * + * This does push the directory organization of the underlying file system, + * which is something we might not want to profile with shared folders. It + * is however useful for directory enumeration. + */ + RTTESTI_CHECK_RC_RETV(RTDirCreate(InDir(RT_STR_TUPLE("manyfiles")), 0755, + RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_DONT_SET | RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_NOT_CRITICAL), + VINF_SUCCESS); + + size_t offFilename = strlen(g_szDir); + g_szDir[offFilename++] = RTPATH_SLASH; + + fsPerfYield(); + RTFILE hFile; + uint64_t const nsStart = RTTimeNanoTS(); + for (uint32_t i = 0; i < g_cManyFiles; i++) + { + RTStrFormatU32(&g_szDir[offFilename], sizeof(g_szDir) - offFilename, i, 10, 5, 5, RTSTR_F_ZEROPAD); + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile, g_szDir, RTFILE_O_CREATE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile), VINF_SUCCESS); + } + uint64_t const cNsElapsed = RTTimeNanoTS() - nsStart; + RTTestIValueF(cNsElapsed, RTTESTUNIT_NS, "Creating %u empty files in single directory", g_cManyFiles); + RTTestIValueF(cNsElapsed / g_cManyFiles, RTTESTUNIT_NS_PER_OCCURRENCE, "Create empty file (single dir)"); + + /* + * Create a bunch of directories with exacly 32 files in each, hoping to + * avoid any directory organization artifacts. + */ + /* Create the directories first, building a list of them for simplifying iteration: */ + RTListInit(&g_ManyTreeHead); + InDir(RT_STR_TUPLE("manytree" RTPATH_SLASH_STR)); + RTTESTI_CHECK_RC_RETV(fsPerfManyTreeRecursiveDirCreator(strlen(g_szDir), 0), VINF_SUCCESS); + + /* Create the zero byte files: */ + fsPerfYield(); + uint64_t const nsStart2 = RTTimeNanoTS(); + uint32_t cFiles = 0; + PFSPERFNAMEENTRY pCur; + RTListForEach(&g_ManyTreeHead, pCur, FSPERFNAMEENTRY, Entry) + { + char szPath[FSPERF_MAX_PATH]; + memcpy(szPath, pCur->szName, pCur->cchName); + for (uint32_t i = 0; i < g_cManyTreeFilesPerDir; i++) + { + RTStrFormatU32(&szPath[pCur->cchName], sizeof(szPath) - pCur->cchName, i, 10, 5, 5, RTSTR_F_ZEROPAD); + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile, szPath, RTFILE_O_CREATE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile), VINF_SUCCESS); + cFiles++; + } + } + uint64_t const cNsElapsed2 = RTTimeNanoTS() - nsStart2; + RTTestIValueF(cNsElapsed2, RTTESTUNIT_NS, "Creating %u empty files in tree", cFiles); + RTTestIValueF(cNsElapsed2 / cFiles, RTTESTUNIT_NS_PER_OCCURRENCE, "Create empty file (tree)"); + RTTESTI_CHECK(g_cManyTreeFiles == cFiles); +} + + +DECL_FORCE_INLINE(int) fsPerfOpenExistingOnceReadonly(const char *pszFile) +{ + RTFILE hFile; + RTTESTI_CHECK_RC_RET(RTFileOpen(&hFile, pszFile, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RC(RTFileClose(hFile), VINF_SUCCESS); + return VINF_SUCCESS; +} + + +DECL_FORCE_INLINE(int) fsPerfOpenExistingOnceWriteonly(const char *pszFile) +{ + RTFILE hFile; + RTTESTI_CHECK_RC_RET(RTFileOpen(&hFile, pszFile, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RC(RTFileClose(hFile), VINF_SUCCESS); + return VINF_SUCCESS; +} + + +/** @note tstRTFileOpenEx-1.cpp has a copy of this code. */ +static void tstOpenExTest(unsigned uLine, int cbExist, int cbNext, const char *pszFilename, uint64_t fAction, + int rcExpect, RTFILEACTION enmActionExpected) +{ + uint64_t const fCreateMode = (0644 << RTFILE_O_CREATE_MODE_SHIFT); + RTFILE hFile; + int rc; + + /* + * File existence and size. + */ + bool fOkay = false; + RTFSOBJINFO ObjInfo; + rc = RTPathQueryInfoEx(pszFilename, &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK); + if (RT_SUCCESS(rc)) + fOkay = cbExist == (int64_t)ObjInfo.cbObject; + else + fOkay = rc == VERR_FILE_NOT_FOUND && cbExist < 0; + if (!fOkay) + { + if (cbExist >= 0) + { + rc = RTFileOpen(&hFile, pszFilename, RTFILE_O_WRITE | RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | fCreateMode); + if (RT_SUCCESS(rc)) + { + while (cbExist > 0) + { + int cbToWrite = (int)strlen(pszFilename); + if (cbToWrite > cbExist) + cbToWrite = cbExist; + rc = RTFileWrite(hFile, pszFilename, cbToWrite, NULL); + if (RT_FAILURE(rc)) + { + RTTestIFailed("%u: RTFileWrite(%s,%#x) -> %Rrc\n", uLine, pszFilename, cbToWrite, rc); + break; + } + cbExist -= cbToWrite; + } + + RTTESTI_CHECK_RC(RTFileClose(hFile), VINF_SUCCESS); + } + else + RTTestIFailed("%u: RTFileDelete(%s) -> %Rrc\n", uLine, pszFilename, rc); + + } + else + { + rc = RTFileDelete(pszFilename); + if (rc != VINF_SUCCESS && rc != VERR_FILE_NOT_FOUND) + RTTestIFailed("%u: RTFileDelete(%s) -> %Rrc\n", uLine, pszFilename, rc); + } + } + + /* + * The actual test. + */ + RTFILEACTION enmActuallyTaken = RTFILEACTION_END; + hFile = NIL_RTFILE; + rc = RTFileOpenEx(pszFilename, fAction | RTFILE_O_READWRITE | RTFILE_O_DENY_NONE | fCreateMode, &hFile, &enmActuallyTaken); + if ( rc != rcExpect + || enmActuallyTaken != enmActionExpected + || (RT_SUCCESS(rc) ? hFile == NIL_RTFILE : hFile != NIL_RTFILE)) + RTTestIFailed("%u: RTFileOpenEx(%s, %#llx) -> %Rrc + %d (hFile=%p), expected %Rrc + %d\n", + uLine, pszFilename, fAction, rc, enmActuallyTaken, hFile, rcExpect, enmActionExpected); + if (RT_SUCCESS(rc)) + { + if ( enmActionExpected == RTFILEACTION_REPLACED + || enmActionExpected == RTFILEACTION_TRUNCATED) + { + uint8_t abBuf[16]; + rc = RTFileRead(hFile, abBuf, 1, NULL); + if (rc != VERR_EOF) + RTTestIFailed("%u: RTFileRead(%s,,1,) -> %Rrc, expected VERR_EOF\n", uLine, pszFilename, rc); + } + + while (cbNext > 0) + { + int cbToWrite = (int)strlen(pszFilename); + if (cbToWrite > cbNext) + cbToWrite = cbNext; + rc = RTFileWrite(hFile, pszFilename, cbToWrite, NULL); + if (RT_FAILURE(rc)) + { + RTTestIFailed("%u: RTFileWrite(%s,%#x) -> %Rrc\n", uLine, pszFilename, cbToWrite, rc); + break; + } + cbNext -= cbToWrite; + } + + rc = RTFileClose(hFile); + if (RT_FAILURE(rc)) + RTTestIFailed("%u: RTFileClose(%p) -> %Rrc\n", uLine, hFile, rc); + } +} + + +void fsPerfOpen(void) +{ + RTTestISub("open"); + + /* Opening non-existing files. */ + RTFILE hFile; + RTTESTI_CHECK_RC(RTFileOpen(&hFile, InEmptyDir(RT_STR_TUPLE("no-such-file")), + RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileOpen(&hFile, InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file")), + RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileOpen(&hFile, InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file")), + RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VERR_PATH_NOT_FOUND); + + /* + * The following is copied from tstRTFileOpenEx-1.cpp: + */ + InDir(RT_STR_TUPLE("file1")); + tstOpenExTest(__LINE__, -1, -1, g_szDir, RTFILE_O_OPEN, VERR_FILE_NOT_FOUND, RTFILEACTION_INVALID); + tstOpenExTest(__LINE__, -1, -1, g_szDir, RTFILE_O_OPEN_CREATE, VINF_SUCCESS, RTFILEACTION_CREATED); + tstOpenExTest(__LINE__, 0, 0, g_szDir, RTFILE_O_OPEN_CREATE, VINF_SUCCESS, RTFILEACTION_OPENED); + tstOpenExTest(__LINE__, 0, 0, g_szDir, RTFILE_O_OPEN, VINF_SUCCESS, RTFILEACTION_OPENED); + + tstOpenExTest(__LINE__, 0, 0, g_szDir, RTFILE_O_OPEN | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_TRUNCATED); + tstOpenExTest(__LINE__, 0, 10, g_szDir, RTFILE_O_OPEN_CREATE | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_TRUNCATED); + tstOpenExTest(__LINE__, 10, 10, g_szDir, RTFILE_O_OPEN_CREATE | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_TRUNCATED); + tstOpenExTest(__LINE__, 10, -1, g_szDir, RTFILE_O_OPEN | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_TRUNCATED); + tstOpenExTest(__LINE__, -1, -1, g_szDir, RTFILE_O_OPEN | RTFILE_O_TRUNCATE, VERR_FILE_NOT_FOUND, RTFILEACTION_INVALID); + tstOpenExTest(__LINE__, -1, 0, g_szDir, RTFILE_O_OPEN_CREATE | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_CREATED); + + tstOpenExTest(__LINE__, 0, -1, g_szDir, RTFILE_O_CREATE_REPLACE, VINF_SUCCESS, RTFILEACTION_REPLACED); + tstOpenExTest(__LINE__, -1, 0, g_szDir, RTFILE_O_CREATE_REPLACE, VINF_SUCCESS, RTFILEACTION_CREATED); + tstOpenExTest(__LINE__, 0, -1, g_szDir, RTFILE_O_CREATE, VERR_ALREADY_EXISTS, RTFILEACTION_ALREADY_EXISTS); + tstOpenExTest(__LINE__, -1, -1, g_szDir, RTFILE_O_CREATE, VINF_SUCCESS, RTFILEACTION_CREATED); + + tstOpenExTest(__LINE__, -1, 10, g_szDir, RTFILE_O_CREATE | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_CREATED); + tstOpenExTest(__LINE__, 10, 10, g_szDir, RTFILE_O_CREATE | RTFILE_O_TRUNCATE, VERR_ALREADY_EXISTS, RTFILEACTION_ALREADY_EXISTS); + tstOpenExTest(__LINE__, 10, -1, g_szDir, RTFILE_O_CREATE_REPLACE | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_REPLACED); + tstOpenExTest(__LINE__, -1, -1, g_szDir, RTFILE_O_CREATE_REPLACE | RTFILE_O_TRUNCATE, VINF_SUCCESS, RTFILEACTION_CREATED); + + RTTESTI_CHECK_RC(RTFileDelete(g_szDir), VINF_SUCCESS); + + /* + * Create file1 and then try exclusivly creating it again. + * Then profile opening it for reading. + */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file1")), + RTFILE_O_CREATE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileOpen(&hFile, g_szDir, RTFILE_O_CREATE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VERR_ALREADY_EXISTS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + PROFILE_FN(fsPerfOpenExistingOnceReadonly(g_szDir), g_nsTestRun, "RTFileOpen/Close/Readonly"); + PROFILE_FN(fsPerfOpenExistingOnceWriteonly(g_szDir), g_nsTestRun, "RTFileOpen/Close/Writeonly"); + + /* + * Profile opening in the deep directory too. + */ + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDeepDir(RT_STR_TUPLE("file1")), + RTFILE_O_CREATE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + PROFILE_FN(fsPerfOpenExistingOnceReadonly(g_szDeepDir), g_nsTestRun, "RTFileOpen/Close/deep/readonly"); + PROFILE_FN(fsPerfOpenExistingOnceWriteonly(g_szDeepDir), g_nsTestRun, "RTFileOpen/Close/deep/writeonly"); + + /* Manytree: */ + char szPath[FSPERF_MAX_PATH]; + PROFILE_MANYTREE_FN(szPath, fsPerfOpenExistingOnceReadonly(szPath), 1, g_nsTestRun, "RTFileOpen/Close/manytree/readonly"); +} + + +void fsPerfFStat(void) +{ + RTTestISub("fstat"); + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file2")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTFSOBJINFO ObjInfo = {0}; + PROFILE_FN(RTFileQueryInfo(hFile1, &ObjInfo, RTFSOBJATTRADD_NOTHING), g_nsTestRun, "RTFileQueryInfo/NOTHING"); + PROFILE_FN(RTFileQueryInfo(hFile1, &ObjInfo, RTFSOBJATTRADD_UNIX), g_nsTestRun, "RTFileQueryInfo/UNIX"); + + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); +} + +#ifdef RT_OS_WINDOWS +/** + * Nt(Query|Set|QueryDir)Information(File|) information class info. + */ +static const struct +{ + const char *pszName; + int enmValue; + bool fQuery; + bool fSet; + bool fQueryDir; + uint8_t cbMin; +} g_aNtQueryInfoFileClasses[] = +{ +#define E(a_enmValue, a_fQuery, a_fSet, a_fQueryDir, a_cbMin) \ + { #a_enmValue, a_enmValue, a_fQuery, a_fSet, a_fQueryDir, a_cbMin } + { "invalid0", 0, false, false, false, 0 }, + E(FileDirectoryInformation, false, false, true, sizeof(FILE_DIRECTORY_INFORMATION)), // 0x00, 0x00, 0x48 + E(FileFullDirectoryInformation, false, false, true, sizeof(FILE_FULL_DIR_INFORMATION)), // 0x00, 0x00, 0x48 + E(FileBothDirectoryInformation, false, false, true, sizeof(FILE_BOTH_DIR_INFORMATION)), // 0x00, 0x00, 0x60 + E(FileBasicInformation, true, true, false, sizeof(FILE_BASIC_INFORMATION)), + E(FileStandardInformation, true, false, false, sizeof(FILE_STANDARD_INFORMATION)), + E(FileInternalInformation, true, false, false, sizeof(FILE_INTERNAL_INFORMATION)), + E(FileEaInformation, true, false, false, sizeof(FILE_EA_INFORMATION)), + E(FileAccessInformation, true, false, false, sizeof(FILE_ACCESS_INFORMATION)), + E(FileNameInformation, true, false, false, sizeof(FILE_NAME_INFORMATION)), + E(FileRenameInformation, false, true, false, sizeof(FILE_RENAME_INFORMATION)), + E(FileLinkInformation, false, true, false, sizeof(FILE_LINK_INFORMATION)), + E(FileNamesInformation, false, false, true, sizeof(FILE_NAMES_INFORMATION)), // 0x00, 0x00, 0x10 + E(FileDispositionInformation, false, true, false, sizeof(FILE_DISPOSITION_INFORMATION)), // 0x00, 0x01, + E(FilePositionInformation, true, true, false, sizeof(FILE_POSITION_INFORMATION)), // 0x08, 0x08, + E(FileFullEaInformation, false, false, false, sizeof(FILE_FULL_EA_INFORMATION)), // 0x00, 0x00, + E(FileModeInformation, true, true, false, sizeof(FILE_MODE_INFORMATION)), // 0x04, 0x04, + E(FileAlignmentInformation, true, false, false, sizeof(FILE_ALIGNMENT_INFORMATION)), // 0x04, 0x00, + E(FileAllInformation, true, false, false, sizeof(FILE_ALL_INFORMATION)), // 0x68, 0x00, + E(FileAllocationInformation, false, true, false, sizeof(FILE_ALLOCATION_INFORMATION)), // 0x00, 0x08, + E(FileEndOfFileInformation, false, true, false, sizeof(FILE_END_OF_FILE_INFORMATION)), // 0x00, 0x08, + E(FileAlternateNameInformation, true, false, false, sizeof(FILE_NAME_INFORMATION)), // 0x08, 0x00, + E(FileStreamInformation, true, false, false, sizeof(FILE_STREAM_INFORMATION)), // 0x20, 0x00, + E(FilePipeInformation, true, true, false, sizeof(FILE_PIPE_INFORMATION)), // 0x08, 0x08, + E(FilePipeLocalInformation, true, false, false, sizeof(FILE_PIPE_LOCAL_INFORMATION)), // 0x28, 0x00, + E(FilePipeRemoteInformation, true, true, false, sizeof(FILE_PIPE_REMOTE_INFORMATION)), // 0x10, 0x10, + E(FileMailslotQueryInformation, true, false, false, sizeof(FILE_MAILSLOT_QUERY_INFORMATION)), // 0x18, 0x00, + E(FileMailslotSetInformation, false, true, false, sizeof(FILE_MAILSLOT_SET_INFORMATION)), // 0x00, 0x08, + E(FileCompressionInformation, true, false, false, sizeof(FILE_COMPRESSION_INFORMATION)), // 0x10, 0x00, + E(FileObjectIdInformation, true, true, true, sizeof(FILE_OBJECTID_INFORMATION)), // 0x48, 0x48, + E(FileCompletionInformation, false, true, false, sizeof(FILE_COMPLETION_INFORMATION)), // 0x00, 0x10, + E(FileMoveClusterInformation, false, true, false, sizeof(FILE_MOVE_CLUSTER_INFORMATION)), // 0x00, 0x18, + E(FileQuotaInformation, true, true, true, sizeof(FILE_QUOTA_INFORMATION)), // 0x38, 0x38, 0x38 + E(FileReparsePointInformation, true, false, true, sizeof(FILE_REPARSE_POINT_INFORMATION)), // 0x10, 0x00, 0x10 + E(FileNetworkOpenInformation, true, false, false, sizeof(FILE_NETWORK_OPEN_INFORMATION)), // 0x38, 0x00, + E(FileAttributeTagInformation, true, false, false, sizeof(FILE_ATTRIBUTE_TAG_INFORMATION)), // 0x08, 0x00, + E(FileTrackingInformation, false, true, false, sizeof(FILE_TRACKING_INFORMATION)), // 0x00, 0x10, + E(FileIdBothDirectoryInformation, false, false, true, sizeof(FILE_ID_BOTH_DIR_INFORMATION)), // 0x00, 0x00, 0x70 + E(FileIdFullDirectoryInformation, false, false, true, sizeof(FILE_ID_FULL_DIR_INFORMATION)), // 0x00, 0x00, 0x58 + E(FileValidDataLengthInformation, false, true, false, sizeof(FILE_VALID_DATA_LENGTH_INFORMATION)), // 0x00, 0x08, + E(FileShortNameInformation, false, true, false, sizeof(FILE_NAME_INFORMATION)), // 0x00, 0x08, + E(FileIoCompletionNotificationInformation, true, true, false, sizeof(FILE_IO_COMPLETION_NOTIFICATION_INFORMATION)), // 0x04, 0x04, + E(FileIoStatusBlockRangeInformation, false, true, false, sizeof(IO_STATUS_BLOCK) /*?*/), // 0x00, 0x10, + E(FileIoPriorityHintInformation, true, true, false, sizeof(FILE_IO_PRIORITY_HINT_INFORMATION)), // 0x04, 0x04, + E(FileSfioReserveInformation, true, true, false, sizeof(FILE_SFIO_RESERVE_INFORMATION)), // 0x14, 0x14, + E(FileSfioVolumeInformation, true, false, false, sizeof(FILE_SFIO_VOLUME_INFORMATION)), // 0x0C, 0x00, + E(FileHardLinkInformation, true, false, false, sizeof(FILE_LINKS_INFORMATION)), // 0x20, 0x00, + E(FileProcessIdsUsingFileInformation, true, false, false, sizeof(FILE_PROCESS_IDS_USING_FILE_INFORMATION)), // 0x10, 0x00, + E(FileNormalizedNameInformation, true, false, false, sizeof(FILE_NAME_INFORMATION)), // 0x08, 0x00, + E(FileNetworkPhysicalNameInformation, true, false, false, sizeof(FILE_NETWORK_PHYSICAL_NAME_INFORMATION)), // 0x08, 0x00, + E(FileIdGlobalTxDirectoryInformation, false, false, true, sizeof(FILE_ID_GLOBAL_TX_DIR_INFORMATION)), // 0x00, 0x00, 0x60 + E(FileIsRemoteDeviceInformation, true, false, false, sizeof(FILE_IS_REMOTE_DEVICE_INFORMATION)), // 0x01, 0x00, + E(FileUnusedInformation, false, false, false, 0), // 0x00, 0x00, + E(FileNumaNodeInformation, true, false, false, sizeof(FILE_NUMA_NODE_INFORMATION)), // 0x02, 0x00, + E(FileStandardLinkInformation, true, false, false, sizeof(FILE_STANDARD_LINK_INFORMATION)), // 0x0C, 0x00, + E(FileRemoteProtocolInformation, true, false, false, sizeof(FILE_REMOTE_PROTOCOL_INFORMATION)), // 0x74, 0x00, + E(FileRenameInformationBypassAccessCheck, false, false, false, 0 /*kernel mode only*/), // 0x00, 0x00, + E(FileLinkInformationBypassAccessCheck, false, false, false, 0 /*kernel mode only*/), // 0x00, 0x00, + E(FileVolumeNameInformation, true, false, false, sizeof(FILE_VOLUME_NAME_INFORMATION)), // 0x08, 0x00, + E(FileIdInformation, true, false, false, sizeof(FILE_ID_INFORMATION)), // 0x18, 0x00, + E(FileIdExtdDirectoryInformation, false, false, true, sizeof(FILE_ID_EXTD_DIR_INFORMATION)), // 0x00, 0x00, 0x60 + E(FileReplaceCompletionInformation, false, true, false, sizeof(FILE_COMPLETION_INFORMATION)), // 0x00, 0x10, + E(FileHardLinkFullIdInformation, true, false, false, sizeof(FILE_LINK_ENTRY_FULL_ID_INFORMATION)), // 0x24, 0x00, + E(FileIdExtdBothDirectoryInformation, false, false, true, sizeof(FILE_ID_EXTD_BOTH_DIR_INFORMATION)), // 0x00, 0x00, 0x78 + E(FileDispositionInformationEx, false, true, false, sizeof(FILE_DISPOSITION_INFORMATION_EX)), // 0x00, 0x04, + E(FileRenameInformationEx, false, true, false, sizeof(FILE_RENAME_INFORMATION)), // 0x00, 0x18, + E(FileRenameInformationExBypassAccessCheck, false, false, false, 0 /*kernel mode only*/), // 0x00, 0x00, + E(FileDesiredStorageClassInformation, true, true, false, sizeof(FILE_DESIRED_STORAGE_CLASS_INFORMATION)), // 0x08, 0x08, + E(FileStatInformation, true, false, false, sizeof(FILE_STAT_INFORMATION)), // 0x48, 0x00, + E(FileMemoryPartitionInformation, false, true, false, 0x10), // 0x00, 0x10, + E(FileStatLxInformation, true, false, false, sizeof(FILE_STAT_LX_INFORMATION)), // 0x60, 0x00, + E(FileCaseSensitiveInformation, true, true, false, sizeof(FILE_CASE_SENSITIVE_INFORMATION)), // 0x04, 0x04, + E(FileLinkInformationEx, false, true, false, sizeof(FILE_LINK_INFORMATION)), // 0x00, 0x18, + E(FileLinkInformationExBypassAccessCheck, false, false, false, 0 /*kernel mode only*/), // 0x00, 0x00, + E(FileStorageReserveIdInformation, true, true, false, 0x04), // 0x04, 0x04, + E(FileCaseSensitiveInformationForceAccessCheck, true, true, false, sizeof(FILE_CASE_SENSITIVE_INFORMATION)), // 0x04, 0x04, +#undef E +}; + +void fsPerfNtQueryInfoFileWorker(HANDLE hNtFile1, uint32_t fType) +{ + char const chType = fType == RTFS_TYPE_DIRECTORY ? 'd' : 'r'; + + /** @todo may run out of buffer for really long paths? */ + union + { + uint8_t ab[4096]; + FILE_ACCESS_INFORMATION Access; + FILE_ALIGNMENT_INFORMATION Align; + FILE_ALL_INFORMATION All; + FILE_ALLOCATION_INFORMATION Alloc; + FILE_ATTRIBUTE_TAG_INFORMATION AttribTag; + FILE_BASIC_INFORMATION Basic; + FILE_BOTH_DIR_INFORMATION BothDir; + FILE_CASE_SENSITIVE_INFORMATION CaseSensitivity; + FILE_COMPLETION_INFORMATION Completion; + FILE_COMPRESSION_INFORMATION Compression; + FILE_DESIRED_STORAGE_CLASS_INFORMATION StorageClass; + FILE_DIRECTORY_INFORMATION Dir; + FILE_DISPOSITION_INFORMATION Disp; + FILE_DISPOSITION_INFORMATION_EX DispEx; + FILE_EA_INFORMATION Ea; + FILE_END_OF_FILE_INFORMATION EndOfFile; + FILE_FULL_DIR_INFORMATION FullDir; + FILE_FULL_EA_INFORMATION FullEa; + FILE_ID_BOTH_DIR_INFORMATION IdBothDir; + FILE_ID_EXTD_BOTH_DIR_INFORMATION ExtIdBothDir; + FILE_ID_EXTD_DIR_INFORMATION ExtIdDir; + FILE_ID_FULL_DIR_INFORMATION IdFullDir; + FILE_ID_GLOBAL_TX_DIR_INFORMATION IdGlobalTx; + FILE_ID_INFORMATION IdInfo; + FILE_INTERNAL_INFORMATION Internal; + FILE_IO_COMPLETION_NOTIFICATION_INFORMATION IoCompletion; + FILE_IO_PRIORITY_HINT_INFORMATION IoPrioHint; + FILE_IS_REMOTE_DEVICE_INFORMATION IsRemoteDev; + FILE_LINK_ENTRY_FULL_ID_INFORMATION LinkFullId; + FILE_LINK_INFORMATION Link; + FILE_MAILSLOT_QUERY_INFORMATION MailslotQuery; + FILE_MAILSLOT_SET_INFORMATION MailslotSet; + FILE_MODE_INFORMATION Mode; + FILE_MOVE_CLUSTER_INFORMATION MoveCluster; + FILE_NAME_INFORMATION Name; + FILE_NAMES_INFORMATION Names; + FILE_NETWORK_OPEN_INFORMATION NetOpen; + FILE_NUMA_NODE_INFORMATION Numa; + FILE_OBJECTID_INFORMATION ObjId; + FILE_PIPE_INFORMATION Pipe; + FILE_PIPE_LOCAL_INFORMATION PipeLocal; + FILE_PIPE_REMOTE_INFORMATION PipeRemote; + FILE_POSITION_INFORMATION Pos; + FILE_PROCESS_IDS_USING_FILE_INFORMATION Pids; + FILE_QUOTA_INFORMATION Quota; + FILE_REMOTE_PROTOCOL_INFORMATION RemoteProt; + FILE_RENAME_INFORMATION Rename; + FILE_REPARSE_POINT_INFORMATION Reparse; + FILE_SFIO_RESERVE_INFORMATION SfiRes; + FILE_SFIO_VOLUME_INFORMATION SfioVol; + FILE_STANDARD_INFORMATION Std; + FILE_STANDARD_LINK_INFORMATION StdLink; + FILE_STAT_INFORMATION Stat; + FILE_STAT_LX_INFORMATION StatLx; + FILE_STREAM_INFORMATION Stream; + FILE_TRACKING_INFORMATION Tracking; + FILE_VALID_DATA_LENGTH_INFORMATION ValidDataLen; + FILE_VOLUME_NAME_INFORMATION VolName; + } uBuf; + + IO_STATUS_BLOCK const VirginIos = RTNT_IO_STATUS_BLOCK_INITIALIZER; + for (unsigned i = 0; i < RT_ELEMENTS(g_aNtQueryInfoFileClasses); i++) + { + FILE_INFORMATION_CLASS const enmClass = (FILE_INFORMATION_CLASS)g_aNtQueryInfoFileClasses[i].enmValue; + const char * const pszClass = g_aNtQueryInfoFileClasses[i].pszName; + + memset(&uBuf, 0xff, sizeof(uBuf)); + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + ULONG cbBuf = sizeof(uBuf); + NTSTATUS rcNt = NtQueryInformationFile(hNtFile1, &Ios, &uBuf, cbBuf, enmClass); + if (NT_SUCCESS(rcNt)) + { + if (Ios.Status == VirginIos.Status || Ios.Information == VirginIos.Information) + RTTestIFailed("%s/%#x: I/O status block was not modified: %#x %#zx", pszClass, cbBuf, Ios.Status, Ios.Information); + else if (!g_aNtQueryInfoFileClasses[i].fQuery) + RTTestIFailed("%s/%#x: This isn't supposed to be queriable! (rcNt=%#x)", pszClass, cbBuf, rcNt); + else + { + ULONG const cbActualMin = enmClass != FileStorageReserveIdInformation ? Ios.Information : 4; /* weird */ + + switch (enmClass) + { + case FileNameInformation: + case FileAlternateNameInformation: + case FileShortNameInformation: + case FileNormalizedNameInformation: + case FileNetworkPhysicalNameInformation: + if ( RT_UOFFSETOF_DYN(FILE_NAME_INFORMATION, FileName[uBuf.Name.FileNameLength / sizeof(WCHAR)]) + != cbActualMin) + RTTestIFailed("%s/%#x: Wrong FileNameLength=%#x vs cbActual=%#x", + pszClass, cbActualMin, uBuf.Name.FileNameLength, cbActualMin); + if (uBuf.Name.FileName[uBuf.Name.FileNameLength / sizeof(WCHAR) - 1] == '\0') + RTTestIFailed("%s/%#x: Zero terminated name!", pszClass, cbActualMin); + if (g_uVerbosity > 1) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#x: FileNameLength=%#x FileName='%.*ls'\n", + pszClass, cbActualMin, uBuf.Name.FileNameLength, + uBuf.Name.FileNameLength / sizeof(WCHAR), uBuf.Name.FileName); + break; + + case FileVolumeNameInformation: + if (RT_UOFFSETOF_DYN(FILE_VOLUME_NAME_INFORMATION, + DeviceName[uBuf.VolName.DeviceNameLength / sizeof(WCHAR)]) != cbActualMin) + RTTestIFailed("%s/%#x: Wrong DeviceNameLength=%#x vs cbActual=%#x", + pszClass, cbActualMin, uBuf.VolName.DeviceNameLength, cbActualMin); + if (uBuf.VolName.DeviceName[uBuf.VolName.DeviceNameLength / sizeof(WCHAR) - 1] == '\0') + RTTestIFailed("%s/%#x: Zero terminated name!", pszClass, cbActualMin); + if (g_uVerbosity > 1) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#x: DeviceNameLength=%#x DeviceName='%.*ls'\n", + pszClass, cbActualMin, uBuf.VolName.DeviceNameLength, + uBuf.VolName.DeviceNameLength / sizeof(WCHAR), uBuf.VolName.DeviceName); + break; + default: + break; + } + + ULONG const cbMin = g_aNtQueryInfoFileClasses[i].cbMin; + ULONG const cbMax = RT_MIN(cbActualMin + 64, sizeof(uBuf)); + for (cbBuf = 0; cbBuf < cbMax; cbBuf++) + { + memset(&uBuf, 0xfe, sizeof(uBuf)); + RTNT_IO_STATUS_BLOCK_REINIT(&Ios); + rcNt = NtQueryInformationFile(hNtFile1, &Ios, &uBuf, cbBuf, enmClass); + if (!ASMMemIsAllU8(&uBuf.ab[cbBuf], sizeof(uBuf) - cbBuf, 0xfe)) + RTTestIFailed("%s/%#x: Touched memory beyond end of buffer (rcNt=%#x)", pszClass, cbBuf, rcNt); + if (cbBuf < cbMin) + { + if (rcNt != STATUS_INFO_LENGTH_MISMATCH) + RTTestIFailed("%s/%#x: %#x, expected STATUS_INFO_LENGTH_MISMATCH", pszClass, cbBuf, rcNt); + if (Ios.Status != VirginIos.Status || Ios.Information != VirginIos.Information) + RTTestIFailed("%s/%#x: I/O status block was modified (STATUS_INFO_LENGTH_MISMATCH): %#x %#zx", + pszClass, cbBuf, Ios.Status, Ios.Information); + } + else if (cbBuf < cbActualMin) + { + if ( rcNt != STATUS_BUFFER_OVERFLOW + /* RDR2/w10 returns success if the buffer can hold exactly the share name: */ + && !( rcNt == STATUS_SUCCESS + && enmClass == FileNetworkPhysicalNameInformation) + ) + RTTestIFailed("%s/%#x: %#x, expected STATUS_BUFFER_OVERFLOW", pszClass, cbBuf, rcNt); + /** @todo check name and length fields */ + } + else + { + if ( !ASMMemIsAllU8(&uBuf.ab[cbActualMin], sizeof(uBuf) - cbActualMin, 0xfe) + && enmClass != FileStorageReserveIdInformation /* NTFS bug? */ ) + RTTestIFailed("%s/%#x: Touched memory beyond returned length (cbActualMin=%#x, rcNt=%#x)", + pszClass, cbBuf, cbActualMin, rcNt); + + } + } + } + } + else + { + if (!g_aNtQueryInfoFileClasses[i].fQuery) + { + if ( rcNt != STATUS_INVALID_INFO_CLASS + && ( rcNt != STATUS_INVALID_PARAMETER /* w7rtm-32 result */ + || enmClass != FileUnusedInformation)) + RTTestIFailed("%s/%#x/%c: %#x, expected STATUS_INVALID_INFO_CLASS", pszClass, cbBuf, chType, rcNt); + } + else if ( rcNt != STATUS_INVALID_INFO_CLASS + && rcNt != STATUS_INVALID_PARAMETER + && !(rcNt == STATUS_OBJECT_NAME_NOT_FOUND && enmClass == FileAlternateNameInformation) + && !( rcNt == STATUS_ACCESS_DENIED + && ( enmClass == FileIoPriorityHintInformation + || enmClass == FileSfioReserveInformation + || enmClass == FileStatLxInformation)) + && !(rcNt == STATUS_NO_SUCH_DEVICE && enmClass == FileNumaNodeInformation) + && !( rcNt == STATUS_NOT_SUPPORTED /* RDR2/W10-17763 */ + && ( enmClass == FileMailslotQueryInformation + || enmClass == FileObjectIdInformation + || enmClass == FileReparsePointInformation + || enmClass == FileSfioVolumeInformation + || enmClass == FileHardLinkInformation + || enmClass == FileStandardLinkInformation + || enmClass == FileHardLinkFullIdInformation + || enmClass == FileDesiredStorageClassInformation + || enmClass == FileStatInformation + || enmClass == FileCaseSensitiveInformation + || enmClass == FileStorageReserveIdInformation + || enmClass == FileCaseSensitiveInformationForceAccessCheck) + || ( fType == RTFS_TYPE_DIRECTORY + && (enmClass == FileSfioReserveInformation || enmClass == FileStatLxInformation))) + && !(rcNt == STATUS_INVALID_DEVICE_REQUEST && fType == RTFS_TYPE_FILE) + ) + RTTestIFailed("%s/%#x/%c: %#x", pszClass, cbBuf, chType, rcNt); + if ( (Ios.Status != VirginIos.Status || Ios.Information != VirginIos.Information) + && !(fType == RTFS_TYPE_DIRECTORY && Ios.Status == rcNt && Ios.Information == 0) /* NTFS/W10-17763 */ + && !( enmClass == FileUnusedInformation + && Ios.Status == rcNt && Ios.Information == sizeof(uBuf)) /* NTFS/VBoxSF/w7rtm */ ) + RTTestIFailed("%s/%#x/%c: I/O status block was modified: %#x %#zx", + pszClass, cbBuf, chType, Ios.Status, Ios.Information); + if (!ASMMemIsAllU8(&uBuf, sizeof(uBuf), 0xff)) + RTTestIFailed("%s/%#x/%c: Buffer was touched in failure case!", pszClass, cbBuf, chType); + } + } +} + +void fsPerfNtQueryInfoFile(void) +{ + RTTestISub("NtQueryInformationFile"); + + /* On a regular file: */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file2qif")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE), VINF_SUCCESS); + fsPerfNtQueryInfoFileWorker((HANDLE)RTFileToNative(hFile1), RTFS_TYPE_FILE); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + /* On a directory: */ + HANDLE hDir1 = INVALID_HANDLE_VALUE; + RTTESTI_CHECK_RC_RETV(RTNtPathOpenDir(InDir(RT_STR_TUPLE("")), GENERIC_READ | SYNCHRONIZE | FILE_SYNCHRONOUS_IO_NONALERT, + FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_OPEN, 0, &hDir1, NULL), VINF_SUCCESS); + fsPerfNtQueryInfoFileWorker(hDir1, RTFS_TYPE_DIRECTORY); + RTTESTI_CHECK(CloseHandle(hDir1) == TRUE); +} + + +/** + * Nt(Query|Set)VolumeInformationFile) information class info. + */ +static const struct +{ + const char *pszName; + int enmValue; + bool fQuery; + bool fSet; + uint8_t cbMin; +} g_aNtQueryVolInfoFileClasses[] = +{ +#define E(a_enmValue, a_fQuery, a_fSet, a_cbMin) \ + { #a_enmValue, a_enmValue, a_fQuery, a_fSet, a_cbMin } + { "invalid0", 0, false, false, 0 }, + E(FileFsVolumeInformation, 1, 0, sizeof(FILE_FS_VOLUME_INFORMATION)), + E(FileFsLabelInformation, 0, 1, sizeof(FILE_FS_LABEL_INFORMATION)), + E(FileFsSizeInformation, 1, 0, sizeof(FILE_FS_SIZE_INFORMATION)), + E(FileFsDeviceInformation, 1, 0, sizeof(FILE_FS_DEVICE_INFORMATION)), + E(FileFsAttributeInformation, 1, 0, sizeof(FILE_FS_ATTRIBUTE_INFORMATION)), + E(FileFsControlInformation, 1, 1, sizeof(FILE_FS_CONTROL_INFORMATION)), + E(FileFsFullSizeInformation, 1, 0, sizeof(FILE_FS_FULL_SIZE_INFORMATION)), + E(FileFsObjectIdInformation, 1, 1, sizeof(FILE_FS_OBJECTID_INFORMATION)), + E(FileFsDriverPathInformation, 1, 0, sizeof(FILE_FS_DRIVER_PATH_INFORMATION)), + E(FileFsVolumeFlagsInformation, 1, 1, sizeof(FILE_FS_VOLUME_FLAGS_INFORMATION)), + E(FileFsSectorSizeInformation, 1, 0, sizeof(FILE_FS_SECTOR_SIZE_INFORMATION)), + E(FileFsDataCopyInformation, 1, 0, sizeof(FILE_FS_DATA_COPY_INFORMATION)), + E(FileFsMetadataSizeInformation, 1, 0, sizeof(FILE_FS_METADATA_SIZE_INFORMATION)), + E(FileFsFullSizeInformationEx, 1, 0, sizeof(FILE_FS_FULL_SIZE_INFORMATION_EX)), +#undef E +}; + +void fsPerfNtQueryVolInfoFileWorker(HANDLE hNtFile1, uint32_t fType) +{ + char const chType = fType == RTFS_TYPE_DIRECTORY ? 'd' : 'r'; + union + { + uint8_t ab[4096]; + FILE_FS_VOLUME_INFORMATION Vol; + FILE_FS_LABEL_INFORMATION Label; + FILE_FS_SIZE_INFORMATION Size; + FILE_FS_DEVICE_INFORMATION Dev; + FILE_FS_ATTRIBUTE_INFORMATION Attrib; + FILE_FS_CONTROL_INFORMATION Ctrl; + FILE_FS_FULL_SIZE_INFORMATION FullSize; + FILE_FS_OBJECTID_INFORMATION ObjId; + FILE_FS_DRIVER_PATH_INFORMATION DrvPath; + FILE_FS_VOLUME_FLAGS_INFORMATION VolFlags; + FILE_FS_SECTOR_SIZE_INFORMATION SectorSize; + FILE_FS_DATA_COPY_INFORMATION DataCopy; + FILE_FS_METADATA_SIZE_INFORMATION Metadata; + FILE_FS_FULL_SIZE_INFORMATION_EX FullSizeEx; + } uBuf; + + IO_STATUS_BLOCK const VirginIos = RTNT_IO_STATUS_BLOCK_INITIALIZER; + for (unsigned i = 0; i < RT_ELEMENTS(g_aNtQueryVolInfoFileClasses); i++) + { + FS_INFORMATION_CLASS const enmClass = (FS_INFORMATION_CLASS)g_aNtQueryVolInfoFileClasses[i].enmValue; + const char * const pszClass = g_aNtQueryVolInfoFileClasses[i].pszName; + + memset(&uBuf, 0xff, sizeof(uBuf)); + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + ULONG cbBuf = sizeof(uBuf); + NTSTATUS rcNt = NtQueryVolumeInformationFile(hNtFile1, &Ios, &uBuf, cbBuf, enmClass); + if (g_uVerbosity > 3) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#04x/%c: rcNt=%#x Ios.Status=%#x Info=%#zx\n", + pszClass, cbBuf, chType, rcNt, Ios.Status, Ios.Information); + if (NT_SUCCESS(rcNt)) + { + if (Ios.Status == VirginIos.Status || Ios.Information == VirginIos.Information) + RTTestIFailed("%s/%#x/%c: I/O status block was not modified: %#x %#zx", + pszClass, cbBuf, chType, Ios.Status, Ios.Information); + else if (!g_aNtQueryVolInfoFileClasses[i].fQuery) + RTTestIFailed("%s/%#x/%c: This isn't supposed to be queriable! (rcNt=%#x)", pszClass, cbBuf, chType, rcNt); + else + { + ULONG const cbActualMin = Ios.Information; + ULONG *pcbName = NULL; + ULONG offName = 0; + + switch (enmClass) + { + case FileFsVolumeInformation: + pcbName = &uBuf.Vol.VolumeLabelLength; + offName = RT_UOFFSETOF(FILE_FS_VOLUME_INFORMATION, VolumeLabel); + if (RT_UOFFSETOF_DYN(FILE_FS_VOLUME_INFORMATION, + VolumeLabel[uBuf.Vol.VolumeLabelLength / sizeof(WCHAR)]) != cbActualMin) + RTTestIFailed("%s/%#x/%c: Wrong VolumeLabelLength=%#x vs cbActual=%#x", + pszClass, cbActualMin, chType, uBuf.Vol.VolumeLabelLength, cbActualMin); + if (uBuf.Vol.VolumeLabel[uBuf.Vol.VolumeLabelLength / sizeof(WCHAR) - 1] == '\0') + RTTestIFailed("%s/%#x/%c: Zero terminated name!", pszClass, cbActualMin, chType); + if (g_uVerbosity > 1) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#04x/%c: VolumeLabelLength=%#x VolumeLabel='%.*ls'\n", + pszClass, cbActualMin, chType, uBuf.Vol.VolumeLabelLength, + uBuf.Vol.VolumeLabelLength / sizeof(WCHAR), uBuf.Vol.VolumeLabel); + break; + + case FileFsAttributeInformation: + pcbName = &uBuf.Attrib.FileSystemNameLength; + offName = RT_UOFFSETOF(FILE_FS_ATTRIBUTE_INFORMATION, FileSystemName); + if (RT_UOFFSETOF_DYN(FILE_FS_ATTRIBUTE_INFORMATION, + FileSystemName[uBuf.Attrib.FileSystemNameLength / sizeof(WCHAR)]) != cbActualMin) + RTTestIFailed("%s/%#x/%c: Wrong FileSystemNameLength=%#x vs cbActual=%#x", + pszClass, cbActualMin, chType, uBuf.Attrib.FileSystemNameLength, cbActualMin); + if (uBuf.Attrib.FileSystemName[uBuf.Attrib.FileSystemNameLength / sizeof(WCHAR) - 1] == '\0') + RTTestIFailed("%s/%#x/%c: Zero terminated name!", pszClass, cbActualMin, chType); + if (g_uVerbosity > 1) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#04x/%c: FileSystemNameLength=%#x FileSystemName='%.*ls' Attribs=%#x MaxCompName=%#x\n", + pszClass, cbActualMin, chType, uBuf.Attrib.FileSystemNameLength, + uBuf.Attrib.FileSystemNameLength / sizeof(WCHAR), uBuf.Attrib.FileSystemName, + uBuf.Attrib.FileSystemAttributes, uBuf.Attrib.MaximumComponentNameLength); + break; + + case FileFsDriverPathInformation: + pcbName = &uBuf.DrvPath.DriverNameLength; + offName = RT_UOFFSETOF(FILE_FS_DRIVER_PATH_INFORMATION, DriverName); + if (RT_UOFFSETOF_DYN(FILE_FS_DRIVER_PATH_INFORMATION, + DriverName[uBuf.DrvPath.DriverNameLength / sizeof(WCHAR)]) != cbActualMin) + RTTestIFailed("%s/%#x/%c: Wrong DriverNameLength=%#x vs cbActual=%#x", + pszClass, cbActualMin, chType, uBuf.DrvPath.DriverNameLength, cbActualMin); + if (uBuf.DrvPath.DriverName[uBuf.DrvPath.DriverNameLength / sizeof(WCHAR) - 1] == '\0') + RTTestIFailed("%s/%#x/%c: Zero terminated name!", pszClass, cbActualMin, chType); + if (g_uVerbosity > 1) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#04x/%c: DriverNameLength=%#x DriverName='%.*ls'\n", + pszClass, cbActualMin, chType, uBuf.DrvPath.DriverNameLength, + uBuf.DrvPath.DriverNameLength / sizeof(WCHAR), uBuf.DrvPath.DriverName); + break; + + case FileFsSectorSizeInformation: + if (g_uVerbosity > 1) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#04x/%c: Flags=%#x log=%#x atomic=%#x perf=%#x eff=%#x offSec=%#x offPart=%#x\n", + pszClass, cbActualMin, chType, uBuf.SectorSize.Flags, + uBuf.SectorSize.LogicalBytesPerSector, + uBuf.SectorSize.PhysicalBytesPerSectorForAtomicity, + uBuf.SectorSize.PhysicalBytesPerSectorForPerformance, + uBuf.SectorSize.FileSystemEffectivePhysicalBytesPerSectorForAtomicity, + uBuf.SectorSize.ByteOffsetForSectorAlignment, + uBuf.SectorSize.ByteOffsetForPartitionAlignment); + break; + + default: + if (g_uVerbosity > 2) + RTTestIPrintf(RTTESTLVL_ALWAYS, "%+34s/%#04x/%c:\n", pszClass, cbActualMin, chType); + break; + } + ULONG const cbName = pcbName ? *pcbName : 0; + uint8_t abNameCopy[4096]; + RT_ZERO(abNameCopy); + if (pcbName) + memcpy(abNameCopy, &uBuf.ab[offName], cbName); + + ULONG const cbMin = g_aNtQueryVolInfoFileClasses[i].cbMin; + ULONG const cbMax = RT_MIN(cbActualMin + 64, sizeof(uBuf)); + for (cbBuf = 0; cbBuf < cbMax; cbBuf++) + { + memset(&uBuf, 0xfe, sizeof(uBuf)); + RTNT_IO_STATUS_BLOCK_REINIT(&Ios); + rcNt = NtQueryVolumeInformationFile(hNtFile1, &Ios, &uBuf, cbBuf, enmClass); + if (!ASMMemIsAllU8(&uBuf.ab[cbBuf], sizeof(uBuf) - cbBuf, 0xfe)) + RTTestIFailed("%s/%#x/%c: Touched memory beyond end of buffer (rcNt=%#x)", pszClass, cbBuf, chType, rcNt); + if (cbBuf < cbMin) + { + if (rcNt != STATUS_INFO_LENGTH_MISMATCH) + RTTestIFailed("%s/%#x/%c: %#x, expected STATUS_INFO_LENGTH_MISMATCH", pszClass, cbBuf, chType, rcNt); + if (Ios.Status != VirginIos.Status || Ios.Information != VirginIos.Information) + RTTestIFailed("%s/%#x/%c: I/O status block was modified (STATUS_INFO_LENGTH_MISMATCH): %#x %#zx", + pszClass, cbBuf, chType, Ios.Status, Ios.Information); + } + else if (cbBuf < cbActualMin) + { + if (rcNt != STATUS_BUFFER_OVERFLOW) + RTTestIFailed("%s/%#x/%c: %#x, expected STATUS_BUFFER_OVERFLOW", pszClass, cbBuf, chType, rcNt); + if (pcbName) + { + size_t const cbNameAlt = offName < cbBuf ? cbBuf - offName : 0; + if ( *pcbName != cbName + && !( *pcbName == cbNameAlt + && (enmClass == FileFsAttributeInformation /*NTFS,FAT*/))) + RTTestIFailed("%s/%#x/%c: Wrong name length: %#x, expected %#x (or %#x)", + pszClass, cbBuf, chType, *pcbName, cbName, cbNameAlt); + if (memcmp(abNameCopy, &uBuf.ab[offName], cbNameAlt) != 0) + RTTestIFailed("%s/%#x/%c: Wrong partial name: %.*Rhxs", + pszClass, cbBuf, chType, cbNameAlt, &uBuf.ab[offName]); + } + if (Ios.Information != cbBuf) + RTTestIFailed("%s/%#x/%c: Ios.Information = %#x, expected %#x", + pszClass, cbBuf, chType, Ios.Information, cbBuf); + } + else + { + if ( !ASMMemIsAllU8(&uBuf.ab[cbActualMin], sizeof(uBuf) - cbActualMin, 0xfe) + && enmClass != FileStorageReserveIdInformation /* NTFS bug? */ ) + RTTestIFailed("%s/%#x/%c: Touched memory beyond returned length (cbActualMin=%#x, rcNt=%#x)", + pszClass, cbBuf, chType, cbActualMin, rcNt); + if (pcbName && *pcbName != cbName) + RTTestIFailed("%s/%#x/%c: Wrong name length: %#x, expected %#x", + pszClass, cbBuf, chType, *pcbName, cbName); + if (pcbName && memcmp(abNameCopy, &uBuf.ab[offName], cbName) != 0) + RTTestIFailed("%s/%#x/%c: Wrong name: %.*Rhxs", + pszClass, cbBuf, chType, cbName, &uBuf.ab[offName]); + } + } + } + } + else + { + if (!g_aNtQueryVolInfoFileClasses[i].fQuery) + { + if (rcNt != STATUS_INVALID_INFO_CLASS) + RTTestIFailed("%s/%#x/%c: %#x, expected STATUS_INVALID_INFO_CLASS", pszClass, cbBuf, chType, rcNt); + } + else if ( rcNt != STATUS_INVALID_INFO_CLASS + && rcNt != STATUS_INVALID_PARAMETER + && !(rcNt == STATUS_ACCESS_DENIED && enmClass == FileFsControlInformation /* RDR2/W10 */) + && !(rcNt == STATUS_OBJECT_NAME_NOT_FOUND && enmClass == FileFsObjectIdInformation /* RDR2/W10 */) + ) + RTTestIFailed("%s/%#x/%c: %#x", pszClass, cbBuf, chType, rcNt); + if ( (Ios.Status != VirginIos.Status || Ios.Information != VirginIos.Information) + && !( Ios.Status == 0 && Ios.Information == 0 + && fType == RTFS_TYPE_DIRECTORY + && ( enmClass == FileFsObjectIdInformation /* RDR2+NTFS on W10 */ + || enmClass == FileFsControlInformation /* RDR2 on W10 */ + || enmClass == FileFsVolumeFlagsInformation /* RDR2+NTFS on W10 */ + || enmClass == FileFsDataCopyInformation /* RDR2 on W10 */ + || enmClass == FileFsMetadataSizeInformation /* RDR2+NTFS on W10 */ + || enmClass == FileFsFullSizeInformationEx /* RDR2 on W10 */ + ) ) + ) + RTTestIFailed("%s/%#x/%c: I/O status block was modified: %#x %#zx (rcNt=%#x)", + pszClass, cbBuf, chType, Ios.Status, Ios.Information, rcNt); + if (!ASMMemIsAllU8(&uBuf, sizeof(uBuf), 0xff)) + RTTestIFailed("%s/%#x/%c: Buffer was touched in failure case!", pszClass, cbBuf, chType); + } + } + RT_NOREF(fType); +} + +void fsPerfNtQueryVolInfoFile(void) +{ + RTTestISub("NtQueryVolumeInformationFile"); + + /* On a regular file: */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file2qvif")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE), VINF_SUCCESS); + fsPerfNtQueryVolInfoFileWorker((HANDLE)RTFileToNative(hFile1), RTFS_TYPE_FILE); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + /* On a directory: */ + HANDLE hDir1 = INVALID_HANDLE_VALUE; + RTTESTI_CHECK_RC_RETV(RTNtPathOpenDir(InDir(RT_STR_TUPLE("")), GENERIC_READ | SYNCHRONIZE | FILE_SYNCHRONOUS_IO_NONALERT, + FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_OPEN, 0, &hDir1, NULL), VINF_SUCCESS); + fsPerfNtQueryVolInfoFileWorker(hDir1, RTFS_TYPE_DIRECTORY); + RTTESTI_CHECK(CloseHandle(hDir1) == TRUE); + + /* On a regular file opened for reading: */ + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file2qvif")), + RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS); + fsPerfNtQueryVolInfoFileWorker((HANDLE)RTFileToNative(hFile1), RTFS_TYPE_FILE); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); +} + +#endif /* RT_OS_WINDOWS */ + +void fsPerfFChMod(void) +{ + RTTestISub("fchmod"); + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file4")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTFSOBJINFO ObjInfo = {0}; + RTTESTI_CHECK_RC(RTFileQueryInfo(hFile1, &ObjInfo, RTFSOBJATTRADD_NOTHING), VINF_SUCCESS); + RTFMODE const fEvenMode = (ObjInfo.Attr.fMode & ~RTFS_UNIX_ALL_ACCESS_PERMS) | RTFS_DOS_READONLY | 0400; + RTFMODE const fOddMode = (ObjInfo.Attr.fMode & ~(RTFS_UNIX_ALL_ACCESS_PERMS | RTFS_DOS_READONLY)) | 0640; + PROFILE_FN(RTFileSetMode(hFile1, iIteration & 1 ? fOddMode : fEvenMode), g_nsTestRun, "RTFileSetMode"); + + RTFileSetMode(hFile1, ObjInfo.Attr.fMode); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); +} + + +void fsPerfFUtimes(void) +{ + RTTestISub("futimes"); + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file5")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTIMESPEC Time1; + RTTimeNow(&Time1); + RTTIMESPEC Time2 = Time1; + RTTimeSpecSubSeconds(&Time2, 3636); + + RTFSOBJINFO ObjInfo0 = {0}; + RTTESTI_CHECK_RC(RTFileQueryInfo(hFile1, &ObjInfo0, RTFSOBJATTRADD_NOTHING), VINF_SUCCESS); + + /* Modify modification time: */ + RTTESTI_CHECK_RC(RTFileSetTimes(hFile1, NULL, &Time2, NULL, NULL), VINF_SUCCESS); + RTFSOBJINFO ObjInfo1 = {0}; + RTTESTI_CHECK_RC(RTFileQueryInfo(hFile1, &ObjInfo1, RTFSOBJATTRADD_NOTHING), VINF_SUCCESS); + RTTESTI_CHECK((RTTimeSpecGetSeconds(&ObjInfo1.ModificationTime) >> 2) == (RTTimeSpecGetSeconds(&Time2) >> 2)); + char sz1[RTTIME_STR_LEN], sz2[RTTIME_STR_LEN]; /* Div by 1000 here for posix impl. using timeval. */ + RTTESTI_CHECK_MSG(RTTimeSpecGetNano(&ObjInfo1.AccessTime) / 1000 == RTTimeSpecGetNano(&ObjInfo0.AccessTime) / 1000, + ("%s, expected %s", RTTimeSpecToString(&ObjInfo1.AccessTime, sz1, sizeof(sz1)), + RTTimeSpecToString(&ObjInfo0.AccessTime, sz2, sizeof(sz2)))); + + /* Modify access time: */ + RTTESTI_CHECK_RC(RTFileSetTimes(hFile1, &Time1, NULL, NULL, NULL), VINF_SUCCESS); + RTFSOBJINFO ObjInfo2 = {0}; + RTTESTI_CHECK_RC(RTFileQueryInfo(hFile1, &ObjInfo2, RTFSOBJATTRADD_NOTHING), VINF_SUCCESS); + RTTESTI_CHECK((RTTimeSpecGetSeconds(&ObjInfo2.AccessTime) >> 2) == (RTTimeSpecGetSeconds(&Time1) >> 2)); + RTTESTI_CHECK(RTTimeSpecGetNano(&ObjInfo2.ModificationTime) / 1000 == RTTimeSpecGetNano(&ObjInfo1.ModificationTime) / 1000); + + /* Benchmark it: */ + PROFILE_FN(RTFileSetTimes(hFile1, NULL, iIteration & 1 ? &Time1 : &Time2, NULL, NULL), g_nsTestRun, "RTFileSetTimes"); + + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); +} + + +void fsPerfStat(void) +{ + RTTestISub("stat"); + RTFSOBJINFO ObjInfo; + + /* Non-existing files. */ + RTTESTI_CHECK_RC(RTPathQueryInfoEx(InEmptyDir(RT_STR_TUPLE("no-such-file")), + &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTPathQueryInfoEx(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file")), + &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTPathQueryInfoEx(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file")), + &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), VERR_PATH_NOT_FOUND); + + /* Shallow: */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file3")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + PROFILE_FN(RTPathQueryInfoEx(g_szDir, &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), g_nsTestRun, + "RTPathQueryInfoEx/NOTHING"); + PROFILE_FN(RTPathQueryInfoEx(g_szDir, &ObjInfo, RTFSOBJATTRADD_UNIX, RTPATH_F_ON_LINK), g_nsTestRun, + "RTPathQueryInfoEx/UNIX"); + + + /* Deep: */ + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDeepDir(RT_STR_TUPLE("file3")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + PROFILE_FN(RTPathQueryInfoEx(g_szDeepDir, &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), g_nsTestRun, + "RTPathQueryInfoEx/deep/NOTHING"); + PROFILE_FN(RTPathQueryInfoEx(g_szDeepDir, &ObjInfo, RTFSOBJATTRADD_UNIX, RTPATH_F_ON_LINK), g_nsTestRun, + "RTPathQueryInfoEx/deep/UNIX"); + + /* Manytree: */ + char szPath[FSPERF_MAX_PATH]; + PROFILE_MANYTREE_FN(szPath, RTPathQueryInfoEx(szPath, &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), + 1, g_nsTestRun, "RTPathQueryInfoEx/manytree/NOTHING"); + PROFILE_MANYTREE_FN(szPath, RTPathQueryInfoEx(szPath, &ObjInfo, RTFSOBJATTRADD_UNIX, RTPATH_F_ON_LINK), + 1, g_nsTestRun, "RTPathQueryInfoEx/manytree/UNIX"); +} + + +void fsPerfChmod(void) +{ + RTTestISub("chmod"); + + /* Non-existing files. */ + RTTESTI_CHECK_RC(RTPathSetMode(InEmptyDir(RT_STR_TUPLE("no-such-file")), 0665), + VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTPathSetMode(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file")), 0665), + FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTPathSetMode(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file")), 0665), VERR_PATH_NOT_FOUND); + + /* Shallow: */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file14")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + RTFSOBJINFO ObjInfo; + RTTESTI_CHECK_RC(RTPathQueryInfoEx(g_szDir, &ObjInfo, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), VINF_SUCCESS); + RTFMODE const fEvenMode = (ObjInfo.Attr.fMode & ~RTFS_UNIX_ALL_ACCESS_PERMS) | RTFS_DOS_READONLY | 0400; + RTFMODE const fOddMode = (ObjInfo.Attr.fMode & ~(RTFS_UNIX_ALL_ACCESS_PERMS | RTFS_DOS_READONLY)) | 0640; + PROFILE_FN(RTPathSetMode(g_szDir, iIteration & 1 ? fOddMode : fEvenMode), g_nsTestRun, "RTPathSetMode"); + RTPathSetMode(g_szDir, ObjInfo.Attr.fMode); + + /* Deep: */ + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDeepDir(RT_STR_TUPLE("file14")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + PROFILE_FN(RTPathSetMode(g_szDeepDir, iIteration & 1 ? fOddMode : fEvenMode), g_nsTestRun, "RTPathSetMode/deep"); + RTPathSetMode(g_szDeepDir, ObjInfo.Attr.fMode); + + /* Manytree: */ + char szPath[FSPERF_MAX_PATH]; + PROFILE_MANYTREE_FN(szPath, RTPathSetMode(szPath, iIteration & 1 ? fOddMode : fEvenMode), 1, g_nsTestRun, + "RTPathSetMode/manytree"); + DO_MANYTREE_FN(szPath, RTPathSetMode(szPath, ObjInfo.Attr.fMode)); +} + + +void fsPerfUtimes(void) +{ + RTTestISub("utimes"); + + RTTIMESPEC Time1; + RTTimeNow(&Time1); + RTTIMESPEC Time2 = Time1; + RTTimeSpecSubSeconds(&Time2, 3636); + + /* Non-existing files. */ + RTTESTI_CHECK_RC(RTPathSetTimesEx(InEmptyDir(RT_STR_TUPLE("no-such-file")), NULL, &Time1, NULL, NULL, RTPATH_F_ON_LINK), + VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTPathSetTimesEx(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file")), + NULL, &Time1, NULL, NULL, RTPATH_F_ON_LINK), + FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTPathSetTimesEx(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file")), + NULL, &Time1, NULL, NULL, RTPATH_F_ON_LINK), + VERR_PATH_NOT_FOUND); + + /* Shallow: */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file15")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + RTFSOBJINFO ObjInfo0 = {0}; + RTTESTI_CHECK_RC(RTPathQueryInfoEx(g_szDir, &ObjInfo0, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), VINF_SUCCESS); + + /* Modify modification time: */ + RTTESTI_CHECK_RC(RTPathSetTimesEx(g_szDir, NULL, &Time2, NULL, NULL, RTPATH_F_ON_LINK), VINF_SUCCESS); + RTFSOBJINFO ObjInfo1; + RTTESTI_CHECK_RC(RTPathQueryInfoEx(g_szDir, &ObjInfo1, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), VINF_SUCCESS); + RTTESTI_CHECK((RTTimeSpecGetSeconds(&ObjInfo1.ModificationTime) >> 2) == (RTTimeSpecGetSeconds(&Time2) >> 2)); + RTTESTI_CHECK(RTTimeSpecGetNano(&ObjInfo1.AccessTime) / 1000 == RTTimeSpecGetNano(&ObjInfo0.AccessTime) / 1000 /* posix timeval */); + + /* Modify access time: */ + RTTESTI_CHECK_RC(RTPathSetTimesEx(g_szDir, &Time1, NULL, NULL, NULL, RTPATH_F_ON_LINK), VINF_SUCCESS); + RTFSOBJINFO ObjInfo2 = {0}; + RTTESTI_CHECK_RC(RTPathQueryInfoEx(g_szDir, &ObjInfo2, RTFSOBJATTRADD_NOTHING, RTPATH_F_ON_LINK), VINF_SUCCESS); + RTTESTI_CHECK((RTTimeSpecGetSeconds(&ObjInfo2.AccessTime) >> 2) == (RTTimeSpecGetSeconds(&Time1) >> 2)); + RTTESTI_CHECK(RTTimeSpecGetNano(&ObjInfo2.ModificationTime) / 1000 == RTTimeSpecGetNano(&ObjInfo1.ModificationTime) / 1000 /* posix timeval */); + + /* Profile shallow: */ + PROFILE_FN(RTPathSetTimesEx(g_szDir, iIteration & 1 ? &Time1 : &Time2, iIteration & 1 ? &Time2 : &Time1, + NULL, NULL, RTPATH_F_ON_LINK), + g_nsTestRun, "RTPathSetTimesEx"); + + /* Deep: */ + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDeepDir(RT_STR_TUPLE("file15")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + PROFILE_FN(RTPathSetTimesEx(g_szDeepDir, iIteration & 1 ? &Time1 : &Time2, iIteration & 1 ? &Time2 : &Time1, + NULL, NULL, RTPATH_F_ON_LINK), + g_nsTestRun, "RTPathSetTimesEx/deep"); + + /* Manytree: */ + char szPath[FSPERF_MAX_PATH]; + PROFILE_MANYTREE_FN(szPath, RTPathSetTimesEx(szPath, iIteration & 1 ? &Time1 : &Time2, iIteration & 1 ? &Time2 : &Time1, + NULL, NULL, RTPATH_F_ON_LINK), + 1, g_nsTestRun, "RTPathSetTimesEx/manytree"); +} + + +DECL_FORCE_INLINE(int) fsPerfRenameMany(const char *pszFile, uint32_t iIteration) +{ + char szRenamed[FSPERF_MAX_PATH]; + strcat(strcpy(szRenamed, pszFile), "-renamed"); + if (!(iIteration & 1)) + return RTPathRename(pszFile, szRenamed, 0); + return RTPathRename(szRenamed, pszFile, 0); +} + + +void fsPerfRename(void) +{ + RTTestISub("rename"); + char szPath[FSPERF_MAX_PATH]; + +/** @todo rename directories too! */ +/** @todo check overwriting files and directoris (empty ones should work on + * unix). */ + + /* Non-existing files. */ + strcpy(szPath, InEmptyDir(RT_STR_TUPLE("other-no-such-file"))); + RTTESTI_CHECK_RC(RTPathRename(InEmptyDir(RT_STR_TUPLE("no-such-file")), szPath, 0), VERR_FILE_NOT_FOUND); + strcpy(szPath, InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "other-no-such-file"))); + RTTESTI_CHECK_RC(RTPathRename(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file")), szPath, 0), + FSPERF_VERR_PATH_NOT_FOUND); + strcpy(szPath, InEmptyDir(RT_STR_TUPLE("other-no-such-file"))); + RTTESTI_CHECK_RC(RTPathRename(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file")), szPath, 0), VERR_PATH_NOT_FOUND); + + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file16")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + strcat(strcpy(szPath, g_szDir), "-no-such-dir" RTPATH_SLASH_STR "file16"); + RTTESTI_CHECK_RC(RTPathRename(szPath, g_szDir, 0), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTPathRename(g_szDir, szPath, 0), FSPERF_VERR_PATH_NOT_FOUND); + + /* Shallow: */ + strcat(strcpy(szPath, g_szDir), "-other"); + PROFILE_FN(RTPathRename(iIteration & 1 ? szPath : g_szDir, iIteration & 1 ? g_szDir : szPath, 0), g_nsTestRun, "RTPathRename"); + + /* Deep: */ + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDeepDir(RT_STR_TUPLE("file15")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + strcat(strcpy(szPath, g_szDeepDir), "-other"); + PROFILE_FN(RTPathRename(iIteration & 1 ? szPath : g_szDeepDir, iIteration & 1 ? g_szDeepDir : szPath, 0), + g_nsTestRun, "RTPathRename/deep"); + + /* Manytree: */ + PROFILE_MANYTREE_FN(szPath, fsPerfRenameMany(szPath, iIteration), 2, g_nsTestRun, "RTPathRename/manytree"); +} + + +/** + * Wrapper around RTDirOpen/RTDirOpenFiltered which takes g_fRelativeDir into + * account. + */ +DECL_FORCE_INLINE(int) fsPerfOpenDirWrap(PRTDIR phDir, const char *pszPath) +{ + if (!g_fRelativeDir) + return RTDirOpen(phDir, pszPath); + return RTDirOpenFiltered(phDir, pszPath, RTDIRFILTER_NONE, RTDIR_F_NO_ABS_PATH); +} + + +DECL_FORCE_INLINE(int) fsPerfOpenClose(const char *pszDir) +{ + RTDIR hDir; + RTTESTI_CHECK_RC_RET(fsPerfOpenDirWrap(&hDir, pszDir), VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RC(RTDirClose(hDir), VINF_SUCCESS); + return VINF_SUCCESS; +} + + +void vsPerfDirOpen(void) +{ + RTTestISub("dir open"); + RTDIR hDir; + + /* + * Non-existing files. + */ + RTTESTI_CHECK_RC(fsPerfOpenDirWrap(&hDir, InEmptyDir(RT_STR_TUPLE("no-such-file"))), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(fsPerfOpenDirWrap(&hDir, InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file"))), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(fsPerfOpenDirWrap(&hDir, InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file"))), VERR_PATH_NOT_FOUND); + + /* + * Check that open + close works. + */ + g_szEmptyDir[g_cchEmptyDir] = '\0'; + RTTESTI_CHECK_RC_RETV(fsPerfOpenDirWrap(&hDir, g_szEmptyDir), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTDirClose(hDir), VINF_SUCCESS); + + + /* + * Profile empty dir and dir with many files. + */ + g_szEmptyDir[g_cchEmptyDir] = '\0'; + PROFILE_FN(fsPerfOpenClose(g_szEmptyDir), g_nsTestRun, "RTDirOpen/Close empty"); + if (g_fManyFiles) + { + InDir(RT_STR_TUPLE("manyfiles")); + PROFILE_FN(fsPerfOpenClose(g_szDir), g_nsTestRun, "RTDirOpen/Close manyfiles"); + } +} + + +DECL_FORCE_INLINE(int) fsPerfEnumEmpty(void) +{ + RTDIR hDir; + g_szEmptyDir[g_cchEmptyDir] = '\0'; + RTTESTI_CHECK_RC_RET(fsPerfOpenDirWrap(&hDir, g_szEmptyDir), VINF_SUCCESS, rcCheck); + + RTDIRENTRY Entry; + RTTESTI_CHECK_RC(RTDirRead(hDir, &Entry, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTDirRead(hDir, &Entry, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTDirRead(hDir, &Entry, NULL), VERR_NO_MORE_FILES); + + RTTESTI_CHECK_RC(RTDirClose(hDir), VINF_SUCCESS); + return VINF_SUCCESS; +} + + +DECL_FORCE_INLINE(int) fsPerfEnumManyFiles(void) +{ + RTDIR hDir; + RTTESTI_CHECK_RC_RET(fsPerfOpenDirWrap(&hDir, InDir(RT_STR_TUPLE("manyfiles"))), VINF_SUCCESS, rcCheck); + uint32_t cLeft = g_cManyFiles + 2; + for (;;) + { + RTDIRENTRY Entry; + if (cLeft > 0) + RTTESTI_CHECK_RC_BREAK(RTDirRead(hDir, &Entry, NULL), VINF_SUCCESS); + else + { + RTTESTI_CHECK_RC(RTDirRead(hDir, &Entry, NULL), VERR_NO_MORE_FILES); + break; + } + cLeft--; + } + RTTESTI_CHECK_RC(RTDirClose(hDir), VINF_SUCCESS); + return VINF_SUCCESS; +} + + +void vsPerfDirEnum(void) +{ + RTTestISub("dir enum"); + RTDIR hDir; + + /* + * The empty directory. + */ + g_szEmptyDir[g_cchEmptyDir] = '\0'; + RTTESTI_CHECK_RC_RETV(fsPerfOpenDirWrap(&hDir, g_szEmptyDir), VINF_SUCCESS); + + uint32_t fDots = 0; + RTDIRENTRY Entry; + RTTESTI_CHECK_RC(RTDirRead(hDir, &Entry, NULL), VINF_SUCCESS); + RTTESTI_CHECK(RTDirEntryIsStdDotLink(&Entry)); + fDots |= RT_BIT_32(Entry.cbName - 1); + + RTTESTI_CHECK_RC(RTDirRead(hDir, &Entry, NULL), VINF_SUCCESS); + RTTESTI_CHECK(RTDirEntryIsStdDotLink(&Entry)); + fDots |= RT_BIT_32(Entry.cbName - 1); + RTTESTI_CHECK(fDots == 3); + + RTTESTI_CHECK_RC(RTDirRead(hDir, &Entry, NULL), VERR_NO_MORE_FILES); + + RTTESTI_CHECK_RC(RTDirClose(hDir), VINF_SUCCESS); + + /* + * The directory with many files in it. + */ + if (g_fManyFiles) + { + fDots = 0; + uint32_t const cBitmap = RT_ALIGN_32(g_cManyFiles, 64); + void *pvBitmap = alloca(cBitmap / 8); + RT_BZERO(pvBitmap, cBitmap / 8); + for (uint32_t i = g_cManyFiles; i < cBitmap; i++) + ASMBitSet(pvBitmap, i); + + uint32_t cFiles = 0; + RTTESTI_CHECK_RC_RETV(fsPerfOpenDirWrap(&hDir, InDir(RT_STR_TUPLE("manyfiles"))), VINF_SUCCESS); + for (;;) + { + int rc = RTDirRead(hDir, &Entry, NULL); + if (rc == VINF_SUCCESS) + { + if (Entry.szName[0] == '.') + { + if (Entry.szName[1] == '.') + { + RTTESTI_CHECK(!(fDots & 2)); + fDots |= 2; + } + else + { + RTTESTI_CHECK(Entry.szName[1] == '\0'); + RTTESTI_CHECK(!(fDots & 1)); + fDots |= 1; + } + } + else + { + uint32_t iFile = UINT32_MAX; + RTTESTI_CHECK_RC(RTStrToUInt32Full(Entry.szName, 10, &iFile), VINF_SUCCESS); + if ( iFile < g_cManyFiles + && !ASMBitTest(pvBitmap, iFile)) + { + ASMBitSet(pvBitmap, iFile); + cFiles++; + } + else + RTTestFailed(g_hTest, "line %u: iFile=%u g_cManyFiles=%u\n", __LINE__, iFile, g_cManyFiles); + } + } + else if (rc == VERR_NO_MORE_FILES) + break; + else + { + RTTestFailed(g_hTest, "RTDirRead failed enumerating manyfiles: %Rrc\n", rc); + RTDirClose(hDir); + return; + } + } + RTTESTI_CHECK_RC(RTDirClose(hDir), VINF_SUCCESS); + RTTESTI_CHECK(fDots == 3); + RTTESTI_CHECK(cFiles == g_cManyFiles); + RTTESTI_CHECK(ASMMemIsAllU8(pvBitmap, cBitmap / 8, 0xff)); + } + + /* + * Profile. + */ + PROFILE_FN(fsPerfEnumEmpty(),g_nsTestRun, "RTDirOpen/Read/Close empty"); + if (g_fManyFiles) + PROFILE_FN(fsPerfEnumManyFiles(), g_nsTestRun, "RTDirOpen/Read/Close manyfiles"); +} + + +void fsPerfMkRmDir(void) +{ + RTTestISub("mkdir/rmdir"); + + /* Non-existing directories: */ + RTTESTI_CHECK_RC(RTDirRemove(InEmptyDir(RT_STR_TUPLE("no-such-dir"))), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTDirRemove(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR))), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTDirRemove(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file"))), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTDirRemove(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file" RTPATH_SLASH_STR))), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file"))), VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file" RTPATH_SLASH_STR))), VERR_PATH_NOT_FOUND); + + RTTESTI_CHECK_RC(RTDirCreate(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file")), 0755, 0), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTDirCreate(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file")), 0755, 0), VERR_PATH_NOT_FOUND); + + /* Already existing directories and files: */ + RTTESTI_CHECK_RC(RTDirCreate(InEmptyDir(RT_STR_TUPLE(".")), 0755, 0), VERR_ALREADY_EXISTS); + RTTESTI_CHECK_RC(RTDirCreate(InEmptyDir(RT_STR_TUPLE("..")), 0755, 0), VERR_ALREADY_EXISTS); + + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE("known-file"))), VERR_NOT_A_DIRECTORY); + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR))), VERR_NOT_A_DIRECTORY); + + /* Remove directory with subdirectories: */ +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE("."))), VERR_DIR_NOT_EMPTY); +#else + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE("."))), VERR_INVALID_PARAMETER); /* EINVAL for '.' */ +#endif +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + int rc = RTDirRemove(InDir(RT_STR_TUPLE(".."))); +# ifdef RT_OS_WINDOWS + if (rc != VERR_DIR_NOT_EMPTY /*ntfs root*/ && rc != VERR_SHARING_VIOLATION /*ntfs weird*/ && rc != VERR_ACCESS_DENIED /*fat32 root*/) + RTTestIFailed("RTDirRemove(%s) -> %Rrc, expected VERR_DIR_NOT_EMPTY, VERR_SHARING_VIOLATION or VERR_ACCESS_DENIED", g_szDir, rc); +# else + if (rc != VERR_DIR_NOT_EMPTY && rc != VERR_RESOURCE_BUSY /*IPRT/kLIBC fun*/) + RTTestIFailed("RTDirRemove(%s) -> %Rrc, expected VERR_DIR_NOT_EMPTY or VERR_RESOURCE_BUSY", g_szDir, rc); + + APIRET orc; + RTTESTI_CHECK_MSG((orc = DosDelete((PCSZ)InEmptyDir(RT_STR_TUPLE(".")))) == ERROR_ACCESS_DENIED, + ("DosDelete(%s) -> %u, expected %u\n", g_szEmptyDir, orc, ERROR_ACCESS_DENIED)); + RTTESTI_CHECK_MSG((orc = DosDelete((PCSZ)InEmptyDir(RT_STR_TUPLE("..")))) == ERROR_ACCESS_DENIED, + ("DosDelete(%s) -> %u, expected %u\n", g_szEmptyDir, orc, ERROR_ACCESS_DENIED)); + RTTESTI_CHECK_MSG((orc = DosDelete((PCSZ)InEmptyDir(RT_STR_TUPLE("")))) == ERROR_PATH_NOT_FOUND, /* a little weird (fsrouter) */ + ("DosDelete(%s) -> %u, expected %u\n", g_szEmptyDir, orc, ERROR_PATH_NOT_FOUND)); + +# endif +#else + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE(".."))), VERR_DIR_NOT_EMPTY); +#endif + RTTESTI_CHECK_RC(RTDirRemove(InDir(RT_STR_TUPLE(""))), VERR_DIR_NOT_EMPTY); + + /* Create a directory and remove it: */ + RTTESTI_CHECK_RC(RTDirCreate(InDir(RT_STR_TUPLE("subdir-1")), 0755, 0), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTDirRemove(g_szDir), VINF_SUCCESS); + + /* Create a file and try remove it or create a directory with the same name: */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file18")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTDirRemove(g_szDir), VERR_NOT_A_DIRECTORY); + RTTESTI_CHECK_RC(RTDirCreate(g_szDir, 0755, 0), VERR_ALREADY_EXISTS); + RTTESTI_CHECK_RC(RTDirCreate(InDir(RT_STR_TUPLE("file18" RTPATH_SLASH_STR "subdir")), 0755, 0), VERR_PATH_NOT_FOUND); + + /* + * Profile alternately creating and removing a bunch of directories. + */ + RTTESTI_CHECK_RC_RETV(RTDirCreate(InDir(RT_STR_TUPLE("subdir-2")), 0755, 0), VINF_SUCCESS); + size_t cchDir = strlen(g_szDir); + g_szDir[cchDir++] = RTPATH_SLASH; + g_szDir[cchDir++] = 's'; + + uint32_t cCreated = 0; + uint64_t nsCreate = 0; + uint64_t nsRemove = 0; + for (;;) + { + /* Create a bunch: */ + uint64_t nsStart = RTTimeNanoTS(); + for (uint32_t i = 0; i < 998; i++) + { + RTStrFormatU32(&g_szDir[cchDir], sizeof(g_szDir) - cchDir, i, 10, 3, 3, RTSTR_F_ZEROPAD); + RTTESTI_CHECK_RC_RETV(RTDirCreate(g_szDir, 0755, 0), VINF_SUCCESS); + } + nsCreate += RTTimeNanoTS() - nsStart; + cCreated += 998; + + /* Remove the bunch: */ + nsStart = RTTimeNanoTS(); + for (uint32_t i = 0; i < 998; i++) + { + RTStrFormatU32(&g_szDir[cchDir], sizeof(g_szDir) - cchDir, i, 10, 3, 3, RTSTR_F_ZEROPAD); + RTTESTI_CHECK_RC_RETV(RTDirRemove(g_szDir), VINF_SUCCESS); + } + nsRemove = RTTimeNanoTS() - nsStart; + + /* Check if we got time for another round: */ + if ( ( nsRemove >= g_nsTestRun + && nsCreate >= g_nsTestRun) + || nsCreate + nsRemove >= g_nsTestRun * 3) + break; + } + RTTestIValue("RTDirCreate", nsCreate / cCreated, RTTESTUNIT_NS_PER_OCCURRENCE); + RTTestIValue("RTDirRemove", nsRemove / cCreated, RTTESTUNIT_NS_PER_OCCURRENCE); +} + + +void fsPerfStatVfs(void) +{ + RTTestISub("statvfs"); + + g_szEmptyDir[g_cchEmptyDir] = '\0'; + RTFOFF cbTotal; + RTFOFF cbFree; + uint32_t cbBlock; + uint32_t cbSector; + RTTESTI_CHECK_RC(RTFsQuerySizes(g_szEmptyDir, &cbTotal, &cbFree, &cbBlock, &cbSector), VINF_SUCCESS); + + uint32_t uSerial; + RTTESTI_CHECK_RC(RTFsQuerySerial(g_szEmptyDir, &uSerial), VINF_SUCCESS); + + RTFSPROPERTIES Props; + RTTESTI_CHECK_RC(RTFsQueryProperties(g_szEmptyDir, &Props), VINF_SUCCESS); + + RTFSTYPE enmType; + RTTESTI_CHECK_RC(RTFsQueryType(g_szEmptyDir, &enmType), VINF_SUCCESS); + + g_szDeepDir[g_cchDeepDir] = '\0'; + PROFILE_FN(RTFsQuerySizes(g_szEmptyDir, &cbTotal, &cbFree, &cbBlock, &cbSector), g_nsTestRun, "RTFsQuerySize/empty"); + PROFILE_FN(RTFsQuerySizes(g_szDeepDir, &cbTotal, &cbFree, &cbBlock, &cbSector), g_nsTestRun, "RTFsQuerySize/deep"); +} + + +void fsPerfRm(void) +{ + RTTestISub("rm"); + + /* Non-existing files. */ + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE("no-such-file"))), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE("no-such-file" RTPATH_SLASH_STR))), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file"))), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file" RTPATH_SLASH_STR))), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileDelete(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file"))), VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileDelete(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file" RTPATH_SLASH_STR))), VERR_PATH_NOT_FOUND); + + /* Existing file but specified as if it was a directory: */ +#if defined(RT_OS_WINDOWS) + RTTESTI_CHECK_RC(RTFileDelete(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR ))), VERR_INVALID_NAME); +#else + RTTESTI_CHECK_RC(RTFileDelete(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR))), VERR_PATH_NOT_FOUND); +#endif + + /* Directories: */ +#if defined(RT_OS_WINDOWS) + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE("."))), VERR_ACCESS_DENIED); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE(".."))), VERR_ACCESS_DENIED); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE(""))), VERR_ACCESS_DENIED); +#elif defined(RT_OS_DARWIN) /* unlink() on xnu 16.7.0 is behaviour totally werid: */ + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE("."))), VERR_INVALID_PARAMETER); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE(".."))), VINF_SUCCESS /*WTF?!?*/); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE(""))), VERR_ACCESS_DENIED); +#elif defined(RT_OS_OS2) /* OS/2 has a busted unlink, it think it should remove directories too. */ + RTTESTI_CHECK_RC(RTFileDelete(InDir(RT_STR_TUPLE("."))), VERR_DIR_NOT_EMPTY); + int rc = RTFileDelete(InDir(RT_STR_TUPLE(".."))); + if (rc != VERR_DIR_NOT_EMPTY && rc != VERR_FILE_NOT_FOUND && rc != VERR_RESOURCE_BUSY) + RTTestIFailed("RTFileDelete(%s) -> %Rrc, expected VERR_DIR_NOT_EMPTY or VERR_FILE_NOT_FOUND or VERR_RESOURCE_BUSY", g_szDir, rc); + RTTESTI_CHECK_RC(RTFileDelete(InDir(RT_STR_TUPLE(""))), VERR_DIR_NOT_EMPTY); + APIRET orc; + RTTESTI_CHECK_MSG((orc = DosDelete((PCSZ)InEmptyDir(RT_STR_TUPLE(".")))) == ERROR_ACCESS_DENIED, + ("DosDelete(%s) -> %u, expected %u\n", g_szEmptyDir, orc, ERROR_ACCESS_DENIED)); + RTTESTI_CHECK_MSG((orc = DosDelete((PCSZ)InEmptyDir(RT_STR_TUPLE("..")))) == ERROR_ACCESS_DENIED, + ("DosDelete(%s) -> %u, expected %u\n", g_szEmptyDir, orc, ERROR_ACCESS_DENIED)); + RTTESTI_CHECK_MSG((orc = DosDelete((PCSZ)InEmptyDir(RT_STR_TUPLE("")))) == ERROR_PATH_NOT_FOUND, + ("DosDelete(%s) -> %u, expected %u\n", g_szEmptyDir, orc, ERROR_PATH_NOT_FOUND)); /* hpfs+jfs; weird. */ + +#else + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE("."))), VERR_IS_A_DIRECTORY); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE(".."))), VERR_IS_A_DIRECTORY); + RTTESTI_CHECK_RC(RTFileDelete(InEmptyDir(RT_STR_TUPLE(""))), VERR_IS_A_DIRECTORY); +#endif + + /* Shallow: */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file19")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileDelete(g_szDir), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileDelete(g_szDir), VERR_FILE_NOT_FOUND); + + if (g_fManyFiles) + { + /* + * Profile the deletion of the manyfiles content. + */ + { + InDir(RT_STR_TUPLE("manyfiles" RTPATH_SLASH_STR)); + size_t const offFilename = strlen(g_szDir); + fsPerfYield(); + uint64_t const nsStart = RTTimeNanoTS(); + for (uint32_t i = 0; i < g_cManyFiles; i++) + { + RTStrFormatU32(&g_szDir[offFilename], sizeof(g_szDir) - offFilename, i, 10, 5, 5, RTSTR_F_ZEROPAD); + RTTESTI_CHECK_RC_RETV(RTFileDelete(g_szDir), VINF_SUCCESS); + } + uint64_t const cNsElapsed = RTTimeNanoTS() - nsStart; + RTTestIValueF(cNsElapsed, RTTESTUNIT_NS, "Deleted %u empty files from a single directory", g_cManyFiles); + RTTestIValueF(cNsElapsed / g_cManyFiles, RTTESTUNIT_NS_PER_OCCURRENCE, "Delete file (single dir)"); + } + + /* + * Ditto for the manytree. + */ + { + char szPath[FSPERF_MAX_PATH]; + uint64_t const nsStart = RTTimeNanoTS(); + DO_MANYTREE_FN(szPath, RTTESTI_CHECK_RC_RETV(RTFileDelete(szPath), VINF_SUCCESS)); + uint64_t const cNsElapsed = RTTimeNanoTS() - nsStart; + RTTestIValueF(cNsElapsed, RTTESTUNIT_NS, "Deleted %u empty files in tree", g_cManyTreeFiles); + RTTestIValueF(cNsElapsed / g_cManyTreeFiles, RTTESTUNIT_NS_PER_OCCURRENCE, "Delete file (tree)"); + } + } +} + + +void fsPerfChSize(void) +{ + RTTestISub("chsize"); + + /* + * We need some free space to perform this test. + */ + g_szDir[g_cchDir] = '\0'; + RTFOFF cbFree = 0; + RTTESTI_CHECK_RC_RETV(RTFsQuerySizes(g_szDir, NULL, &cbFree, NULL, NULL), VINF_SUCCESS); + if (cbFree < _1M) + { + RTTestSkipped(g_hTest, "Insufficent free space: %'RU64 bytes, requires >= 1MB", cbFree); + return; + } + + /* + * Create a file and play around with it's size. + * We let the current file position follow the end position as we make changes. + */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file20")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE), VINF_SUCCESS); + uint64_t cbFile = UINT64_MAX; + RTTESTI_CHECK_RC(RTFileQuerySize(hFile1, &cbFile), VINF_SUCCESS); + RTTESTI_CHECK(cbFile == 0); + + uint8_t abBuf[4096]; + static uint64_t const s_acbChanges[] = + { + 1023, 1024, 1024, 1025, 8192, 11111, _1M, _8M, _8M, + _4M, _2M + 1, _1M - 1, 65537, 65536, 32768, 8000, 7999, 7998, 1024, 1, 0 + }; + uint64_t cbOld = 0; + for (unsigned i = 0; i < RT_ELEMENTS(s_acbChanges); i++) + { + uint64_t cbNew = s_acbChanges[i]; + if (cbNew + _64K >= (uint64_t)cbFree) + continue; + + RTTESTI_CHECK_RC(RTFileSetSize(hFile1, cbNew), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileQuerySize(hFile1, &cbFile), VINF_SUCCESS); + RTTESTI_CHECK_MSG(cbFile == cbNew, ("cbFile=%#RX64 cbNew=%#RX64\n", cbFile, cbNew)); + + if (cbNew > cbOld) + { + /* Check that the extension is all zeroed: */ + uint64_t cbLeft = cbNew - cbOld; + while (cbLeft > 0) + { + memset(abBuf, 0xff, sizeof(abBuf)); + size_t cbToRead = sizeof(abBuf); + if (cbToRead > cbLeft) + cbToRead = (size_t)cbLeft; + RTTESTI_CHECK_RC(RTFileRead(hFile1, abBuf, cbToRead, NULL), VINF_SUCCESS); + RTTESTI_CHECK(ASMMemIsZero(abBuf, cbToRead)); + cbLeft -= cbToRead; + } + } + else + { + /* Check that reading fails with EOF because current position is now beyond the end: */ + RTTESTI_CHECK_RC(RTFileRead(hFile1, abBuf, 1, NULL), VERR_EOF); + + /* Keep current position at the end of the file: */ + RTTESTI_CHECK_RC(RTFileSeek(hFile1, cbNew, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + } + cbOld = cbNew; + } + + /* + * Profile just the file setting operation itself, keeping the changes within + * an allocation unit to avoid needing to adjust the actual (host) FS allocation. + * ASSUMES allocation unit >= 512 and power of two. + */ + RTTESTI_CHECK_RC(RTFileSetSize(hFile1, _64K), VINF_SUCCESS); + PROFILE_FN(RTFileSetSize(hFile1, _64K - (iIteration & 255) - 128), g_nsTestRun, "RTFileSetSize/noalloc"); + + RTTESTI_CHECK_RC(RTFileSetSize(hFile1, 0), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileDelete(g_szDir), VINF_SUCCESS); +} + + +int fsPerfIoPrepFileWorker(RTFILE hFile1, uint64_t cbFile, uint8_t *pbBuf, size_t cbBuf) +{ + /* + * Fill the file with 0xf6 and insert offset markers with 1KB intervals. + */ + RTTESTI_CHECK_RC_RET(RTFileSeek(hFile1, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS, rcCheck); + memset(pbBuf, 0xf6, cbBuf); + uint64_t cbLeft = cbFile; + uint64_t off = 0; + while (cbLeft > 0) + { + Assert(!(off & (_1K - 1))); + Assert(!(cbBuf & (_1K - 1))); + for (size_t offBuf = 0; offBuf < cbBuf; offBuf += _1K, off += _1K) + *(uint64_t *)&pbBuf[offBuf] = off; + + size_t cbToWrite = cbBuf; + if (cbToWrite > cbLeft) + cbToWrite = (size_t)cbLeft; + + RTTESTI_CHECK_RC_RET(RTFileWrite(hFile1, pbBuf, cbToWrite, NULL), VINF_SUCCESS, rcCheck); + cbLeft -= cbToWrite; + } + return VINF_SUCCESS; +} + +int fsPerfIoPrepFile(RTFILE hFile1, uint64_t cbFile, uint8_t **ppbFree) +{ + /* + * Seek to the end - 4K and write the last 4K. + * This should have the effect of filling the whole file with zeros. + */ + RTTESTI_CHECK_RC_RET(RTFileSeek(hFile1, cbFile - _4K, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RC_RET(RTFileWrite(hFile1, g_abRTZero4K, _4K, NULL), VINF_SUCCESS, rcCheck); + + /* + * Check that the space we searched across actually is zero filled. + */ + RTTESTI_CHECK_RC_RET(RTFileSeek(hFile1, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS, rcCheck); + size_t cbBuf = RT_MIN(_1M, g_cbMaxBuffer); + uint8_t *pbBuf = *ppbFree = (uint8_t *)RTMemAlloc(cbBuf); + RTTESTI_CHECK_RET(pbBuf != NULL, VERR_NO_MEMORY); + uint64_t cbLeft = cbFile; + while (cbLeft > 0) + { + size_t cbToRead = cbBuf; + if (cbToRead > cbLeft) + cbToRead = (size_t)cbLeft; + pbBuf[cbToRead - 1] = 0xff; + + RTTESTI_CHECK_RC_RET(RTFileRead(hFile1, pbBuf, cbToRead, NULL), VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RET(ASMMemIsZero(pbBuf, cbToRead), VERR_MISMATCH); + + cbLeft -= cbToRead; + } + + /* + * Fill the file with 0xf6 and insert offset markers with 1KB intervals. + */ + return fsPerfIoPrepFileWorker(hFile1, cbFile, pbBuf, cbBuf); +} + +/** + * Used in relation to the mmap test when in non-default position. + */ +int fsPerfReinitFile(RTFILE hFile1, uint64_t cbFile) +{ + size_t cbBuf = RT_MIN(_1M, g_cbMaxBuffer); + uint8_t *pbBuf = (uint8_t *)RTMemAlloc(cbBuf); + RTTESTI_CHECK_RET(pbBuf != NULL, VERR_NO_MEMORY); + + int rc = fsPerfIoPrepFileWorker(hFile1, cbFile, pbBuf, cbBuf); + + RTMemFree(pbBuf); + return rc; +} + +/** + * Checks the content read from the file fsPerfIoPrepFile() prepared. + */ +bool fsPerfCheckReadBuf(unsigned uLineNo, uint64_t off, uint8_t const *pbBuf, size_t cbBuf, uint8_t bFiller = 0xf6) +{ + uint32_t cMismatches = 0; + size_t offBuf = 0; + uint32_t offBlock = (uint32_t)(off & (_1K - 1)); + while (offBuf < cbBuf) + { + /* + * Check the offset marker: + */ + if (offBlock < sizeof(uint64_t)) + { + RTUINT64U uMarker; + uMarker.u = off + offBuf - offBlock; + unsigned offMarker = offBlock & (sizeof(uint64_t) - 1); + while (offMarker < sizeof(uint64_t) && offBuf < cbBuf) + { + if (uMarker.au8[offMarker] != pbBuf[offBuf]) + { + RTTestIFailed("%u: Mismatch at buffer/file offset %#zx/%#RX64: %#x, expected %#x", + uLineNo, offBuf, off + offBuf, pbBuf[offBuf], uMarker.au8[offMarker]); + if (cMismatches++ > 32) + return false; + } + offMarker++; + offBuf++; + } + offBlock = sizeof(uint64_t); + } + + /* + * Check the filling: + */ + size_t cbFilling = RT_MIN(_1K - offBlock, cbBuf - offBuf); + if ( cbFilling == 0 + || ASMMemIsAllU8(&pbBuf[offBuf], cbFilling, bFiller)) + offBuf += cbFilling; + else + { + /* Some mismatch, locate it/them: */ + while (cbFilling > 0 && offBuf < cbBuf) + { + if (pbBuf[offBuf] != bFiller) + { + RTTestIFailed("%u: Mismatch at buffer/file offset %#zx/%#RX64: %#x, expected %#04x", + uLineNo, offBuf, off + offBuf, pbBuf[offBuf], bFiller); + if (cMismatches++ > 32) + return false; + } + offBuf++; + cbFilling--; + } + } + offBlock = 0; + } + return cMismatches == 0; +} + + +/** + * Sets up write buffer with offset markers and fillers. + */ +void fsPerfFillWriteBuf(uint64_t off, uint8_t *pbBuf, size_t cbBuf, uint8_t bFiller = 0xf6) +{ + uint32_t offBlock = (uint32_t)(off & (_1K - 1)); + while (cbBuf > 0) + { + /* The marker. */ + if (offBlock < sizeof(uint64_t)) + { + RTUINT64U uMarker; + uMarker.u = off + offBlock; + if (cbBuf > sizeof(uMarker) - offBlock) + { + memcpy(pbBuf, &uMarker.au8[offBlock], sizeof(uMarker) - offBlock); + pbBuf += sizeof(uMarker) - offBlock; + cbBuf -= sizeof(uMarker) - offBlock; + off += sizeof(uMarker) - offBlock; + } + else + { + memcpy(pbBuf, &uMarker.au8[offBlock], cbBuf); + return; + } + offBlock = sizeof(uint64_t); + } + + /* Do the filling. */ + size_t cbFilling = RT_MIN(_1K - offBlock, cbBuf); + memset(pbBuf, bFiller, cbFilling); + pbBuf += cbFilling; + cbBuf -= cbFilling; + off += cbFilling; + + offBlock = 0; + } +} + + + +void fsPerfIoSeek(RTFILE hFile1, uint64_t cbFile) +{ + /* + * Do a bunch of search tests, most which are random. + */ + struct + { + int rc; + uint32_t uMethod; + int64_t offSeek; + uint64_t offActual; + + } aSeeks[9 + 64] = + { + { VINF_SUCCESS, RTFILE_SEEK_BEGIN, 0, 0 }, + { VINF_SUCCESS, RTFILE_SEEK_CURRENT, 0, 0 }, + { VINF_SUCCESS, RTFILE_SEEK_END, 0, cbFile }, + { VINF_SUCCESS, RTFILE_SEEK_CURRENT, -4096, cbFile - 4096 }, + { VINF_SUCCESS, RTFILE_SEEK_CURRENT, 4096 - (int64_t)cbFile, 0 }, + { VINF_SUCCESS, RTFILE_SEEK_END, -(int64_t)cbFile/2, cbFile / 2 + (cbFile & 1) }, + { VINF_SUCCESS, RTFILE_SEEK_CURRENT, -(int64_t)cbFile/2, 0 }, +#if defined(RT_OS_WINDOWS) + { VERR_NEGATIVE_SEEK, RTFILE_SEEK_CURRENT, -1, 0 }, +#else + { VERR_INVALID_PARAMETER, RTFILE_SEEK_CURRENT, -1, 0 }, +#endif + { VINF_SUCCESS, RTFILE_SEEK_CURRENT, 0, 0 }, + }; + + uint64_t offActual = 0; + for (unsigned i = 9; i < RT_ELEMENTS(aSeeks); i++) + { + switch (RTRandU32Ex(RTFILE_SEEK_BEGIN, RTFILE_SEEK_END)) + { + default: AssertFailedBreak(); + case RTFILE_SEEK_BEGIN: + aSeeks[i].uMethod = RTFILE_SEEK_BEGIN; + aSeeks[i].rc = VINF_SUCCESS; + aSeeks[i].offSeek = RTRandU64Ex(0, cbFile + cbFile / 8); + aSeeks[i].offActual = offActual = aSeeks[i].offSeek; + break; + + case RTFILE_SEEK_CURRENT: + aSeeks[i].uMethod = RTFILE_SEEK_CURRENT; + aSeeks[i].rc = VINF_SUCCESS; + aSeeks[i].offSeek = (int64_t)RTRandU64Ex(0, cbFile + cbFile / 8) - (int64_t)offActual; + aSeeks[i].offActual = offActual += aSeeks[i].offSeek; + break; + + case RTFILE_SEEK_END: + aSeeks[i].uMethod = RTFILE_SEEK_END; + aSeeks[i].rc = VINF_SUCCESS; + aSeeks[i].offSeek = -(int64_t)RTRandU64Ex(0, cbFile); + aSeeks[i].offActual = offActual = cbFile + aSeeks[i].offSeek; + break; + } + } + + for (unsigned iDoReadCheck = 0; iDoReadCheck < 2; iDoReadCheck++) + { + for (uint32_t i = 0; i < RT_ELEMENTS(aSeeks); i++) + { + offActual = UINT64_MAX; + int rc = RTFileSeek(hFile1, aSeeks[i].offSeek, aSeeks[i].uMethod, &offActual); + if (rc != aSeeks[i].rc) + RTTestIFailed("Seek #%u: Expected %Rrc, got %Rrc", i, aSeeks[i].rc, rc); + if (RT_SUCCESS(rc) && offActual != aSeeks[i].offActual) + RTTestIFailed("Seek #%u: offActual %#RX64, expected %#RX64", i, offActual, aSeeks[i].offActual); + if (RT_SUCCESS(rc)) + { + uint64_t offTell = RTFileTell(hFile1); + if (offTell != offActual) + RTTestIFailed("Seek #%u: offActual %#RX64, RTFileTell %#RX64", i, offActual, offTell); + } + + if (RT_SUCCESS(rc) && offActual + _2K <= cbFile && iDoReadCheck) + { + uint8_t abBuf[_2K]; + RTTESTI_CHECK_RC(rc = RTFileRead(hFile1, abBuf, sizeof(abBuf), NULL), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + size_t offMarker = (size_t)(RT_ALIGN_64(offActual, _1K) - offActual); + uint64_t uMarker = *(uint64_t *)&abBuf[offMarker]; /** @todo potentially unaligned access */ + if (uMarker != offActual + offMarker) + RTTestIFailed("Seek #%u: Invalid marker value (@ %#RX64): %#RX64, expected %#RX64", + i, offActual, uMarker, offActual + offMarker); + + RTTESTI_CHECK_RC(RTFileSeek(hFile1, -(int64_t)sizeof(abBuf), RTFILE_SEEK_CURRENT, NULL), VINF_SUCCESS); + } + } + } + } + + + /* + * Profile seeking relative to the beginning of the file and relative + * to the end. The latter might be more expensive in a SF context. + */ + PROFILE_FN(RTFileSeek(hFile1, iIteration < cbFile ? iIteration : iIteration % cbFile, RTFILE_SEEK_BEGIN, NULL), + g_nsTestRun, "RTFileSeek/BEGIN"); + PROFILE_FN(RTFileSeek(hFile1, iIteration < cbFile ? -(int64_t)iIteration : -(int64_t)(iIteration % cbFile), RTFILE_SEEK_END, NULL), + g_nsTestRun, "RTFileSeek/END"); + +} + +#ifdef FSPERF_TEST_SENDFILE + +/** + * Send file thread arguments. + */ +typedef struct FSPERFSENDFILEARGS +{ + uint64_t offFile; + size_t cbSend; + uint64_t cbSent; + size_t cbBuf; + uint8_t *pbBuf; + uint8_t bFiller; + bool fCheckBuf; + RTSOCKET hSocket; + uint64_t volatile tsThreadDone; +} FSPERFSENDFILEARGS; + +/** Thread receiving the bytes from a sendfile() call. */ +static DECLCALLBACK(int) fsPerfSendFileThread(RTTHREAD hSelf, void *pvUser) +{ + FSPERFSENDFILEARGS *pArgs = (FSPERFSENDFILEARGS *)pvUser; + int rc = VINF_SUCCESS; + + if (pArgs->fCheckBuf) + RTTestSetDefault(g_hTest, NULL); + + uint64_t cbReceived = 0; + while (cbReceived < pArgs->cbSent) + { + size_t const cbToRead = RT_MIN(pArgs->cbBuf, pArgs->cbSent - cbReceived); + size_t cbActual = 0; + RTTEST_CHECK_RC_BREAK(g_hTest, rc = RTTcpRead(pArgs->hSocket, pArgs->pbBuf, cbToRead, &cbActual), VINF_SUCCESS); + RTTEST_CHECK_BREAK(g_hTest, cbActual != 0); + RTTEST_CHECK(g_hTest, cbActual <= cbToRead); + if (pArgs->fCheckBuf) + fsPerfCheckReadBuf(__LINE__, pArgs->offFile + cbReceived, pArgs->pbBuf, cbActual, pArgs->bFiller); + cbReceived += cbActual; + } + + pArgs->tsThreadDone = RTTimeNanoTS(); + + if (cbReceived == pArgs->cbSent && RT_SUCCESS(rc)) + { + size_t cbActual = 0; + rc = RTSocketReadNB(pArgs->hSocket, pArgs->pbBuf, 1, &cbActual); + if (rc != VINF_SUCCESS && rc != VINF_TRY_AGAIN) + RTTestFailed(g_hTest, "RTSocketReadNB(sendfile client socket) -> %Rrc; expected VINF_SUCCESS or VINF_TRY_AGAIN\n", rc); + else if (cbActual != 0) + RTTestFailed(g_hTest, "sendfile client socket still contains data when done!\n"); + } + + RTTEST_CHECK_RC(g_hTest, RTSocketClose(pArgs->hSocket), VINF_SUCCESS); + pArgs->hSocket = NIL_RTSOCKET; + + RT_NOREF(hSelf); + return rc; +} + + +static uint64_t fsPerfSendFileOne(FSPERFSENDFILEARGS *pArgs, RTFILE hFile1, uint64_t offFile, + size_t cbSend, uint64_t cbSent, uint8_t bFiller, bool fCheckBuf, unsigned iLine) +{ + /* Copy parameters to the argument structure: */ + pArgs->offFile = offFile; + pArgs->cbSend = cbSend; + pArgs->cbSent = cbSent; + pArgs->bFiller = bFiller; + pArgs->fCheckBuf = fCheckBuf; + + /* Create a socket pair. */ + pArgs->hSocket = NIL_RTSOCKET; + RTSOCKET hServer = NIL_RTSOCKET; + RTTESTI_CHECK_RC_RET(RTTcpCreatePair(&hServer, &pArgs->hSocket, 0), VINF_SUCCESS, 0); + + /* Create the receiving thread: */ + int rc; + RTTHREAD hThread = NIL_RTTHREAD; + RTTESTI_CHECK_RC(rc = RTThreadCreate(&hThread, fsPerfSendFileThread, pArgs, 0, + RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "sendfile"), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + uint64_t const tsStart = RTTimeNanoTS(); + +# if defined(RT_OS_LINUX) || defined(RT_OS_SOLARIS) + /* SystemV sendfile: */ + loff_t offFileSf = pArgs->offFile; + ssize_t cbActual = sendfile((int)RTSocketToNative(hServer), (int)RTFileToNative(hFile1), &offFileSf, pArgs->cbSend); + int const iErr = errno; + if (cbActual < 0) + RTTestIFailed("%u: sendfile(socket, file, &%#X64, %#zx) failed (%zd): %d (%Rrc), offFileSf=%#RX64\n", + iLine, pArgs->offFile, pArgs->cbSend, cbActual, iErr, RTErrConvertFromErrno(iErr), (uint64_t)offFileSf); + else if ((uint64_t)cbActual != pArgs->cbSent) + RTTestIFailed("%u: sendfile(socket, file, &%#RX64, %#zx): %#zx, expected %#RX64 (offFileSf=%#RX64)\n", + iLine, pArgs->offFile, pArgs->cbSend, cbActual, pArgs->cbSent, (uint64_t)offFileSf); + else if ((uint64_t)offFileSf != pArgs->offFile + pArgs->cbSent) + RTTestIFailed("%u: sendfile(socket, file, &%#RX64, %#zx): %#zx; offFileSf=%#RX64, expected %#RX64\n", + iLine, pArgs->offFile, pArgs->cbSend, cbActual, (uint64_t)offFileSf, pArgs->offFile + pArgs->cbSent); +#else + /* BSD sendfile: */ +# ifdef SF_SYNC + int fSfFlags = SF_SYNC; +# else + int fSfFlags = 0; +# endif + off_t cbActual = pArgs->cbSend; + rc = sendfile((int)RTFileToNative(hFile1), (int)RTSocketToNative(hServer), +# ifdef RT_OS_DARWIN + pArgs->offFile, &cbActual, NULL, fSfFlags); +# else + pArgs->offFile, cbActual, NULL, &cbActual, fSfFlags); +# endif + int const iErr = errno; + if (rc != 0) + RTTestIFailed("%u: sendfile(file, socket, %#RX64, %#zx, NULL,, %#x) failed (%d): %d (%Rrc), cbActual=%#RX64\n", + iLine, pArgs->offFile, (size_t)pArgs->cbSend, rc, iErr, RTErrConvertFromErrno(iErr), (uint64_t)cbActual); + if ((uint64_t)cbActual != pArgs->cbSent) + RTTestIFailed("%u: sendfile(file, socket, %#RX64, %#zx, NULL,, %#x): cbActual=%#RX64, expected %#RX64 (rc=%d, errno=%d)\n", + iLine, pArgs->offFile, (size_t)pArgs->cbSend, (uint64_t)cbActual, pArgs->cbSent, rc, iErr); +# endif + RTTESTI_CHECK_RC(RTSocketClose(hServer), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTThreadWait(hThread, 30 * RT_NS_1SEC, NULL), VINF_SUCCESS); + + if (pArgs->tsThreadDone >= tsStart) + return RT_MAX(pArgs->tsThreadDone - tsStart, 1); + } + return 0; +} + + +static void fsPerfSendFile(RTFILE hFile1, uint64_t cbFile) +{ + RTTestISub("sendfile"); +# ifdef RT_OS_LINUX + uint64_t const cbFileMax = RT_MIN(cbFile, UINT32_MAX - PAGE_OFFSET_MASK); +# else + uint64_t const cbFileMax = RT_MIN(cbFile, SSIZE_MAX - PAGE_OFFSET_MASK); +# endif + signal(SIGPIPE, SIG_IGN); + + /* + * Allocate a buffer. + */ + FSPERFSENDFILEARGS Args; + Args.cbBuf = RT_MIN(RT_MIN(cbFileMax, _16M), g_cbMaxBuffer); + Args.pbBuf = (uint8_t *)RTMemAlloc(Args.cbBuf); + while (!Args.pbBuf) + { + Args.cbBuf /= 8; + RTTESTI_CHECK_RETV(Args.cbBuf >= _64K); + Args.pbBuf = (uint8_t *)RTMemAlloc(Args.cbBuf); + } + + /* + * First iteration with default buffer content. + */ + fsPerfSendFileOne(&Args, hFile1, 0, cbFileMax, cbFileMax, 0xf6, true /*fCheckBuf*/, __LINE__); + if (cbFileMax == cbFile) + fsPerfSendFileOne(&Args, hFile1, 63, cbFileMax, cbFileMax - 63, 0xf6, true /*fCheckBuf*/, __LINE__); + else + fsPerfSendFileOne(&Args, hFile1, 63, cbFileMax - 63, cbFileMax - 63, 0xf6, true /*fCheckBuf*/, __LINE__); + + /* + * Write a block using the regular API and then send it, checking that + * the any caching that sendfile does is correctly updated. + */ + uint8_t bFiller = 0xf6; + size_t cbToSend = RT_MIN(cbFileMax, Args.cbBuf); + do + { + fsPerfSendFileOne(&Args, hFile1, 0, cbToSend, cbToSend, bFiller, true /*fCheckBuf*/, __LINE__); /* prime cache */ + + bFiller += 1; + fsPerfFillWriteBuf(0, Args.pbBuf, cbToSend, bFiller); + RTTESTI_CHECK_RC(RTFileWriteAt(hFile1, 0, Args.pbBuf, cbToSend, NULL), VINF_SUCCESS); + + fsPerfSendFileOne(&Args, hFile1, 0, cbToSend, cbToSend, bFiller, true /*fCheckBuf*/, __LINE__); + + cbToSend /= 2; + } while (cbToSend >= PAGE_SIZE && ((unsigned)bFiller - 0xf7U) < 64); + + /* + * Restore buffer content + */ + bFiller = 0xf6; + fsPerfFillWriteBuf(0, Args.pbBuf, Args.cbBuf, bFiller); + RTTESTI_CHECK_RC(RTFileWriteAt(hFile1, 0, Args.pbBuf, Args.cbBuf, NULL), VINF_SUCCESS); + + /* + * Do 128 random sends. + */ + uint64_t const cbSmall = RT_MIN(_256K, cbFileMax / 16); + for (uint32_t iTest = 0; iTest < 128; iTest++) + { + cbToSend = (size_t)RTRandU64Ex(1, iTest < 64 ? cbSmall : cbFileMax); + uint64_t const offToSendFrom = RTRandU64Ex(0, cbFile - 1); + uint64_t const cbSent = offToSendFrom + cbToSend <= cbFile ? cbToSend : cbFile - offToSendFrom; + + fsPerfSendFileOne(&Args, hFile1, offToSendFrom, cbToSend, cbSent, bFiller, true /*fCheckBuf*/, __LINE__); + } + + /* + * Benchmark it. + */ + uint32_t cIterations = 0; + uint64_t nsElapsed = 0; + for (;;) + { + uint64_t cNsThis = fsPerfSendFileOne(&Args, hFile1, 0, cbFileMax, cbFileMax, 0xf6, false /*fCheckBuf*/, __LINE__); + nsElapsed += cNsThis; + cIterations++; + if (!cNsThis || nsElapsed >= g_nsTestRun) + break; + } + uint64_t cbTotal = cbFileMax * cIterations; + RTTestIValue("latency", nsElapsed / cIterations, RTTESTUNIT_NS_PER_CALL); + RTTestIValue("throughput", (uint64_t)(cbTotal / ((double)nsElapsed / RT_NS_1SEC)), RTTESTUNIT_BYTES_PER_SEC); + RTTestIValue("calls", cIterations, RTTESTUNIT_CALLS); + RTTestIValue("bytes", cbTotal, RTTESTUNIT_BYTES); + if (g_fShowDuration) + RTTestIValue("duration", nsElapsed, RTTESTUNIT_NS); + + /* + * Cleanup. + */ + RTMemFree(Args.pbBuf); +} + +#endif /* FSPERF_TEST_SENDFILE */ +#ifdef RT_OS_LINUX + +#ifndef __NR_splice +# if defined(RT_ARCH_AMD64) +# define __NR_splice 275 +# elif defined(RT_ARCH_X86) +# define __NR_splice 313 +# else +# error "fix me" +# endif +#endif + +/** FsPerf is built against ancient glibc, so make the splice syscall ourselves. */ +DECLINLINE(ssize_t) syscall_splice(int fdIn, loff_t *poffIn, int fdOut, loff_t *poffOut, size_t cbChunk, unsigned fFlags) +{ + return syscall(__NR_splice, fdIn, poffIn, fdOut, poffOut, cbChunk, fFlags); +} + + +/** + * Send file thread arguments. + */ +typedef struct FSPERFSPLICEARGS +{ + uint64_t offFile; + size_t cbSend; + uint64_t cbSent; + size_t cbBuf; + uint8_t *pbBuf; + uint8_t bFiller; + bool fCheckBuf; + uint32_t cCalls; + RTPIPE hPipe; + uint64_t volatile tsThreadDone; +} FSPERFSPLICEARGS; + + +/** Thread receiving the bytes from a splice() call. */ +static DECLCALLBACK(int) fsPerfSpliceToPipeThread(RTTHREAD hSelf, void *pvUser) +{ + FSPERFSPLICEARGS *pArgs = (FSPERFSPLICEARGS *)pvUser; + int rc = VINF_SUCCESS; + + if (pArgs->fCheckBuf) + RTTestSetDefault(g_hTest, NULL); + + uint64_t cbReceived = 0; + while (cbReceived < pArgs->cbSent) + { + size_t const cbToRead = RT_MIN(pArgs->cbBuf, pArgs->cbSent - cbReceived); + size_t cbActual = 0; + RTTEST_CHECK_RC_BREAK(g_hTest, rc = RTPipeReadBlocking(pArgs->hPipe, pArgs->pbBuf, cbToRead, &cbActual), VINF_SUCCESS); + RTTEST_CHECK_BREAK(g_hTest, cbActual != 0); + RTTEST_CHECK(g_hTest, cbActual <= cbToRead); + if (pArgs->fCheckBuf) + fsPerfCheckReadBuf(__LINE__, pArgs->offFile + cbReceived, pArgs->pbBuf, cbActual, pArgs->bFiller); + cbReceived += cbActual; + } + + pArgs->tsThreadDone = RTTimeNanoTS(); + + if (cbReceived == pArgs->cbSent && RT_SUCCESS(rc)) + { + size_t cbActual = 0; + rc = RTPipeRead(pArgs->hPipe, pArgs->pbBuf, 1, &cbActual); + if (rc != VINF_SUCCESS && rc != VINF_TRY_AGAIN && rc != VERR_BROKEN_PIPE) + RTTestFailed(g_hTest, "RTPipeReadBlocking() -> %Rrc; expected VINF_SUCCESS or VINF_TRY_AGAIN\n", rc); + else if (cbActual != 0) + RTTestFailed(g_hTest, "splice read pipe still contains data when done!\n"); + } + + RTTEST_CHECK_RC(g_hTest, RTPipeClose(pArgs->hPipe), VINF_SUCCESS); + pArgs->hPipe = NIL_RTPIPE; + + RT_NOREF(hSelf); + return rc; +} + + +/** Sends hFile1 to a pipe via the Linux-specific splice() syscall. */ +static uint64_t fsPerfSpliceToPipeOne(FSPERFSPLICEARGS *pArgs, RTFILE hFile1, uint64_t offFile, + size_t cbSend, uint64_t cbSent, uint8_t bFiller, bool fCheckBuf, unsigned iLine) +{ + /* Copy parameters to the argument structure: */ + pArgs->offFile = offFile; + pArgs->cbSend = cbSend; + pArgs->cbSent = cbSent; + pArgs->bFiller = bFiller; + pArgs->fCheckBuf = fCheckBuf; + + /* Create a socket pair. */ + pArgs->hPipe = NIL_RTPIPE; + RTPIPE hPipeW = NIL_RTPIPE; + RTTESTI_CHECK_RC_RET(RTPipeCreate(&pArgs->hPipe, &hPipeW, 0 /*fFlags*/), VINF_SUCCESS, 0); + + /* Create the receiving thread: */ + int rc; + RTTHREAD hThread = NIL_RTTHREAD; + RTTESTI_CHECK_RC(rc = RTThreadCreate(&hThread, fsPerfSpliceToPipeThread, pArgs, 0, + RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "splicerecv"), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + uint64_t const tsStart = RTTimeNanoTS(); + size_t cbLeft = cbSend; + size_t cbTotal = 0; + do + { + loff_t offFileIn = offFile; + ssize_t cbActual = syscall_splice((int)RTFileToNative(hFile1), &offFileIn, (int)RTPipeToNative(hPipeW), NULL, + cbLeft, 0 /*fFlags*/); + int const iErr = errno; + if (RT_UNLIKELY(cbActual < 0)) + { + if (iErr == EPIPE && cbTotal == pArgs->cbSent) + break; + RTTestIFailed("%u: splice(file, &%#RX64, pipe, NULL, %#zx, 0) failed (%zd): %d (%Rrc), offFileIn=%#RX64\n", + iLine, offFile, cbLeft, cbActual, iErr, RTErrConvertFromErrno(iErr), (uint64_t)offFileIn); + break; + } + RTTESTI_CHECK_BREAK((uint64_t)cbActual <= cbLeft); + if ((uint64_t)offFileIn != offFile + (uint64_t)cbActual) + { + RTTestIFailed("%u: splice(file, &%#RX64, pipe, NULL, %#zx, 0): %#zx; offFileIn=%#RX64, expected %#RX64\n", + iLine, offFile, cbLeft, cbActual, (uint64_t)offFileIn, offFile + (uint64_t)cbActual); + break; + } + if (cbActual > 0) + { + pArgs->cCalls++; + offFile += (size_t)cbActual; + cbTotal += (size_t)cbActual; + cbLeft -= (size_t)cbActual; + } + else + break; + } while (cbLeft > 0); + + if (cbTotal != pArgs->cbSent) + RTTestIFailed("%u: spliced a total of %#zx bytes, expected %#zx!\n", iLine, cbTotal, pArgs->cbSent); + + RTTESTI_CHECK_RC(RTPipeClose(hPipeW), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTThreadWait(hThread, 30 * RT_NS_1SEC, NULL), VINF_SUCCESS); + + if (pArgs->tsThreadDone >= tsStart) + return RT_MAX(pArgs->tsThreadDone - tsStart, 1); + } + return 0; +} + + +static void fsPerfSpliceToPipe(RTFILE hFile1, uint64_t cbFile) +{ + RTTestISub("splice/to-pipe"); + + /* + * splice was introduced in 2.6.17 according to the man-page. + */ + char szRelease[64]; + RTSystemQueryOSInfo(RTSYSOSINFO_RELEASE, szRelease, sizeof(szRelease)); + if (RTStrVersionCompare(szRelease, "2.6.17") < 0) + { + RTTestPassed(g_hTest, "too old kernel (%s)", szRelease); + return; + } + + uint64_t const cbFileMax = RT_MIN(cbFile, UINT32_MAX - PAGE_OFFSET_MASK); + signal(SIGPIPE, SIG_IGN); + + /* + * Allocate a buffer. + */ + FSPERFSPLICEARGS Args; + Args.cbBuf = RT_MIN(RT_MIN(cbFileMax, _16M), g_cbMaxBuffer); + Args.pbBuf = (uint8_t *)RTMemAlloc(Args.cbBuf); + while (!Args.pbBuf) + { + Args.cbBuf /= 8; + RTTESTI_CHECK_RETV(Args.cbBuf >= _64K); + Args.pbBuf = (uint8_t *)RTMemAlloc(Args.cbBuf); + } + + /* + * First iteration with default buffer content. + */ + fsPerfSpliceToPipeOne(&Args, hFile1, 0, cbFileMax, cbFileMax, 0xf6, true /*fCheckBuf*/, __LINE__); + if (cbFileMax == cbFile) + fsPerfSpliceToPipeOne(&Args, hFile1, 63, cbFileMax, cbFileMax - 63, 0xf6, true /*fCheckBuf*/, __LINE__); + else + fsPerfSpliceToPipeOne(&Args, hFile1, 63, cbFileMax - 63, cbFileMax - 63, 0xf6, true /*fCheckBuf*/, __LINE__); + + /* + * Write a block using the regular API and then send it, checking that + * the any caching that sendfile does is correctly updated. + */ + uint8_t bFiller = 0xf6; + size_t cbToSend = RT_MIN(cbFileMax, Args.cbBuf); + do + { + fsPerfSpliceToPipeOne(&Args, hFile1, 0, cbToSend, cbToSend, bFiller, true /*fCheckBuf*/, __LINE__); /* prime cache */ + + bFiller += 1; + fsPerfFillWriteBuf(0, Args.pbBuf, cbToSend, bFiller); + RTTESTI_CHECK_RC(RTFileWriteAt(hFile1, 0, Args.pbBuf, cbToSend, NULL), VINF_SUCCESS); + + fsPerfSpliceToPipeOne(&Args, hFile1, 0, cbToSend, cbToSend, bFiller, true /*fCheckBuf*/, __LINE__); + + cbToSend /= 2; + } while (cbToSend >= PAGE_SIZE && ((unsigned)bFiller - 0xf7U) < 64); + + /* + * Restore buffer content + */ + bFiller = 0xf6; + fsPerfFillWriteBuf(0, Args.pbBuf, Args.cbBuf, bFiller); + RTTESTI_CHECK_RC(RTFileWriteAt(hFile1, 0, Args.pbBuf, Args.cbBuf, NULL), VINF_SUCCESS); + + /* + * Do 128 random sends. + */ + uint64_t const cbSmall = RT_MIN(_256K, cbFileMax / 16); + for (uint32_t iTest = 0; iTest < 128; iTest++) + { + cbToSend = (size_t)RTRandU64Ex(1, iTest < 64 ? cbSmall : cbFileMax); + uint64_t const offToSendFrom = RTRandU64Ex(0, cbFile - 1); + uint64_t const cbSent = offToSendFrom + cbToSend <= cbFile ? cbToSend : cbFile - offToSendFrom; + + fsPerfSpliceToPipeOne(&Args, hFile1, offToSendFrom, cbToSend, cbSent, bFiller, true /*fCheckBuf*/, __LINE__); + } + + /* + * Benchmark it. + */ + Args.cCalls = 0; + uint32_t cIterations = 0; + uint64_t nsElapsed = 0; + for (;;) + { + uint64_t cNsThis = fsPerfSpliceToPipeOne(&Args, hFile1, 0, cbFileMax, cbFileMax, 0xf6, false /*fCheckBuf*/, __LINE__); + nsElapsed += cNsThis; + cIterations++; + if (!cNsThis || nsElapsed >= g_nsTestRun) + break; + } + uint64_t cbTotal = cbFileMax * cIterations; + RTTestIValue("latency", nsElapsed / Args.cCalls, RTTESTUNIT_NS_PER_CALL); + RTTestIValue("throughput", (uint64_t)(cbTotal / ((double)nsElapsed / RT_NS_1SEC)), RTTESTUNIT_BYTES_PER_SEC); + RTTestIValue("calls", Args.cCalls, RTTESTUNIT_CALLS); + RTTestIValue("bytes/call", cbTotal / Args.cCalls, RTTESTUNIT_BYTES); + RTTestIValue("iterations", cIterations, RTTESTUNIT_NONE); + RTTestIValue("bytes", cbTotal, RTTESTUNIT_BYTES); + if (g_fShowDuration) + RTTestIValue("duration", nsElapsed, RTTESTUNIT_NS); + + /* + * Cleanup. + */ + RTMemFree(Args.pbBuf); +} + + +/** Thread sending the bytes to a splice() call. */ +static DECLCALLBACK(int) fsPerfSpliceToFileThread(RTTHREAD hSelf, void *pvUser) +{ + FSPERFSPLICEARGS *pArgs = (FSPERFSPLICEARGS *)pvUser; + int rc = VINF_SUCCESS; + + uint64_t offFile = pArgs->offFile; + uint64_t cbTotalSent = 0; + while (cbTotalSent < pArgs->cbSent) + { + size_t const cbToSend = RT_MIN(pArgs->cbBuf, pArgs->cbSent - cbTotalSent); + fsPerfFillWriteBuf(offFile, pArgs->pbBuf, cbToSend, pArgs->bFiller); + RTTEST_CHECK_RC_BREAK(g_hTest, rc = RTPipeWriteBlocking(pArgs->hPipe, pArgs->pbBuf, cbToSend, NULL), VINF_SUCCESS); + offFile += cbToSend; + cbTotalSent += cbToSend; + } + + pArgs->tsThreadDone = RTTimeNanoTS(); + + RTTEST_CHECK_RC(g_hTest, RTPipeClose(pArgs->hPipe), VINF_SUCCESS); + pArgs->hPipe = NIL_RTPIPE; + + RT_NOREF(hSelf); + return rc; +} + + +/** Fill hFile1 via a pipe and the Linux-specific splice() syscall. */ +static uint64_t fsPerfSpliceToFileOne(FSPERFSPLICEARGS *pArgs, RTFILE hFile1, uint64_t offFile, + size_t cbSend, uint64_t cbSent, uint8_t bFiller, bool fCheckFile, unsigned iLine) +{ + /* Copy parameters to the argument structure: */ + pArgs->offFile = offFile; + pArgs->cbSend = cbSend; + pArgs->cbSent = cbSent; + pArgs->bFiller = bFiller; + pArgs->fCheckBuf = false; + + /* Create a socket pair. */ + pArgs->hPipe = NIL_RTPIPE; + RTPIPE hPipeR = NIL_RTPIPE; + RTTESTI_CHECK_RC_RET(RTPipeCreate(&hPipeR, &pArgs->hPipe, 0 /*fFlags*/), VINF_SUCCESS, 0); + + /* Create the receiving thread: */ + int rc; + RTTHREAD hThread = NIL_RTTHREAD; + RTTESTI_CHECK_RC(rc = RTThreadCreate(&hThread, fsPerfSpliceToFileThread, pArgs, 0, + RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "splicerecv"), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + /* + * Do the splicing. + */ + uint64_t const tsStart = RTTimeNanoTS(); + size_t cbLeft = cbSend; + size_t cbTotal = 0; + do + { + loff_t offFileOut = offFile; + ssize_t cbActual = syscall_splice((int)RTPipeToNative(hPipeR), NULL, (int)RTFileToNative(hFile1), &offFileOut, + cbLeft, 0 /*fFlags*/); + int const iErr = errno; + if (RT_UNLIKELY(cbActual < 0)) + { + RTTestIFailed("%u: splice(pipe, NULL, file, &%#RX64, %#zx, 0) failed (%zd): %d (%Rrc), offFileOut=%#RX64\n", + iLine, offFile, cbLeft, cbActual, iErr, RTErrConvertFromErrno(iErr), (uint64_t)offFileOut); + break; + } + RTTESTI_CHECK_BREAK((uint64_t)cbActual <= cbLeft); + if ((uint64_t)offFileOut != offFile + (uint64_t)cbActual) + { + RTTestIFailed("%u: splice(pipe, NULL, file, &%#RX64, %#zx, 0): %#zx; offFileOut=%#RX64, expected %#RX64\n", + iLine, offFile, cbLeft, cbActual, (uint64_t)offFileOut, offFile + (uint64_t)cbActual); + break; + } + if (cbActual > 0) + { + pArgs->cCalls++; + offFile += (size_t)cbActual; + cbTotal += (size_t)cbActual; + cbLeft -= (size_t)cbActual; + } + else + break; + } while (cbLeft > 0); + uint64_t const nsElapsed = RTTimeNanoTS() - tsStart; + + if (cbTotal != pArgs->cbSent) + RTTestIFailed("%u: spliced a total of %#zx bytes, expected %#zx!\n", iLine, cbTotal, pArgs->cbSent); + + RTTESTI_CHECK_RC(RTPipeClose(hPipeR), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTThreadWait(hThread, 30 * RT_NS_1SEC, NULL), VINF_SUCCESS); + + /* Check the file content. */ + if (fCheckFile && cbTotal == pArgs->cbSent) + { + offFile = pArgs->offFile; + cbLeft = cbSent; + while (cbLeft > 0) + { + size_t cbToRead = RT_MIN(cbLeft, pArgs->cbBuf); + RTTESTI_CHECK_RC_BREAK(RTFileReadAt(hFile1, offFile, pArgs->pbBuf, cbToRead, NULL), VINF_SUCCESS); + if (!fsPerfCheckReadBuf(iLine, offFile, pArgs->pbBuf, cbToRead, pArgs->bFiller)) + break; + offFile += cbToRead; + cbLeft -= cbToRead; + } + } + return nsElapsed; + } + return 0; +} + + +static void fsPerfSpliceToFile(RTFILE hFile1, uint64_t cbFile) +{ + RTTestISub("splice/to-file"); + + /* + * splice was introduced in 2.6.17 according to the man-page. + */ + char szRelease[64]; + RTSystemQueryOSInfo(RTSYSOSINFO_RELEASE, szRelease, sizeof(szRelease)); + if (RTStrVersionCompare(szRelease, "2.6.17") < 0) + { + RTTestPassed(g_hTest, "too old kernel (%s)", szRelease); + return; + } + + uint64_t const cbFileMax = RT_MIN(cbFile, UINT32_MAX - PAGE_OFFSET_MASK); + signal(SIGPIPE, SIG_IGN); + + /* + * Allocate a buffer. + */ + FSPERFSPLICEARGS Args; + Args.cbBuf = RT_MIN(RT_MIN(cbFileMax, _16M), g_cbMaxBuffer); + Args.pbBuf = (uint8_t *)RTMemAlloc(Args.cbBuf); + while (!Args.pbBuf) + { + Args.cbBuf /= 8; + RTTESTI_CHECK_RETV(Args.cbBuf >= _64K); + Args.pbBuf = (uint8_t *)RTMemAlloc(Args.cbBuf); + } + + /* + * Do the whole file. + */ + uint8_t bFiller = 0x76; + fsPerfSpliceToFileOne(&Args, hFile1, 0, cbFileMax, cbFileMax, bFiller, true /*fCheckFile*/, __LINE__); + + /* + * Do 64 random chunks (this is slower). + */ + uint64_t const cbSmall = RT_MIN(_256K, cbFileMax / 16); + for (uint32_t iTest = 0; iTest < 64; iTest++) + { + size_t const cbToWrite = (size_t)RTRandU64Ex(1, iTest < 24 ? cbSmall : cbFileMax); + uint64_t const offToWriteAt = RTRandU64Ex(0, cbFile - cbToWrite); + uint64_t const cbTryRead = cbToWrite + (iTest & 1 ? RTRandU32Ex(0, _64K) : 0); + + bFiller++; + fsPerfSpliceToFileOne(&Args, hFile1, offToWriteAt, cbTryRead, cbToWrite, bFiller, true /*fCheckFile*/, __LINE__); + } + + /* + * Benchmark it. + */ + Args.cCalls = 0; + uint32_t cIterations = 0; + uint64_t nsElapsed = 0; + for (;;) + { + uint64_t cNsThis = fsPerfSpliceToFileOne(&Args, hFile1, 0, cbFileMax, cbFileMax, 0xf6, false /*fCheckBuf*/, __LINE__); + nsElapsed += cNsThis; + cIterations++; + if (!cNsThis || nsElapsed >= g_nsTestRun) + break; + } + uint64_t cbTotal = cbFileMax * cIterations; + RTTestIValue("latency", nsElapsed / Args.cCalls, RTTESTUNIT_NS_PER_CALL); + RTTestIValue("throughput", (uint64_t)(cbTotal / ((double)nsElapsed / RT_NS_1SEC)), RTTESTUNIT_BYTES_PER_SEC); + RTTestIValue("calls", Args.cCalls, RTTESTUNIT_CALLS); + RTTestIValue("bytes/call", cbTotal / Args.cCalls, RTTESTUNIT_BYTES); + RTTestIValue("iterations", cIterations, RTTESTUNIT_NONE); + RTTestIValue("bytes", cbTotal, RTTESTUNIT_BYTES); + if (g_fShowDuration) + RTTestIValue("duration", nsElapsed, RTTESTUNIT_NS); + + /* + * Cleanup. + */ + RTMemFree(Args.pbBuf); +} + +#endif /* RT_OS_LINUX */ + +/** For fsPerfIoRead and fsPerfIoWrite. */ +#define PROFILE_IO_FN(a_szOperation, a_fnCall) \ + do \ + { \ + RTTESTI_CHECK_RC_RETV(RTFileSeek(hFile1, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); \ + uint64_t offActual = 0; \ + uint32_t cSeeks = 0; \ + \ + /* Estimate how many iterations we need to fill up the given timeslot: */ \ + fsPerfYield(); \ + uint64_t nsStart = RTTimeNanoTS(); \ + uint64_t ns; \ + do \ + ns = RTTimeNanoTS(); \ + while (ns == nsStart); \ + nsStart = ns; \ + \ + uint64_t iIteration = 0; \ + do \ + { \ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + iIteration++; \ + ns = RTTimeNanoTS() - nsStart; \ + } while (ns < RT_NS_10MS); \ + ns /= iIteration; \ + if (ns > g_nsPerNanoTSCall + 32) \ + ns -= g_nsPerNanoTSCall; \ + uint64_t cIterations = g_nsTestRun / ns; \ + if (cIterations < 2) \ + cIterations = 2; \ + else if (cIterations & 1) \ + cIterations++; \ + \ + /* Do the actual profiling: */ \ + cSeeks = 0; \ + iIteration = 0; \ + fsPerfYield(); \ + nsStart = RTTimeNanoTS(); \ + for (uint32_t iAdjust = 0; iAdjust < 4; iAdjust++) \ + { \ + for (; iIteration < cIterations; iIteration++)\ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + ns = RTTimeNanoTS() - nsStart;\ + if (ns >= g_nsTestRun - (g_nsTestRun / 10)) \ + break; \ + cIterations += cIterations / 4; \ + if (cIterations & 1) \ + cIterations++; \ + nsStart += g_nsPerNanoTSCall; \ + } \ + RTTestIValueF(ns / iIteration, \ + RTTESTUNIT_NS_PER_OCCURRENCE, a_szOperation "/seq/%RU32 latency", cbBlock); \ + RTTestIValueF((uint64_t)((double)(iIteration * cbBlock) / ((double)ns / RT_NS_1SEC)), \ + RTTESTUNIT_BYTES_PER_SEC, a_szOperation "/seq/%RU32 throughput", cbBlock); \ + RTTestIValueF(iIteration, \ + RTTESTUNIT_CALLS, a_szOperation "/seq/%RU32 calls", cbBlock); \ + RTTestIValueF((uint64_t)iIteration * cbBlock, \ + RTTESTUNIT_BYTES, a_szOperation "/seq/%RU32 bytes", cbBlock); \ + RTTestIValueF(cSeeks, \ + RTTESTUNIT_OCCURRENCES, a_szOperation "/seq/%RU32 seeks", cbBlock); \ + if (g_fShowDuration) \ + RTTestIValueF(ns, RTTESTUNIT_NS, a_szOperation "/seq/%RU32 duration", cbBlock); \ + } while (0) + + +/** + * One RTFileRead profiling iteration. + */ +DECL_FORCE_INLINE(int) fsPerfIoReadWorker(RTFILE hFile1, uint64_t cbFile, uint32_t cbBlock, uint8_t *pbBlock, + uint64_t *poffActual, uint32_t *pcSeeks) +{ + /* Do we need to seek back to the start? */ + if (*poffActual + cbBlock <= cbFile) + { /* likely */ } + else + { + RTTESTI_CHECK_RC_RET(RTFileSeek(hFile1, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS, rcCheck); + *pcSeeks += 1; + *poffActual = 0; + } + + size_t cbActuallyRead = 0; + RTTESTI_CHECK_RC_RET(RTFileRead(hFile1, pbBlock, cbBlock, &cbActuallyRead), VINF_SUCCESS, rcCheck); + if (cbActuallyRead == cbBlock) + { + *poffActual += cbActuallyRead; + return VINF_SUCCESS; + } + RTTestIFailed("RTFileRead at %#RX64 returned just %#x bytes, expected %#x", *poffActual, cbActuallyRead, cbBlock); + *poffActual += cbActuallyRead; + return VERR_READ_ERROR; +} + + +void fsPerfIoReadBlockSize(RTFILE hFile1, uint64_t cbFile, uint32_t cbBlock) +{ + RTTestISubF("IO - Sequential read %RU32", cbBlock); + if (cbBlock <= cbFile) + { + + uint8_t *pbBuf = (uint8_t *)RTMemPageAlloc(cbBlock); + if (pbBuf) + { + memset(pbBuf, 0xf7, cbBlock); + PROFILE_IO_FN("RTFileRead", fsPerfIoReadWorker(hFile1, cbFile, cbBlock, pbBuf, &offActual, &cSeeks)); + RTMemPageFree(pbBuf, cbBlock); + } + else + RTTestSkipped(g_hTest, "insufficient (virtual) memory available"); + } + else + RTTestSkipped(g_hTest, "test file too small"); +} + + +/** preadv is too new to be useful, so we use the readv api via this wrapper. */ +DECLINLINE(int) myFileSgReadAt(RTFILE hFile, RTFOFF off, PRTSGBUF pSgBuf, size_t cbToRead, size_t *pcbRead) +{ + int rc = RTFileSeek(hFile, off, RTFILE_SEEK_BEGIN, NULL); + if (RT_SUCCESS(rc)) + rc = RTFileSgRead(hFile, pSgBuf, cbToRead, pcbRead); + return rc; +} + + +void fsPerfRead(RTFILE hFile1, RTFILE hFileNoCache, uint64_t cbFile) +{ + RTTestISubF("IO - RTFileRead"); + + /* + * Allocate a big buffer we can play around with. Min size is 1MB. + */ + size_t cbMaxBuf = RT_MIN(_64M, g_cbMaxBuffer); + size_t cbBuf = cbFile < cbMaxBuf ? (size_t)cbFile : cbMaxBuf; + uint8_t *pbBuf = (uint8_t *)RTMemPageAlloc(cbBuf); + while (!pbBuf) + { + cbBuf /= 2; + RTTESTI_CHECK_RETV(cbBuf >= _1M); + pbBuf = (uint8_t *)RTMemPageAlloc(_32M); + } + +#if 1 + /* + * Start at the beginning and read the full buffer in random small chunks, thereby + * checking that unaligned buffer addresses, size and file offsets work fine. + */ + struct + { + uint64_t offFile; + uint32_t cbMax; + } aRuns[] = { { 0, 127 }, { cbFile - cbBuf, UINT32_MAX }, { 0, UINT32_MAX -1 }}; + for (uint32_t i = 0; i < RT_ELEMENTS(aRuns); i++) + { + memset(pbBuf, 0x55, cbBuf); + RTTESTI_CHECK_RC(RTFileSeek(hFile1, aRuns[i].offFile, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + for (size_t offBuf = 0; offBuf < cbBuf; ) + { + uint32_t const cbLeft = (uint32_t)(cbBuf - offBuf); + uint32_t const cbToRead = aRuns[i].cbMax < UINT32_MAX / 2 ? RTRandU32Ex(1, RT_MIN(aRuns[i].cbMax, cbLeft)) + : aRuns[i].cbMax == UINT32_MAX ? RTRandU32Ex(RT_MAX(cbLeft / 4, 1), cbLeft) + : RTRandU32Ex(cbLeft >= _8K ? _8K : 1, RT_MIN(_1M, cbLeft)); + size_t cbActual = 0; + RTTESTI_CHECK_RC(RTFileRead(hFile1, &pbBuf[offBuf], cbToRead, &cbActual), VINF_SUCCESS); + if (cbActual == cbToRead) + { + offBuf += cbActual; + RTTESTI_CHECK_MSG(RTFileTell(hFile1) == aRuns[i].offFile + offBuf, + ("%#RX64, expected %#RX64\n", RTFileTell(hFile1), aRuns[i].offFile + offBuf)); + } + else + { + RTTestIFailed("Attempting to read %#x bytes at %#zx, only got %#x bytes back! (cbLeft=%#x cbBuf=%#zx)\n", + cbToRead, offBuf, cbActual, cbLeft, cbBuf); + if (cbActual) + offBuf += cbActual; + else + pbBuf[offBuf++] = 0x11; + } + } + fsPerfCheckReadBuf(__LINE__, aRuns[i].offFile, pbBuf, cbBuf); + } + + /* + * Test reading beyond the end of the file. + */ + size_t const acbMax[] = { cbBuf, _64K, _16K, _4K, 256 }; + uint32_t const aoffFromEos[] = + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 32, 63, 64, 127, 128, 255, 254, 256, 1023, 1024, 2048, + 4092, 4093, 4094, 4095, 4096, 4097, 4098, 4099, 4100, 8192, 16384, 32767, 32768, 32769, 65535, 65536, _1M - 1 + }; + for (unsigned iMax = 0; iMax < RT_ELEMENTS(acbMax); iMax++) + { + size_t const cbMaxRead = acbMax[iMax]; + for (uint32_t iOffFromEos = 0; iOffFromEos < RT_ELEMENTS(aoffFromEos); iOffFromEos++) + { + uint32_t off = aoffFromEos[iOffFromEos]; + if (off >= cbMaxRead) + continue; + RTTESTI_CHECK_RC(RTFileSeek(hFile1, cbFile - off, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + size_t cbActual = ~(size_t)0; + RTTESTI_CHECK_RC(RTFileRead(hFile1, pbBuf, cbMaxRead, &cbActual), VINF_SUCCESS); + RTTESTI_CHECK(cbActual == off); + + RTTESTI_CHECK_RC(RTFileSeek(hFile1, cbFile - off, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + cbActual = ~(size_t)0; + RTTESTI_CHECK_RC(RTFileRead(hFile1, pbBuf, off, &cbActual), VINF_SUCCESS); + RTTESTI_CHECK_MSG(cbActual == off, ("%#zx vs %#zx\n", cbActual, off)); + + cbActual = ~(size_t)0; + RTTESTI_CHECK_RC(RTFileRead(hFile1, pbBuf, 1, &cbActual), VINF_SUCCESS); + RTTESTI_CHECK_MSG(cbActual == 0, ("cbActual=%zu\n", cbActual)); + + RTTESTI_CHECK_RC(RTFileRead(hFile1, pbBuf, cbMaxRead, NULL), VERR_EOF); + + /* Repeat using native APIs in case IPRT or other layers hide status codes: */ +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) + RTTESTI_CHECK_RC(RTFileSeek(hFile1, cbFile - off, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); +# ifdef RT_OS_OS2 + ULONG cbActual2 = ~(ULONG)0; + APIRET orc = DosRead((HFILE)RTFileToNative(hFile1), pbBuf, cbMaxRead, &cbActual2); + RTTESTI_CHECK_MSG(orc == NO_ERROR, ("orc=%u, expected 0\n", orc)); + RTTESTI_CHECK_MSG(cbActual2 == off, ("%#x vs %#x\n", cbActual2, off)); +# else + IO_STATUS_BLOCK const IosVirgin = RTNT_IO_STATUS_BLOCK_INITIALIZER; + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + NTSTATUS rcNt = NtReadFile((HANDLE)RTFileToNative(hFile1), NULL /*hEvent*/, NULL /*ApcRoutine*/, NULL /*ApcContext*/, + &Ios, pbBuf, (ULONG)cbMaxRead, NULL /*poffFile*/, NULL /*Key*/); + if (off == 0) + { + RTTESTI_CHECK_MSG(rcNt == STATUS_END_OF_FILE, ("rcNt=%#x, expected %#x\n", rcNt, STATUS_END_OF_FILE)); + RTTESTI_CHECK_MSG(Ios.Status == IosVirgin.Status /*slow?*/ || Ios.Status == STATUS_END_OF_FILE /*fastio?*/, + ("%#x vs %x/%#x; off=%#x\n", Ios.Status, IosVirgin.Status, STATUS_END_OF_FILE, off)); + RTTESTI_CHECK_MSG(Ios.Information == IosVirgin.Information /*slow*/ || Ios.Information == 0 /*fastio?*/, + ("%#zx vs %zx/0; off=%#x\n", Ios.Information, IosVirgin.Information, off)); + } + else + { + RTTESTI_CHECK_MSG(rcNt == STATUS_SUCCESS, ("rcNt=%#x, expected 0 (off=%#x cbMaxRead=%#zx)\n", rcNt, off, cbMaxRead)); + RTTESTI_CHECK_MSG(Ios.Status == STATUS_SUCCESS, ("%#x; off=%#x\n", Ios.Status, off)); + RTTESTI_CHECK_MSG(Ios.Information == off, ("%#zx vs %#x\n", Ios.Information, off)); + } +# endif + +# ifdef RT_OS_OS2 + cbActual2 = ~(ULONG)0; + orc = DosRead((HFILE)RTFileToNative(hFile1), pbBuf, 1, &cbActual2); + RTTESTI_CHECK_MSG(orc == NO_ERROR, ("orc=%u, expected 0\n", orc)); + RTTESTI_CHECK_MSG(cbActual2 == 0, ("cbActual2=%u\n", cbActual2)); +# else + RTNT_IO_STATUS_BLOCK_REINIT(&Ios); + rcNt = NtReadFile((HANDLE)RTFileToNative(hFile1), NULL /*hEvent*/, NULL /*ApcRoutine*/, NULL /*ApcContext*/, + &Ios, pbBuf, 1, NULL /*poffFile*/, NULL /*Key*/); + RTTESTI_CHECK_MSG(rcNt == STATUS_END_OF_FILE, ("rcNt=%#x, expected %#x\n", rcNt, STATUS_END_OF_FILE)); +# endif + +#endif + } + } + + /* + * Test reading beyond end of the file. + */ + for (unsigned iMax = 0; iMax < RT_ELEMENTS(acbMax); iMax++) + { + size_t const cbMaxRead = acbMax[iMax]; + for (uint32_t off = 0; off < 256; off++) + { + RTTESTI_CHECK_RC(RTFileSeek(hFile1, cbFile + off, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + size_t cbActual = ~(size_t)0; + RTTESTI_CHECK_RC(RTFileRead(hFile1, pbBuf, cbMaxRead, &cbActual), VINF_SUCCESS); + RTTESTI_CHECK(cbActual == 0); + + RTTESTI_CHECK_RC(RTFileRead(hFile1, pbBuf, cbMaxRead, NULL), VERR_EOF); + + /* Repeat using native APIs in case IPRT or other layers hid status codes: */ +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) + RTTESTI_CHECK_RC(RTFileSeek(hFile1, cbFile + off, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); +# ifdef RT_OS_OS2 + ULONG cbActual2 = ~(ULONG)0; + APIRET orc = DosRead((HFILE)RTFileToNative(hFile1), pbBuf, cbMaxRead, &cbActual2); + RTTESTI_CHECK_MSG(orc == NO_ERROR, ("orc=%u, expected 0\n", orc)); + RTTESTI_CHECK_MSG(cbActual2 == 0, ("%#x vs %#x\n", cbActual2, off)); +# else + IO_STATUS_BLOCK const IosVirgin = RTNT_IO_STATUS_BLOCK_INITIALIZER; + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + NTSTATUS rcNt = NtReadFile((HANDLE)RTFileToNative(hFile1), NULL /*hEvent*/, NULL /*ApcRoutine*/, NULL /*ApcContext*/, + &Ios, pbBuf, (ULONG)cbMaxRead, NULL /*poffFile*/, NULL /*Key*/); + RTTESTI_CHECK_MSG(rcNt == STATUS_END_OF_FILE, ("rcNt=%#x, expected %#x\n", rcNt, STATUS_END_OF_FILE)); + RTTESTI_CHECK_MSG(Ios.Status == IosVirgin.Status /*slow?*/ || Ios.Status == STATUS_END_OF_FILE /*fastio?*/, + ("%#x vs %x/%#x; off=%#x\n", Ios.Status, IosVirgin.Status, STATUS_END_OF_FILE, off)); + RTTESTI_CHECK_MSG(Ios.Information == IosVirgin.Information /*slow*/ || Ios.Information == 0 /*fastio?*/, + ("%#zx vs %zx/0; off=%#x\n", Ios.Information, IosVirgin.Information, off)); + + /* Need to work with sector size on uncached, but might be worth it for non-fastio path. */ + uint32_t cbSector = 0x1000; + uint32_t off2 = off * cbSector + (cbFile & (cbSector - 1) ? cbSector - (cbFile & (cbSector - 1)) : 0); + RTTESTI_CHECK_RC(RTFileSeek(hFileNoCache, cbFile + off2, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + size_t const cbMaxRead2 = RT_ALIGN_Z(cbMaxRead, cbSector); + RTNT_IO_STATUS_BLOCK_REINIT(&Ios); + rcNt = NtReadFile((HANDLE)RTFileToNative(hFileNoCache), NULL /*hEvent*/, NULL /*ApcRoutine*/, NULL /*ApcContext*/, + &Ios, pbBuf, (ULONG)cbMaxRead2, NULL /*poffFile*/, NULL /*Key*/); + RTTESTI_CHECK_MSG(rcNt == STATUS_END_OF_FILE, + ("rcNt=%#x, expected %#x; off2=%x cbMaxRead2=%#x\n", rcNt, STATUS_END_OF_FILE, off2, cbMaxRead2)); + RTTESTI_CHECK_MSG(Ios.Status == IosVirgin.Status /*slow?*/, + ("%#x vs %x; off2=%#x cbMaxRead2=%#x\n", Ios.Status, IosVirgin.Status, off2, cbMaxRead2)); + RTTESTI_CHECK_MSG(Ios.Information == IosVirgin.Information /*slow*/, + ("%#zx vs %zx; off2=%#x cbMaxRead2=%#x\n", Ios.Information, IosVirgin.Information, off2, cbMaxRead2)); +# endif +#endif + } + } + + /* + * Do uncached access, must be page aligned. + */ + uint32_t cbPage = PAGE_SIZE; + memset(pbBuf, 0x66, cbBuf); + if (!g_fIgnoreNoCache || hFileNoCache != NIL_RTFILE) + { + RTTESTI_CHECK_RC(RTFileSeek(hFileNoCache, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + for (size_t offBuf = 0; offBuf < cbBuf; ) + { + uint32_t const cPagesLeft = (uint32_t)((cbBuf - offBuf) / cbPage); + uint32_t const cPagesToRead = RTRandU32Ex(1, cPagesLeft); + size_t const cbToRead = cPagesToRead * (size_t)cbPage; + size_t cbActual = 0; + RTTESTI_CHECK_RC(RTFileRead(hFileNoCache, &pbBuf[offBuf], cbToRead, &cbActual), VINF_SUCCESS); + if (cbActual == cbToRead) + offBuf += cbActual; + else + { + RTTestIFailed("Attempting to read %#zx bytes at %#zx, only got %#x bytes back!\n", cbToRead, offBuf, cbActual); + if (cbActual) + offBuf += cbActual; + else + { + memset(&pbBuf[offBuf], 0x11, cbPage); + offBuf += cbPage; + } + } + } + fsPerfCheckReadBuf(__LINE__, 0, pbBuf, cbBuf); + } + + /* + * Check reading zero bytes at the end of the file. + * Requires native call because RTFileWrite doesn't call kernel on zero byte reads. + */ + RTTESTI_CHECK_RC(RTFileSeek(hFile1, 0, RTFILE_SEEK_END, NULL), VINF_SUCCESS); +# ifdef RT_OS_WINDOWS + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + NTSTATUS rcNt = NtReadFile((HANDLE)RTFileToNative(hFile1), NULL, NULL, NULL, &Ios, pbBuf, 0, NULL, NULL); + RTTESTI_CHECK_MSG(rcNt == STATUS_SUCCESS, ("rcNt=%#x", rcNt)); + RTTESTI_CHECK(Ios.Status == STATUS_SUCCESS); + RTTESTI_CHECK(Ios.Information == 0); + + IO_STATUS_BLOCK const IosVirgin = RTNT_IO_STATUS_BLOCK_INITIALIZER; + RTNT_IO_STATUS_BLOCK_REINIT(&Ios); + rcNt = NtReadFile((HANDLE)RTFileToNative(hFile1), NULL, NULL, NULL, &Ios, pbBuf, 1, NULL, NULL); + RTTESTI_CHECK_MSG(rcNt == STATUS_END_OF_FILE, ("rcNt=%#x", rcNt)); + RTTESTI_CHECK_MSG(Ios.Status == IosVirgin.Status /*slow?*/ || Ios.Status == STATUS_END_OF_FILE /*fastio?*/, + ("%#x vs %x/%#x\n", Ios.Status, IosVirgin.Status, STATUS_END_OF_FILE)); + RTTESTI_CHECK_MSG(Ios.Information == IosVirgin.Information /*slow*/ || Ios.Information == 0 /*fastio?*/, + ("%#zx vs %zx/0\n", Ios.Information, IosVirgin.Information)); +# else + ssize_t cbRead = read((int)RTFileToNative(hFile1), pbBuf, 0); + RTTESTI_CHECK(cbRead == 0); +# endif + +#else + RT_NOREF(hFileNoCache); +#endif + + /* + * Scatter read function operation. + */ +#ifdef RT_OS_WINDOWS + /** @todo RTFileSgReadAt is just a RTFileReadAt loop for windows NT. Need + * to use ReadFileScatter (nocache + page aligned). */ +#elif !defined(RT_OS_OS2) /** @todo implement RTFileSg using list i/o */ + +# ifdef UIO_MAXIOV + RTSGSEG aSegs[UIO_MAXIOV]; +# else + RTSGSEG aSegs[512]; +# endif + RTSGBUF SgBuf; + uint32_t cIncr = 1; + for (uint32_t cSegs = 1; cSegs <= RT_ELEMENTS(aSegs); cSegs += cIncr) + { + size_t const cbSeg = cbBuf / cSegs; + size_t const cbToRead = cbSeg * cSegs; + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + aSegs[iSeg].cbSeg = cbSeg; + aSegs[iSeg].pvSeg = &pbBuf[cbToRead - (iSeg + 1) * cbSeg]; + } + RTSgBufInit(&SgBuf, &aSegs[0], cSegs); + int rc = myFileSgReadAt(hFile1, 0, &SgBuf, cbToRead, NULL); + if (RT_SUCCESS(rc)) + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + if (!fsPerfCheckReadBuf(__LINE__, iSeg * cbSeg, &pbBuf[cbToRead - (iSeg + 1) * cbSeg], cbSeg)) + { + cSegs = RT_ELEMENTS(aSegs); + break; + } + } + else + { + RTTestIFailed("myFileSgReadAt failed: %Rrc - cSegs=%u cbSegs=%#zx cbToRead=%#zx", rc, cSegs, cbSeg, cbToRead); + break; + } + if (cSegs == 16) + cIncr = 7; + else if (cSegs == 16 * 7 + 16 /*= 128*/) + cIncr = 64; + } + + for (uint32_t iTest = 0; iTest < 128; iTest++) + { + uint32_t cSegs = RTRandU32Ex(1, RT_ELEMENTS(aSegs)); + uint32_t iZeroSeg = cSegs > 10 ? RTRandU32Ex(0, cSegs - 1) : UINT32_MAX / 2; + uint32_t cZeroSegs = cSegs > 10 ? RTRandU32Ex(1, RT_MIN(cSegs - iZeroSeg, 25)) : 0; + size_t cbToRead = 0; + size_t cbLeft = cbBuf; + uint8_t *pbCur = &pbBuf[cbBuf]; + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + uint32_t iAlign = RTRandU32Ex(0, 3); + if (iAlign & 2) /* end is page aligned */ + { + cbLeft -= (uintptr_t)pbCur & PAGE_OFFSET_MASK; + pbCur -= (uintptr_t)pbCur & PAGE_OFFSET_MASK; + } + + size_t cbSegOthers = (cSegs - iSeg) * _8K; + size_t cbSegMax = cbLeft > cbSegOthers ? cbLeft - cbSegOthers + : cbLeft > cSegs ? cbLeft - cSegs + : cbLeft; + size_t cbSeg = cbLeft != 0 ? RTRandU32Ex(0, cbSegMax) : 0; + if (iAlign & 1) /* start is page aligned */ + cbSeg += ((uintptr_t)pbCur - cbSeg) & PAGE_OFFSET_MASK; + + if (iSeg - iZeroSeg < cZeroSegs) + cbSeg = 0; + + cbToRead += cbSeg; + cbLeft -= cbSeg; + pbCur -= cbSeg; + aSegs[iSeg].cbSeg = cbSeg; + aSegs[iSeg].pvSeg = pbCur; + } + + uint64_t offFile = cbToRead < cbFile ? RTRandU64Ex(0, cbFile - cbToRead) : 0; + RTSgBufInit(&SgBuf, &aSegs[0], cSegs); + int rc = myFileSgReadAt(hFile1, offFile, &SgBuf, cbToRead, NULL); + if (RT_SUCCESS(rc)) + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + if (!fsPerfCheckReadBuf(__LINE__, offFile, (uint8_t *)aSegs[iSeg].pvSeg, aSegs[iSeg].cbSeg)) + { + RTTestIFailureDetails("iSeg=%#x cSegs=%#x cbSeg=%#zx cbToRead=%#zx\n", iSeg, cSegs, aSegs[iSeg].cbSeg, cbToRead); + iTest = _16K; + break; + } + offFile += aSegs[iSeg].cbSeg; + } + else + { + RTTestIFailed("myFileSgReadAt failed: %Rrc - cSegs=%#x cbToRead=%#zx", rc, cSegs, cbToRead); + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + RTTestIFailureDetails("aSeg[%u] = %p LB %#zx (last %p)\n", iSeg, aSegs[iSeg].pvSeg, aSegs[iSeg].cbSeg, + (uint8_t *)aSegs[iSeg].pvSeg + aSegs[iSeg].cbSeg - 1); + break; + } + } + + /* reading beyond the end of the file */ + for (uint32_t cSegs = 1; cSegs < 6; cSegs++) + for (uint32_t iTest = 0; iTest < 128; iTest++) + { + uint32_t const cbToRead = RTRandU32Ex(0, cbBuf); + uint32_t const cbBeyond = cbToRead ? RTRandU32Ex(0, cbToRead) : 0; + uint32_t const cbSeg = cbToRead / cSegs; + uint32_t cbLeft = cbToRead; + uint8_t *pbCur = &pbBuf[cbToRead]; + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + aSegs[iSeg].cbSeg = iSeg + 1 < cSegs ? cbSeg : cbLeft; + aSegs[iSeg].pvSeg = pbCur -= aSegs[iSeg].cbSeg; + cbLeft -= aSegs[iSeg].cbSeg; + } + Assert(pbCur == pbBuf); + + uint64_t offFile = cbFile + cbBeyond - cbToRead; + RTSgBufInit(&SgBuf, &aSegs[0], cSegs); + int rcExpect = cbBeyond == 0 || cbToRead == 0 ? VINF_SUCCESS : VERR_EOF; + int rc = myFileSgReadAt(hFile1, offFile, &SgBuf, cbToRead, NULL); + if (rc != rcExpect) + { + RTTestIFailed("myFileSgReadAt failed: %Rrc - cSegs=%#x cbToRead=%#zx cbBeyond=%#zx\n", rc, cSegs, cbToRead, cbBeyond); + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + RTTestIFailureDetails("aSeg[%u] = %p LB %#zx (last %p)\n", iSeg, aSegs[iSeg].pvSeg, aSegs[iSeg].cbSeg, + (uint8_t *)aSegs[iSeg].pvSeg + aSegs[iSeg].cbSeg - 1); + } + + RTSgBufInit(&SgBuf, &aSegs[0], cSegs); + size_t cbActual = 0; + rc = myFileSgReadAt(hFile1, offFile, &SgBuf, cbToRead, &cbActual); + if (rc != VINF_SUCCESS || cbActual != cbToRead - cbBeyond) + RTTestIFailed("myFileSgReadAt failed: %Rrc cbActual=%#zu - cSegs=%#x cbToRead=%#zx cbBeyond=%#zx expected %#zx\n", + rc, cbActual, cSegs, cbToRead, cbBeyond, cbToRead - cbBeyond); + if (RT_SUCCESS(rc) && cbActual > 0) + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + if (!fsPerfCheckReadBuf(__LINE__, offFile, (uint8_t *)aSegs[iSeg].pvSeg, RT_MIN(cbActual, aSegs[iSeg].cbSeg))) + { + RTTestIFailureDetails("iSeg=%#x cSegs=%#x cbSeg=%#zx cbActual%#zx cbToRead=%#zx cbBeyond=%#zx\n", + iSeg, cSegs, aSegs[iSeg].cbSeg, cbActual, cbToRead, cbBeyond); + iTest = _16K; + break; + } + if (cbActual <= aSegs[iSeg].cbSeg) + break; + cbActual -= aSegs[iSeg].cbSeg; + offFile += aSegs[iSeg].cbSeg; + } + } + +#endif + + /* + * Other OS specific stuff. + */ +#ifdef RT_OS_WINDOWS + /* Check that reading at an offset modifies the position: */ + RTTESTI_CHECK_RC(RTFileSeek(hFile1, 0, RTFILE_SEEK_END, NULL), VINF_SUCCESS); + RTTESTI_CHECK(RTFileTell(hFile1) == cbFile); + + RTNT_IO_STATUS_BLOCK_REINIT(&Ios); + LARGE_INTEGER offNt; + offNt.QuadPart = cbFile / 2; + rcNt = NtReadFile((HANDLE)RTFileToNative(hFile1), NULL, NULL, NULL, &Ios, pbBuf, _4K, &offNt, NULL); + RTTESTI_CHECK_MSG(rcNt == STATUS_SUCCESS, ("rcNt=%#x", rcNt)); + RTTESTI_CHECK(Ios.Status == STATUS_SUCCESS); + RTTESTI_CHECK(Ios.Information == _4K); + RTTESTI_CHECK(RTFileTell(hFile1) == cbFile / 2 + _4K); + fsPerfCheckReadBuf(__LINE__, cbFile / 2, pbBuf, _4K); +#endif + + + RTMemPageFree(pbBuf, cbBuf); +} + + +/** + * One RTFileWrite profiling iteration. + */ +DECL_FORCE_INLINE(int) fsPerfIoWriteWorker(RTFILE hFile1, uint64_t cbFile, uint32_t cbBlock, uint8_t *pbBlock, + uint64_t *poffActual, uint32_t *pcSeeks) +{ + /* Do we need to seek back to the start? */ + if (*poffActual + cbBlock <= cbFile) + { /* likely */ } + else + { + RTTESTI_CHECK_RC_RET(RTFileSeek(hFile1, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS, rcCheck); + *pcSeeks += 1; + *poffActual = 0; + } + + size_t cbActuallyWritten = 0; + RTTESTI_CHECK_RC_RET(RTFileWrite(hFile1, pbBlock, cbBlock, &cbActuallyWritten), VINF_SUCCESS, rcCheck); + if (cbActuallyWritten == cbBlock) + { + *poffActual += cbActuallyWritten; + return VINF_SUCCESS; + } + RTTestIFailed("RTFileWrite at %#RX64 returned just %#x bytes, expected %#x", *poffActual, cbActuallyWritten, cbBlock); + *poffActual += cbActuallyWritten; + return VERR_WRITE_ERROR; +} + + +void fsPerfIoWriteBlockSize(RTFILE hFile1, uint64_t cbFile, uint32_t cbBlock) +{ + RTTestISubF("IO - Sequential write %RU32", cbBlock); + + if (cbBlock <= cbFile) + { + uint8_t *pbBuf = (uint8_t *)RTMemPageAlloc(cbBlock); + if (pbBuf) + { + memset(pbBuf, 0xf7, cbBlock); + PROFILE_IO_FN("RTFileWrite", fsPerfIoWriteWorker(hFile1, cbFile, cbBlock, pbBuf, &offActual, &cSeeks)); + RTMemPageFree(pbBuf, cbBlock); + } + else + RTTestSkipped(g_hTest, "insufficient (virtual) memory available"); + } + else + RTTestSkipped(g_hTest, "test file too small"); +} + + +/** pwritev is too new to be useful, so we use the writev api via this wrapper. */ +DECLINLINE(int) myFileSgWriteAt(RTFILE hFile, RTFOFF off, PRTSGBUF pSgBuf, size_t cbToWrite, size_t *pcbWritten) +{ + int rc = RTFileSeek(hFile, off, RTFILE_SEEK_BEGIN, NULL); + if (RT_SUCCESS(rc)) + rc = RTFileSgWrite(hFile, pSgBuf, cbToWrite, pcbWritten); + return rc; +} + + +void fsPerfWrite(RTFILE hFile1, RTFILE hFileNoCache, RTFILE hFileWriteThru, uint64_t cbFile) +{ + RTTestISubF("IO - RTFileWrite"); + + /* + * Allocate a big buffer we can play around with. Min size is 1MB. + */ + size_t cbMaxBuf = RT_MIN(_64M, g_cbMaxBuffer); + size_t cbBuf = cbFile < cbMaxBuf ? (size_t)cbFile : cbMaxBuf; + uint8_t *pbBuf = (uint8_t *)RTMemPageAlloc(cbBuf); + while (!pbBuf) + { + cbBuf /= 2; + RTTESTI_CHECK_RETV(cbBuf >= _1M); + pbBuf = (uint8_t *)RTMemPageAlloc(_32M); + } + + uint8_t bFiller = 0x88; + +#if 1 + /* + * Start at the beginning and write out the full buffer in random small chunks, thereby + * checking that unaligned buffer addresses, size and file offsets work fine. + */ + struct + { + uint64_t offFile; + uint32_t cbMax; + } aRuns[] = { { 0, 127 }, { cbFile - cbBuf, UINT32_MAX }, { 0, UINT32_MAX -1 }}; + for (uint32_t i = 0; i < RT_ELEMENTS(aRuns); i++, bFiller) + { + fsPerfFillWriteBuf(aRuns[i].offFile, pbBuf, cbBuf, bFiller); + fsPerfCheckReadBuf(__LINE__, aRuns[i].offFile, pbBuf, cbBuf, bFiller); + + RTTESTI_CHECK_RC(RTFileSeek(hFile1, aRuns[i].offFile, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + for (size_t offBuf = 0; offBuf < cbBuf; ) + { + uint32_t const cbLeft = (uint32_t)(cbBuf - offBuf); + uint32_t const cbToWrite = aRuns[i].cbMax < UINT32_MAX / 2 ? RTRandU32Ex(1, RT_MIN(aRuns[i].cbMax, cbLeft)) + : aRuns[i].cbMax == UINT32_MAX ? RTRandU32Ex(RT_MAX(cbLeft / 4, 1), cbLeft) + : RTRandU32Ex(cbLeft >= _8K ? _8K : 1, RT_MIN(_1M, cbLeft)); + size_t cbActual = 0; + RTTESTI_CHECK_RC(RTFileWrite(hFile1, &pbBuf[offBuf], cbToWrite, &cbActual), VINF_SUCCESS); + if (cbActual == cbToWrite) + { + offBuf += cbActual; + RTTESTI_CHECK_MSG(RTFileTell(hFile1) == aRuns[i].offFile + offBuf, + ("%#RX64, expected %#RX64\n", RTFileTell(hFile1), aRuns[i].offFile + offBuf)); + } + else + { + RTTestIFailed("Attempting to write %#x bytes at %#zx (%#x left), only got %#x written!\n", + cbToWrite, offBuf, cbLeft, cbActual); + if (cbActual) + offBuf += cbActual; + else + pbBuf[offBuf++] = 0x11; + } + } + + RTTESTI_CHECK_RC(RTFileReadAt(hFile1, aRuns[i].offFile, pbBuf, cbBuf, NULL), VINF_SUCCESS); + fsPerfCheckReadBuf(__LINE__, aRuns[i].offFile, pbBuf, cbBuf, bFiller); + } + + + /* + * Do uncached and write-thru accesses, must be page aligned. + */ + RTFILE ahFiles[2] = { hFileWriteThru, hFileNoCache }; + for (unsigned iFile = 0; iFile < RT_ELEMENTS(ahFiles); iFile++, bFiller++) + { + if (g_fIgnoreNoCache && ahFiles[iFile] == NIL_RTFILE) + continue; + + fsPerfFillWriteBuf(0, pbBuf, cbBuf, bFiller); + fsPerfCheckReadBuf(__LINE__, 0, pbBuf, cbBuf, bFiller); + RTTESTI_CHECK_RC(RTFileSeek(ahFiles[iFile], 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + + uint32_t cbPage = PAGE_SIZE; + for (size_t offBuf = 0; offBuf < cbBuf; ) + { + uint32_t const cPagesLeft = (uint32_t)((cbBuf - offBuf) / cbPage); + uint32_t const cPagesToWrite = RTRandU32Ex(1, cPagesLeft); + size_t const cbToWrite = cPagesToWrite * (size_t)cbPage; + size_t cbActual = 0; + RTTESTI_CHECK_RC(RTFileWrite(ahFiles[iFile], &pbBuf[offBuf], cbToWrite, &cbActual), VINF_SUCCESS); + if (cbActual == cbToWrite) + { + RTTESTI_CHECK_RC(RTFileReadAt(hFile1, offBuf, pbBuf, cbToWrite, NULL), VINF_SUCCESS); + fsPerfCheckReadBuf(__LINE__, offBuf, pbBuf, cbToWrite, bFiller); + offBuf += cbActual; + } + else + { + RTTestIFailed("Attempting to read %#zx bytes at %#zx, only got %#x written!\n", cbToWrite, offBuf, cbActual); + if (cbActual) + offBuf += cbActual; + else + { + memset(&pbBuf[offBuf], 0x11, cbPage); + offBuf += cbPage; + } + } + } + + RTTESTI_CHECK_RC(RTFileReadAt(ahFiles[iFile], 0, pbBuf, cbBuf, NULL), VINF_SUCCESS); + fsPerfCheckReadBuf(__LINE__, 0, pbBuf, cbBuf, bFiller); + } + + /* + * Check the behavior of writing zero bytes to the file _4K from the end + * using native API. In the olden days zero sized write have been known + * to be used to truncate a file. + */ + RTTESTI_CHECK_RC(RTFileSeek(hFile1, -_4K, RTFILE_SEEK_END, NULL), VINF_SUCCESS); +# ifdef RT_OS_WINDOWS + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + NTSTATUS rcNt = NtWriteFile((HANDLE)RTFileToNative(hFile1), NULL, NULL, NULL, &Ios, pbBuf, 0, NULL, NULL); + RTTESTI_CHECK_MSG(rcNt == STATUS_SUCCESS, ("rcNt=%#x", rcNt)); + RTTESTI_CHECK(Ios.Status == STATUS_SUCCESS); + RTTESTI_CHECK(Ios.Information == 0); +# else + ssize_t cbWritten = write((int)RTFileToNative(hFile1), pbBuf, 0); + RTTESTI_CHECK(cbWritten == 0); +# endif + RTTESTI_CHECK_RC(RTFileRead(hFile1, pbBuf, _4K, NULL), VINF_SUCCESS); + fsPerfCheckReadBuf(__LINE__, cbFile - _4K, pbBuf, _4K, pbBuf[0x8]); + +#else + RT_NOREF(hFileNoCache, hFileWriteThru); +#endif + + /* + * Gather write function operation. + */ +#ifdef RT_OS_WINDOWS + /** @todo RTFileSgWriteAt is just a RTFileWriteAt loop for windows NT. Need + * to use WriteFileGather (nocache + page aligned). */ +#elif !defined(RT_OS_OS2) /** @todo implement RTFileSg using list i/o */ + +# ifdef UIO_MAXIOV + RTSGSEG aSegs[UIO_MAXIOV]; +# else + RTSGSEG aSegs[512]; +# endif + RTSGBUF SgBuf; + uint32_t cIncr = 1; + for (uint32_t cSegs = 1; cSegs <= RT_ELEMENTS(aSegs); cSegs += cIncr, bFiller++) + { + size_t const cbSeg = cbBuf / cSegs; + size_t const cbToWrite = cbSeg * cSegs; + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + aSegs[iSeg].cbSeg = cbSeg; + aSegs[iSeg].pvSeg = &pbBuf[cbToWrite - (iSeg + 1) * cbSeg]; + fsPerfFillWriteBuf(iSeg * cbSeg, (uint8_t *)aSegs[iSeg].pvSeg, cbSeg, bFiller); + } + RTSgBufInit(&SgBuf, &aSegs[0], cSegs); + int rc = myFileSgWriteAt(hFile1, 0, &SgBuf, cbToWrite, NULL); + if (RT_SUCCESS(rc)) + { + RTTESTI_CHECK_RC(RTFileReadAt(hFile1, 0, pbBuf, cbToWrite, NULL), VINF_SUCCESS); + fsPerfCheckReadBuf(__LINE__, 0, pbBuf, cbToWrite, bFiller); + } + else + { + RTTestIFailed("myFileSgWriteAt failed: %Rrc - cSegs=%u cbSegs=%#zx cbToWrite=%#zx", rc, cSegs, cbSeg, cbToWrite); + break; + } + if (cSegs == 16) + cIncr = 7; + else if (cSegs == 16 * 7 + 16 /*= 128*/) + cIncr = 64; + } + + /* random stuff, including zero segments. */ + for (uint32_t iTest = 0; iTest < 128; iTest++, bFiller++) + { + uint32_t cSegs = RTRandU32Ex(1, RT_ELEMENTS(aSegs)); + uint32_t iZeroSeg = cSegs > 10 ? RTRandU32Ex(0, cSegs - 1) : UINT32_MAX / 2; + uint32_t cZeroSegs = cSegs > 10 ? RTRandU32Ex(1, RT_MIN(cSegs - iZeroSeg, 25)) : 0; + size_t cbToWrite = 0; + size_t cbLeft = cbBuf; + uint8_t *pbCur = &pbBuf[cbBuf]; + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + { + uint32_t iAlign = RTRandU32Ex(0, 3); + if (iAlign & 2) /* end is page aligned */ + { + cbLeft -= (uintptr_t)pbCur & PAGE_OFFSET_MASK; + pbCur -= (uintptr_t)pbCur & PAGE_OFFSET_MASK; + } + + size_t cbSegOthers = (cSegs - iSeg) * _8K; + size_t cbSegMax = cbLeft > cbSegOthers ? cbLeft - cbSegOthers + : cbLeft > cSegs ? cbLeft - cSegs + : cbLeft; + size_t cbSeg = cbLeft != 0 ? RTRandU32Ex(0, cbSegMax) : 0; + if (iAlign & 1) /* start is page aligned */ + cbSeg += ((uintptr_t)pbCur - cbSeg) & PAGE_OFFSET_MASK; + + if (iSeg - iZeroSeg < cZeroSegs) + cbSeg = 0; + + cbToWrite += cbSeg; + cbLeft -= cbSeg; + pbCur -= cbSeg; + aSegs[iSeg].cbSeg = cbSeg; + aSegs[iSeg].pvSeg = pbCur; + } + + uint64_t const offFile = cbToWrite < cbFile ? RTRandU64Ex(0, cbFile - cbToWrite) : 0; + uint64_t offFill = offFile; + for (uint32_t iSeg = 0; iSeg < cSegs; iSeg++) + if (aSegs[iSeg].cbSeg) + { + fsPerfFillWriteBuf(offFill, (uint8_t *)aSegs[iSeg].pvSeg, aSegs[iSeg].cbSeg, bFiller); + offFill += aSegs[iSeg].cbSeg; + } + + RTSgBufInit(&SgBuf, &aSegs[0], cSegs); + int rc = myFileSgWriteAt(hFile1, offFile, &SgBuf, cbToWrite, NULL); + if (RT_SUCCESS(rc)) + { + RTTESTI_CHECK_RC(RTFileReadAt(hFile1, offFile, pbBuf, cbToWrite, NULL), VINF_SUCCESS); + fsPerfCheckReadBuf(__LINE__, offFile, pbBuf, cbToWrite, bFiller); + } + else + { + RTTestIFailed("myFileSgWriteAt failed: %Rrc - cSegs=%#x cbToWrite=%#zx", rc, cSegs, cbToWrite); + break; + } + } + +#endif + + /* + * Other OS specific stuff. + */ +#ifdef RT_OS_WINDOWS + /* Check that reading at an offset modifies the position: */ + RTTESTI_CHECK_RC(RTFileReadAt(hFile1, cbFile / 2, pbBuf, _4K, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSeek(hFile1, 0, RTFILE_SEEK_END, NULL), VINF_SUCCESS); + RTTESTI_CHECK(RTFileTell(hFile1) == cbFile); + + RTNT_IO_STATUS_BLOCK_REINIT(&Ios); + LARGE_INTEGER offNt; + offNt.QuadPart = cbFile / 2; + rcNt = NtWriteFile((HANDLE)RTFileToNative(hFile1), NULL, NULL, NULL, &Ios, pbBuf, _4K, &offNt, NULL); + RTTESTI_CHECK_MSG(rcNt == STATUS_SUCCESS, ("rcNt=%#x", rcNt)); + RTTESTI_CHECK(Ios.Status == STATUS_SUCCESS); + RTTESTI_CHECK(Ios.Information == _4K); + RTTESTI_CHECK(RTFileTell(hFile1) == cbFile / 2 + _4K); +#endif + + RTMemPageFree(pbBuf, cbBuf); +} + + +/** + * Worker for testing RTFileFlush. + */ +DECL_FORCE_INLINE(int) fsPerfFSyncWorker(RTFILE hFile1, uint64_t cbFile, uint8_t *pbBuf, size_t cbBuf, uint64_t *poffFile) +{ + if (*poffFile + cbBuf <= cbFile) + { /* likely */ } + else + { + RTTESTI_CHECK_RC(RTFileSeek(hFile1, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + *poffFile = 0; + } + + RTTESTI_CHECK_RC_RET(RTFileWrite(hFile1, pbBuf, cbBuf, NULL), VINF_SUCCESS, rcCheck); + RTTESTI_CHECK_RC_RET(RTFileFlush(hFile1), VINF_SUCCESS, rcCheck); + + *poffFile += cbBuf; + return VINF_SUCCESS; +} + + +void fsPerfFSync(RTFILE hFile1, uint64_t cbFile) +{ + RTTestISub("fsync"); + + RTTESTI_CHECK_RC(RTFileFlush(hFile1), VINF_SUCCESS); + + PROFILE_FN(RTFileFlush(hFile1), g_nsTestRun, "RTFileFlush"); + + size_t cbBuf = PAGE_SIZE; + uint8_t *pbBuf = (uint8_t *)RTMemPageAlloc(cbBuf); + RTTESTI_CHECK_RETV(pbBuf != NULL); + memset(pbBuf, 0xf4, cbBuf); + + RTTESTI_CHECK_RC(RTFileSeek(hFile1, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + uint64_t offFile = 0; + PROFILE_FN(fsPerfFSyncWorker(hFile1, cbFile, pbBuf, cbBuf, &offFile), g_nsTestRun, "RTFileWrite[Page]/RTFileFlush"); + + RTMemPageFree(pbBuf, cbBuf); +} + + +#ifndef RT_OS_OS2 +/** + * Worker for profiling msync. + */ +DECL_FORCE_INLINE(int) fsPerfMSyncWorker(uint8_t *pbMapping, size_t offMapping, size_t cbFlush, size_t *pcbFlushed) +{ + uint8_t *pbCur = &pbMapping[offMapping]; + for (size_t offFlush = 0; offFlush < cbFlush; offFlush += PAGE_SIZE) + *(size_t volatile *)&pbCur[offFlush + 8] = cbFlush; +# ifdef RT_OS_WINDOWS + CHECK_WINAPI_CALL(FlushViewOfFile(pbCur, cbFlush) == TRUE); +# else + RTTESTI_CHECK(msync(pbCur, cbFlush, MS_SYNC) == 0); +# endif + if (*pcbFlushed < offMapping + cbFlush) + *pcbFlushed = offMapping + cbFlush; + return VINF_SUCCESS; +} +#endif /* !RT_OS_OS2 */ + + +void fsPerfMMap(RTFILE hFile1, RTFILE hFileNoCache, uint64_t cbFile) +{ + RTTestISub("mmap"); +#if !defined(RT_OS_OS2) + static const char * const s_apszStates[] = { "readonly", "writecopy", "readwrite" }; + enum { kMMap_ReadOnly = 0, kMMap_WriteCopy, kMMap_ReadWrite, kMMap_End }; + for (int enmState = kMMap_ReadOnly; enmState < kMMap_End; enmState++) + { + /* + * Do the mapping. + */ + size_t cbMapping = (size_t)cbFile; + if (cbMapping != cbFile) + cbMapping = _256M; + uint8_t *pbMapping; + +# ifdef RT_OS_WINDOWS + HANDLE hSection; + pbMapping = NULL; + for (;; cbMapping /= 2) + { + hSection = CreateFileMapping((HANDLE)RTFileToNative(hFile1), NULL, + enmState == kMMap_ReadOnly ? PAGE_READONLY + : enmState == kMMap_WriteCopy ? PAGE_WRITECOPY : PAGE_READWRITE, + (uint32_t)((uint64_t)cbMapping >> 32), (uint32_t)cbMapping, NULL); + DWORD dwErr1 = GetLastError(); + DWORD dwErr2 = 0; + if (hSection != NULL) + { + pbMapping = (uint8_t *)MapViewOfFile(hSection, + enmState == kMMap_ReadOnly ? FILE_MAP_READ + : enmState == kMMap_WriteCopy ? FILE_MAP_COPY + : FILE_MAP_WRITE, + 0, 0, cbMapping); + if (pbMapping) + break; + dwErr2 = GetLastError(); + CHECK_WINAPI_CALL(CloseHandle(hSection) == TRUE); + } + if (cbMapping <= _2M) + { + RTTestIFailed("%u/%s: CreateFileMapping or MapViewOfFile failed: %u, %u", + enmState, s_apszStates[enmState], dwErr1, dwErr2); + break; + } + } +# else + for (;; cbMapping /= 2) + { + pbMapping = (uint8_t *)mmap(NULL, cbMapping, + enmState == kMMap_ReadOnly ? PROT_READ : PROT_READ | PROT_WRITE, + enmState == kMMap_WriteCopy ? MAP_PRIVATE : MAP_SHARED, + (int)RTFileToNative(hFile1), 0); + if ((void *)pbMapping != MAP_FAILED) + break; + if (cbMapping <= _2M) + { + RTTestIFailed("%u/%s: mmap failed: %s (%u)", enmState, s_apszStates[enmState], strerror(errno), errno); + break; + } + } +# endif + if (cbMapping <= _2M) + continue; + + /* + * Time page-ins just for fun. + */ + size_t const cPages = cbMapping >> PAGE_SHIFT; + size_t uDummy = 0; + uint64_t ns = RTTimeNanoTS(); + for (size_t iPage = 0; iPage < cPages; iPage++) + uDummy += ASMAtomicReadU8(&pbMapping[iPage << PAGE_SHIFT]); + ns = RTTimeNanoTS() - ns; + RTTestIValueF(ns / cPages, RTTESTUNIT_NS_PER_OCCURRENCE, "page-in %s", s_apszStates[enmState]); + + /* Check the content. */ + fsPerfCheckReadBuf(__LINE__, 0, pbMapping, cbMapping); + + if (enmState != kMMap_ReadOnly) + { + /* Write stuff to the first two megabytes. In the COW case, we'll detect + corruption of shared data during content checking of the RW iterations. */ + fsPerfFillWriteBuf(0, pbMapping, _2M, 0xf7); + if (enmState == kMMap_ReadWrite && g_fMMapCoherency) + { + /* For RW we can try read back from the file handle and check if we get + a match there first. */ + uint8_t abBuf[_4K]; + for (uint32_t off = 0; off < _2M; off += sizeof(abBuf)) + { + RTTESTI_CHECK_RC(RTFileReadAt(hFile1, off, abBuf, sizeof(abBuf), NULL), VINF_SUCCESS); + fsPerfCheckReadBuf(__LINE__, off, abBuf, sizeof(abBuf), 0xf7); + } +# ifdef RT_OS_WINDOWS + CHECK_WINAPI_CALL(FlushViewOfFile(pbMapping, _2M) == TRUE); +# else + RTTESTI_CHECK(msync(pbMapping, _2M, MS_SYNC) == 0); +# endif + } + + /* + * Time modifying and flushing a few different number of pages. + */ + if (enmState == kMMap_ReadWrite) + { + static size_t const s_acbFlush[] = { PAGE_SIZE, PAGE_SIZE * 2, PAGE_SIZE * 3, PAGE_SIZE * 8, PAGE_SIZE * 16, _2M }; + for (unsigned iFlushSize = 0 ; iFlushSize < RT_ELEMENTS(s_acbFlush); iFlushSize++) + { + size_t const cbFlush = s_acbFlush[iFlushSize]; + if (cbFlush > cbMapping) + continue; + + char szDesc[80]; + RTStrPrintf(szDesc, sizeof(szDesc), "touch/flush/%zu", cbFlush); + size_t const cFlushes = cbMapping / cbFlush; + size_t const cbMappingUsed = cFlushes * cbFlush; + size_t cbFlushed = 0; + PROFILE_FN(fsPerfMSyncWorker(pbMapping, (iIteration * cbFlush) % cbMappingUsed, cbFlush, &cbFlushed), + g_nsTestRun, szDesc); + + /* + * Check that all the changes made it thru to the file: + */ + if (!g_fIgnoreNoCache || hFileNoCache != NIL_RTFILE) + { + size_t cbBuf = RT_MIN(_2M, g_cbMaxBuffer); + uint8_t *pbBuf = (uint8_t *)RTMemPageAlloc(cbBuf); + if (!pbBuf) + { + cbBuf = _4K; + pbBuf = (uint8_t *)RTMemPageAlloc(cbBuf); + } + RTTESTI_CHECK(pbBuf != NULL); + if (pbBuf) + { + RTTESTI_CHECK_RC(RTFileSeek(hFileNoCache, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + size_t const cbToCheck = RT_MIN(cFlushes * cbFlush, cbFlushed); + unsigned cErrors = 0; + for (size_t offBuf = 0; cErrors < 32 && offBuf < cbToCheck; offBuf += cbBuf) + { + size_t cbToRead = RT_MIN(cbBuf, cbToCheck - offBuf); + RTTESTI_CHECK_RC(RTFileRead(hFileNoCache, pbBuf, cbToRead, NULL), VINF_SUCCESS); + + for (size_t offFlush = 0; offFlush < cbToRead; offFlush += PAGE_SIZE) + if (*(size_t volatile *)&pbBuf[offFlush + 8] != cbFlush) + { + RTTestIFailed("Flush issue at offset #%zx: %#zx, expected %#zx (cbFlush=%#zx, %#RX64)", + offBuf + offFlush + 8, *(size_t volatile *)&pbBuf[offFlush + 8], + cbFlush, cbFlush, *(uint64_t volatile *)&pbBuf[offFlush]); + if (++cErrors > 32) + break; + } + } + RTMemPageFree(pbBuf, cbBuf); + } + } + } + +# if 0 /* not needed, very very slow */ + /* + * Restore the file to 0xf6 state for the next test. + */ + RTTestIPrintf(RTTESTLVL_ALWAYS, "Restoring content...\n"); + fsPerfFillWriteBuf(0, pbMapping, cbMapping, 0xf6); +# ifdef RT_OS_WINDOWS + CHECK_WINAPI_CALL(FlushViewOfFile(pbMapping, cbMapping) == TRUE); +# else + RTTESTI_CHECK(msync(pbMapping, cbMapping, MS_SYNC) == 0); +# endif + RTTestIPrintf(RTTESTLVL_ALWAYS, "... done\n"); +# endif + } + } + + /* + * Observe how regular writes affects a read-only or readwrite mapping. + * These should ideally be immediately visible in the mapping, at least + * when not performed thru an no-cache handle. + */ + if ( (enmState == kMMap_ReadOnly || enmState == kMMap_ReadWrite) + && g_fMMapCoherency) + { + size_t cbBuf = RT_MIN(RT_MIN(_2M, cbMapping / 2), g_cbMaxBuffer); + uint8_t *pbBuf = (uint8_t *)RTMemPageAlloc(cbBuf); + if (!pbBuf) + { + cbBuf = _4K; + pbBuf = (uint8_t *)RTMemPageAlloc(cbBuf); + } + RTTESTI_CHECK(pbBuf != NULL); + if (pbBuf) + { + /* Do a number of random writes to the file (using hFile1). + Immediately undoing them. */ + for (uint32_t i = 0; i < 128; i++) + { + /* Generate a randomly sized write at a random location, making + sure it differs from whatever is there already before writing. */ + uint32_t const cbToWrite = RTRandU32Ex(1, (uint32_t)cbBuf); + uint64_t const offToWrite = RTRandU64Ex(0, cbMapping - cbToWrite); + + fsPerfFillWriteBuf(offToWrite, pbBuf, cbToWrite, 0xf8); + pbBuf[0] = ~pbBuf[0]; + if (cbToWrite > 1) + pbBuf[cbToWrite - 1] = ~pbBuf[cbToWrite - 1]; + RTTESTI_CHECK_RC(RTFileWriteAt(hFile1, offToWrite, pbBuf, cbToWrite, NULL), VINF_SUCCESS); + + /* Check the mapping. */ + if (memcmp(&pbMapping[(size_t)offToWrite], pbBuf, cbToWrite) != 0) + { + RTTestIFailed("Write #%u @ %#RX64 LB %#x was not reflected in the mapping!\n", i, offToWrite, cbToWrite); + } + + /* Restore */ + fsPerfFillWriteBuf(offToWrite, pbBuf, cbToWrite, 0xf6); + RTTESTI_CHECK_RC(RTFileWriteAt(hFile1, offToWrite, pbBuf, cbToWrite, NULL), VINF_SUCCESS); + } + + RTMemPageFree(pbBuf, cbBuf); + } + } + + /* + * Unmap it. + */ +# ifdef RT_OS_WINDOWS + CHECK_WINAPI_CALL(UnmapViewOfFile(pbMapping) == TRUE); + CHECK_WINAPI_CALL(CloseHandle(hSection) == TRUE); +# else + RTTESTI_CHECK(munmap(pbMapping, cbMapping) == 0); +# endif + } + + /* + * Memory mappings without open handles (pretty common). + */ + for (uint32_t i = 0; i < 32; i++) + { + /* Create a new file, 256 KB in size, and fill it with random bytes. + Try uncached access if we can to force the page-in to do actual reads. */ + char szFile2[FSPERF_MAX_PATH + 32]; + memcpy(szFile2, g_szDir, g_cchDir); + RTStrPrintf(&szFile2[g_cchDir], sizeof(szFile2) - g_cchDir, "mmap-%u.noh", i); + RTFILE hFile2 = NIL_RTFILE; + int rc = (i & 3) == 3 ? VERR_TRY_AGAIN + : RTFileOpen(&hFile2, szFile2, RTFILE_O_READWRITE | RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_NO_CACHE); + if (RT_FAILURE(rc)) + { + RTTESTI_CHECK_RC_BREAK(RTFileOpen(&hFile2, szFile2, RTFILE_O_READWRITE | RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE), + VINF_SUCCESS); + } + + static char s_abContentUnaligned[256*1024 + PAGE_SIZE - 1]; + char * const pbContent = &s_abContentUnaligned[PAGE_SIZE - ((uintptr_t)&s_abContentUnaligned[0] & PAGE_OFFSET_MASK)]; + size_t const cbContent = 256*1024; + RTRandBytes(pbContent, cbContent); + RTTESTI_CHECK_RC(rc = RTFileWrite(hFile2, pbContent, cbContent, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + /* Reopen the file with normal caching. Every second time, we also + does a read-only open of it to confuse matters. */ + RTFILE hFile3 = NIL_RTFILE; + if ((i & 3) == 3) + RTTESTI_CHECK_RC(RTFileOpen(&hFile3, szFile2, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE), VINF_SUCCESS); + hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC_BREAK(RTFileOpen(&hFile2, szFile2, RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE), + VINF_SUCCESS); + if ((i & 3) == 1) + RTTESTI_CHECK_RC(RTFileOpen(&hFile3, szFile2, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE), VINF_SUCCESS); + + /* Memory map it read-write (no COW). */ +#ifdef RT_OS_WINDOWS + HANDLE hSection = CreateFileMapping((HANDLE)RTFileToNative(hFile2), NULL, PAGE_READWRITE, 0, cbContent, NULL); + CHECK_WINAPI_CALL(hSection != NULL); + uint8_t *pbMapping = (uint8_t *)MapViewOfFile(hSection, FILE_MAP_WRITE, 0, 0, cbContent); + CHECK_WINAPI_CALL(pbMapping != NULL); + CHECK_WINAPI_CALL(CloseHandle(hSection) == TRUE); +# else + uint8_t *pbMapping = (uint8_t *)mmap(NULL, cbContent, PROT_READ | PROT_WRITE, MAP_SHARED, + (int)RTFileToNative(hFile2), 0); + if ((void *)pbMapping == MAP_FAILED) + pbMapping = NULL; + RTTESTI_CHECK_MSG(pbMapping != NULL, ("errno=%s (%d)\n", strerror(errno), errno)); +# endif + + /* Close the file handles. */ + if ((i & 7) == 7) + { + RTTESTI_CHECK_RC(RTFileClose(hFile3), VINF_SUCCESS); + hFile3 = NIL_RTFILE; + } + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + if ((i & 7) == 5) + { + RTTESTI_CHECK_RC(RTFileClose(hFile3), VINF_SUCCESS); + hFile3 = NIL_RTFILE; + } + if (pbMapping) + { + RTThreadSleep(2); /* fudge for cleanup/whatever */ + + /* Page in the mapping by comparing with the content we wrote above. */ + RTTESTI_CHECK(memcmp(pbMapping, pbContent, cbContent) == 0); + + /* Now dirty everything by inverting everything. */ + size_t *puCur = (size_t *)pbMapping; + size_t cLeft = cbContent / sizeof(*puCur); + while (cLeft-- > 0) + { + *puCur = ~*puCur; + puCur++; + } + + /* Sync it all. */ +# ifdef RT_OS_WINDOWS + //CHECK_WINAPI_CALL(FlushViewOfFile(pbMapping, cbContent) == TRUE); + SetLastError(0); + if (FlushViewOfFile(pbMapping, cbContent) != TRUE) + RTTestIFailed("line %u, i=%u: FlushViewOfFile(%p, %#zx) failed: %u / %#x", __LINE__, i, + pbMapping, cbContent, GetLastError(), RTNtLastStatusValue()); +# else + RTTESTI_CHECK(msync(pbMapping, cbContent, MS_SYNC) == 0); +# endif + + /* Unmap it. */ +# ifdef RT_OS_WINDOWS + CHECK_WINAPI_CALL(UnmapViewOfFile(pbMapping) == TRUE); +# else + RTTESTI_CHECK(munmap(pbMapping, cbContent) == 0); +# endif + } + + if (hFile3 != NIL_RTFILE) + RTTESTI_CHECK_RC(RTFileClose(hFile3), VINF_SUCCESS); + } + RTTESTI_CHECK_RC(RTFileDelete(szFile2), VINF_SUCCESS); + } + + +#else + RTTestSkipped(g_hTest, "not supported/implemented"); + RT_NOREF(hFile1, hFileNoCache, cbFile); +#endif +} + + +/** + * This does the read, write and seek tests. + */ +void fsPerfIo(void) +{ + RTTestISub("I/O"); + + /* + * Determin the size of the test file. + */ + g_szDir[g_cchDir] = '\0'; + RTFOFF cbFree = 0; + RTTESTI_CHECK_RC_RETV(RTFsQuerySizes(g_szDir, NULL, &cbFree, NULL, NULL), VINF_SUCCESS); + uint64_t cbFile = g_cbIoFile; + if (cbFile + _16M < (uint64_t)cbFree) + cbFile = RT_ALIGN_64(cbFile, _64K); + else if (cbFree < _32M) + { + RTTestSkipped(g_hTest, "Insufficent free space: %'RU64 bytes, requires >= 32MB", cbFree); + return; + } + else + { + cbFile = cbFree - (cbFree > _128M ? _64M : _16M); + cbFile = RT_ALIGN_64(cbFile, _64K); + RTTestIPrintf(RTTESTLVL_ALWAYS, "Adjusted file size to %'RU64 bytes, due to %'RU64 bytes free.\n", cbFile, cbFree); + } + if (cbFile < _64K) + { + RTTestSkipped(g_hTest, "Specified test file size too small: %'RU64 bytes, requires >= 64KB", cbFile); + return; + } + + /* + * Create a cbFile sized test file. + */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file21")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE), VINF_SUCCESS); + RTFILE hFileNoCache; + if (!g_fIgnoreNoCache) + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFileNoCache, g_szDir, + RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE | RTFILE_O_NO_CACHE), + VINF_SUCCESS); + else + { + int rc = RTFileOpen(&hFileNoCache, g_szDir, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE | RTFILE_O_NO_CACHE); + if (RT_FAILURE(rc)) + { + RTTestIPrintf(RTTESTLVL_ALWAYS, "Unable to open I/O file with non-cache flag (%Rrc), skipping related tests.\n", rc); + hFileNoCache = NIL_RTFILE; + } + } + RTFILE hFileWriteThru; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFileWriteThru, g_szDir, + RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE | RTFILE_O_WRITE_THROUGH), + VINF_SUCCESS); + + uint8_t *pbFree = NULL; + int rc = fsPerfIoPrepFile(hFile1, cbFile, &pbFree); + RTMemFree(pbFree); + if (RT_SUCCESS(rc)) + { + /* + * Do the testing & profiling. + */ + if (g_fSeek) + fsPerfIoSeek(hFile1, cbFile); + + if (g_fMMap && g_iMMapPlacement < 0) + { + fsPerfMMap(hFile1, hFileNoCache, cbFile); + fsPerfReinitFile(hFile1, cbFile); + } + + if (g_fReadTests) + fsPerfRead(hFile1, hFileNoCache, cbFile); + if (g_fReadPerf) + for (unsigned i = 0; i < g_cIoBlocks; i++) + fsPerfIoReadBlockSize(hFile1, cbFile, g_acbIoBlocks[i]); +#ifdef FSPERF_TEST_SENDFILE + if (g_fSendFile) + fsPerfSendFile(hFile1, cbFile); +#endif +#ifdef RT_OS_LINUX + if (g_fSplice) + fsPerfSpliceToPipe(hFile1, cbFile); +#endif + if (g_fMMap && g_iMMapPlacement == 0) + fsPerfMMap(hFile1, hFileNoCache, cbFile); + + /* This is destructive to the file content. */ + if (g_fWriteTests) + fsPerfWrite(hFile1, hFileNoCache, hFileWriteThru, cbFile); + if (g_fWritePerf) + for (unsigned i = 0; i < g_cIoBlocks; i++) + fsPerfIoWriteBlockSize(hFile1, cbFile, g_acbIoBlocks[i]); +#ifdef RT_OS_LINUX + if (g_fSplice) + fsPerfSpliceToFile(hFile1, cbFile); +#endif + if (g_fFSync) + fsPerfFSync(hFile1, cbFile); + + if (g_fMMap && g_iMMapPlacement > 0) + { + fsPerfReinitFile(hFile1, cbFile); + fsPerfMMap(hFile1, hFileNoCache, cbFile); + } + } + + RTTESTI_CHECK_RC(RTFileSetSize(hFile1, 0), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + if (hFileNoCache != NIL_RTFILE || !g_fIgnoreNoCache) + RTTESTI_CHECK_RC(RTFileClose(hFileNoCache), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFileWriteThru), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileDelete(g_szDir), VINF_SUCCESS); +} + + +DECL_FORCE_INLINE(int) fsPerfCopyWorker1(const char *pszSrc, const char *pszDst) +{ + RTFileDelete(pszDst); + return RTFileCopy(pszSrc, pszDst); +} + + +#ifdef RT_OS_LINUX +DECL_FORCE_INLINE(int) fsPerfCopyWorkerSendFile(RTFILE hFile1, RTFILE hFile2, size_t cbFile) +{ + RTTESTI_CHECK_RC_RET(RTFileSeek(hFile2, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS, rcCheck); + + loff_t off = 0; + ssize_t cbSent = sendfile((int)RTFileToNative(hFile2), (int)RTFileToNative(hFile1), &off, cbFile); + if (cbSent > 0 && (size_t)cbSent == cbFile) + return 0; + + int rc = VERR_GENERAL_FAILURE; + if (cbSent < 0) + { + rc = RTErrConvertFromErrno(errno); + RTTestIFailed("sendfile(file,file,NULL,%#zx) failed (%zd): %d (%Rrc)", cbFile, cbSent, errno, rc); + } + else + RTTestIFailed("sendfile(file,file,NULL,%#zx) returned %#zx, expected %#zx (diff %zd)", + cbFile, cbSent, cbFile, cbSent - cbFile); + return rc; +} +#endif /* RT_OS_LINUX */ + + +static void fsPerfCopy(void) +{ + RTTestISub("copy"); + + /* + * Non-existing files. + */ + RTTESTI_CHECK_RC(RTFileCopy(InEmptyDir(RT_STR_TUPLE("no-such-file")), + InDir2(RT_STR_TUPLE("whatever"))), VERR_FILE_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileCopy(InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file")), + InDir2(RT_STR_TUPLE("no-such-file"))), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileCopy(InDir(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file")), + InDir2(RT_STR_TUPLE("whatever"))), VERR_PATH_NOT_FOUND); + + RTTESTI_CHECK_RC(RTFileCopy(InDir(RT_STR_TUPLE("known-file")), + InEmptyDir(RT_STR_TUPLE("no-such-dir" RTPATH_SLASH_STR "no-such-file"))), FSPERF_VERR_PATH_NOT_FOUND); + RTTESTI_CHECK_RC(RTFileCopy(InDir(RT_STR_TUPLE("known-file")), + InDir2(RT_STR_TUPLE("known-file" RTPATH_SLASH_STR "no-such-file"))), VERR_PATH_NOT_FOUND); + + /* + * Determin the size of the test file. + * We want to be able to make 1 copy of it. + */ + g_szDir[g_cchDir] = '\0'; + RTFOFF cbFree = 0; + RTTESTI_CHECK_RC_RETV(RTFsQuerySizes(g_szDir, NULL, &cbFree, NULL, NULL), VINF_SUCCESS); + uint64_t cbFile = g_cbIoFile; + if (cbFile + _16M < (uint64_t)cbFree) + cbFile = RT_ALIGN_64(cbFile, _64K); + else if (cbFree < _32M) + { + RTTestSkipped(g_hTest, "Insufficent free space: %'RU64 bytes, requires >= 32MB", cbFree); + return; + } + else + { + cbFile = cbFree - (cbFree > _128M ? _64M : _16M); + cbFile = RT_ALIGN_64(cbFile, _64K); + RTTestIPrintf(RTTESTLVL_ALWAYS, "Adjusted file size to %'RU64 bytes, due to %'RU64 bytes free.\n", cbFile, cbFree); + } + if (cbFile < _512K * 2) + { + RTTestSkipped(g_hTest, "Specified test file size too small: %'RU64 bytes, requires >= 1MB", cbFile); + return; + } + cbFile /= 2; + + /* + * Create a cbFile sized test file. + */ + RTFILE hFile1; + RTTESTI_CHECK_RC_RETV(RTFileOpen(&hFile1, InDir(RT_STR_TUPLE("file22")), + RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE), VINF_SUCCESS); + uint8_t *pbFree = NULL; + int rc = fsPerfIoPrepFile(hFile1, cbFile, &pbFree); + RTMemFree(pbFree); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + /* + * Make copies. + */ + /* plain */ + RTFileDelete(InDir2(RT_STR_TUPLE("file23"))); + RTTESTI_CHECK_RC(RTFileCopy(g_szDir, g_szDir2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCopy(g_szDir, g_szDir2), VERR_ALREADY_EXISTS); + RTTESTI_CHECK_RC(RTFileCompare(g_szDir, g_szDir2), VINF_SUCCESS); + + /* by handle */ + hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile1, g_szDir, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS); + RTFILE hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile2, g_szDir2, RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCopyByHandles(hFile1, hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCompare(g_szDir, g_szDir2), VINF_SUCCESS); + + /* copy part */ + hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile1, g_szDir, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS); + hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile2, g_szDir2, RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCopyPart(hFile1, 0, hFile2, 0, cbFile / 2, 0, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCopyPart(hFile1, cbFile / 2, hFile2, cbFile / 2, cbFile - cbFile / 2, 0, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCompare(g_szDir, g_szDir2), VINF_SUCCESS); + +#ifdef RT_OS_LINUX + /* + * On linux we can also use sendfile between two files, except for 2.5.x to 2.6.33. + */ + uint64_t const cbFileMax = RT_MIN(cbFile, UINT32_C(0x7ffff000)); + char szRelease[64]; + RTSystemQueryOSInfo(RTSYSOSINFO_RELEASE, szRelease, sizeof(szRelease)); + bool const fSendFileBetweenFiles = RTStrVersionCompare(szRelease, "2.5.0") < 0 + || RTStrVersionCompare(szRelease, "2.6.33") >= 0; + if (fSendFileBetweenFiles) + { + /* Copy the whole file: */ + hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile1, g_szDir, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS); + RTFileDelete(g_szDir2); + hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile2, g_szDir2, RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + ssize_t cbSent = sendfile((int)RTFileToNative(hFile2), (int)RTFileToNative(hFile1), NULL, cbFile); + if (cbSent < 0) + RTTestIFailed("sendfile(file,file,NULL,%#zx) failed (%zd): %d (%Rrc)", + cbFile, cbSent, errno, RTErrConvertFromErrno(errno)); + else if ((size_t)cbSent != cbFileMax) + RTTestIFailed("sendfile(file,file,NULL,%#zx) returned %#zx, expected %#zx (diff %zd)", + cbFile, cbSent, cbFileMax, cbSent - cbFileMax); + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCompare(g_szDir, g_szDir2), VINF_SUCCESS); + + /* Try copy a little bit too much: */ + if (cbFile == cbFileMax) + { + hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile1, g_szDir, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS); + RTFileDelete(g_szDir2); + hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile2, g_szDir2, RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + size_t cbToCopy = cbFile + RTRandU32Ex(1, _64M); + cbSent = sendfile((int)RTFileToNative(hFile2), (int)RTFileToNative(hFile1), NULL, cbToCopy); + if (cbSent < 0) + RTTestIFailed("sendfile(file,file,NULL,%#zx) failed (%zd): %d (%Rrc)", + cbToCopy, cbSent, errno, RTErrConvertFromErrno(errno)); + else if ((size_t)cbSent != cbFile) + RTTestIFailed("sendfile(file,file,NULL,%#zx) returned %#zx, expected %#zx (diff %zd)", + cbToCopy, cbSent, cbFile, cbSent - cbFile); + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCompare(g_szDir, g_szDir2), VINF_SUCCESS); + } + + /* Do partial copy: */ + hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile2, g_szDir2, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + for (uint32_t i = 0; i < 64; i++) + { + size_t cbToCopy = RTRandU32Ex(0, cbFileMax - 1); + uint32_t const offFile = RTRandU32Ex(1, (uint64_t)RT_MIN(cbFileMax - cbToCopy, UINT32_MAX)); + RTTESTI_CHECK_RC_BREAK(RTFileSeek(hFile2, offFile, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + loff_t offFile2 = offFile; + cbSent = sendfile((int)RTFileToNative(hFile2), (int)RTFileToNative(hFile1), &offFile2, cbToCopy); + if (cbSent < 0) + RTTestIFailed("sendfile(file,file,%#x,%#zx) failed (%zd): %d (%Rrc)", + offFile, cbToCopy, cbSent, errno, RTErrConvertFromErrno(errno)); + else if ((size_t)cbSent != cbToCopy) + RTTestIFailed("sendfile(file,file,%#x,%#zx) returned %#zx, expected %#zx (diff %zd)", + offFile, cbToCopy, cbSent, cbToCopy, cbSent - cbToCopy); + else if (offFile2 != (loff_t)(offFile + cbToCopy)) + RTTestIFailed("sendfile(file,file,%#x,%#zx) returned %#zx + off=%#RX64, expected off %#x", + offFile, cbToCopy, cbSent, offFile2, offFile + cbToCopy); + } + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileCompare(g_szDir, g_szDir2), VINF_SUCCESS); + } +#endif + + /* + * Do some benchmarking. + */ +#define PROFILE_COPY_FN(a_szOperation, a_fnCall) \ + do \ + { \ + /* Estimate how many iterations we need to fill up the given timeslot: */ \ + fsPerfYield(); \ + uint64_t nsStart = RTTimeNanoTS(); \ + uint64_t ns; \ + do \ + ns = RTTimeNanoTS(); \ + while (ns == nsStart); \ + nsStart = ns; \ + \ + uint64_t iIteration = 0; \ + do \ + { \ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + iIteration++; \ + ns = RTTimeNanoTS() - nsStart; \ + } while (ns < RT_NS_10MS); \ + ns /= iIteration; \ + if (ns > g_nsPerNanoTSCall + 32) \ + ns -= g_nsPerNanoTSCall; \ + uint64_t cIterations = g_nsTestRun / ns; \ + if (cIterations < 2) \ + cIterations = 2; \ + else if (cIterations & 1) \ + cIterations++; \ + \ + /* Do the actual profiling: */ \ + iIteration = 0; \ + fsPerfYield(); \ + nsStart = RTTimeNanoTS(); \ + for (uint32_t iAdjust = 0; iAdjust < 4; iAdjust++) \ + { \ + for (; iIteration < cIterations; iIteration++)\ + RTTESTI_CHECK_RC(a_fnCall, VINF_SUCCESS); \ + ns = RTTimeNanoTS() - nsStart;\ + if (ns >= g_nsTestRun - (g_nsTestRun / 10)) \ + break; \ + cIterations += cIterations / 4; \ + if (cIterations & 1) \ + cIterations++; \ + nsStart += g_nsPerNanoTSCall; \ + } \ + RTTestIValueF(ns / iIteration, \ + RTTESTUNIT_NS_PER_OCCURRENCE, a_szOperation " latency"); \ + RTTestIValueF((uint64_t)((double)(iIteration * cbFile) / ((double)ns / RT_NS_1SEC)), \ + RTTESTUNIT_BYTES_PER_SEC, a_szOperation " throughput"); \ + RTTestIValueF((uint64_t)iIteration * cbFile, \ + RTTESTUNIT_BYTES, a_szOperation " bytes"); \ + RTTestIValueF(iIteration, \ + RTTESTUNIT_OCCURRENCES, a_szOperation " iterations"); \ + if (g_fShowDuration) \ + RTTestIValueF(ns, RTTESTUNIT_NS, a_szOperation " duration"); \ + } while (0) + + PROFILE_COPY_FN("RTFileCopy/Replace", fsPerfCopyWorker1(g_szDir, g_szDir2)); + + hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile1, g_szDir, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS); + RTFileDelete(g_szDir2); + hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile2, g_szDir2, RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + PROFILE_COPY_FN("RTFileCopyByHandles/Overwrite", RTFileCopyByHandles(hFile1, hFile2)); + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + /* We could benchmark RTFileCopyPart with various block sizes and whatnot... + But it's currently well covered by the two previous operations. */ + +#ifdef RT_OS_LINUX + if (fSendFileBetweenFiles) + { + hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile1, g_szDir, RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_READ), VINF_SUCCESS); + RTFileDelete(g_szDir2); + hFile2 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpen(&hFile2, g_szDir2, RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_WRITE), VINF_SUCCESS); + PROFILE_COPY_FN("sendfile/overwrite", fsPerfCopyWorkerSendFile(hFile1, hFile2, cbFileMax)); + RTTESTI_CHECK_RC(RTFileClose(hFile2), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + } +#endif + } + + /* + * Clean up. + */ + RTFileDelete(InDir2(RT_STR_TUPLE("file22c1"))); + RTFileDelete(InDir2(RT_STR_TUPLE("file22c2"))); + RTFileDelete(InDir2(RT_STR_TUPLE("file22c3"))); + RTTESTI_CHECK_RC(RTFileDelete(g_szDir), VINF_SUCCESS); +} + + +static void fsPerfRemote(void) +{ + RTTestISub("remote"); + uint8_t abBuf[16384]; + + + /* + * Create a file on the remote end and check that we can immediately see it. + */ + RTTESTI_CHECK_RC_RETV(FsPerfCommsSend("reset\n" + "open 0 'file30' 'w' 'ca'\n" + "writepattern 0 0 0 4096" FSPERF_EOF_STR), VINF_SUCCESS); + + RTFILEACTION enmActuallyTaken = RTFILEACTION_END; + RTFILE hFile0 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpenEx(InDir(RT_STR_TUPLE("file30")), RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE, + &hFile0, &enmActuallyTaken), VINF_SUCCESS); + RTTESTI_CHECK(enmActuallyTaken == RTFILEACTION_OPENED); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 4096, NULL), VINF_SUCCESS); + AssertCompile(RT_ELEMENTS(g_abPattern0) == 1); + RTTESTI_CHECK(ASMMemIsAllU8(abBuf, 4096, g_abPattern0[0])); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1, NULL), VERR_EOF); + + /* + * Append a little to it on the host and see that we can read it. + */ + RTTESTI_CHECK_RC(FsPerfCommsSend("writepattern 0 4096 1 1024" FSPERF_EOF_STR), VINF_SUCCESS); + AssertCompile(RT_ELEMENTS(g_abPattern1) == 1); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1024, NULL), VINF_SUCCESS); + RTTESTI_CHECK(ASMMemIsAllU8(abBuf, 1024, g_abPattern1[0])); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1, NULL), VERR_EOF); + + /* + * Have the host truncate the file. + */ + RTTESTI_CHECK_RC(FsPerfCommsSend("truncate 0 1024" FSPERF_EOF_STR), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1, NULL), VERR_EOF); + RTTESTI_CHECK_RC(RTFileSeek(hFile0, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1024, NULL), VINF_SUCCESS); + AssertCompile(RT_ELEMENTS(g_abPattern0) == 1); + RTTESTI_CHECK(ASMMemIsAllU8(abBuf, 4096, g_abPattern0[0])); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1, NULL), VERR_EOF); + + /* + * Write a bunch of stuff to the file here, then truncate it to a given size, + * then have the host add more, finally test that we can successfully chop off + * what the host added by reissuing the same truncate call as before (issue of + * RDBSS using cached size to noop out set-eof-to-same-size). + */ + memset(abBuf, 0xe9, sizeof(abBuf)); + RTTESTI_CHECK_RC(RTFileSeek(hFile0, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileWrite(hFile0, abBuf, 16384, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 8000), VINF_SUCCESS); + RTTESTI_CHECK_RC(FsPerfCommsSend("writepattern 0 8000 0 1000" FSPERF_EOF_STR), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 8000), VINF_SUCCESS); + uint64_t cbFile = 0; + RTTESTI_CHECK_RC(RTFileQuerySize(hFile0, &cbFile), VINF_SUCCESS); + RTTESTI_CHECK_MSG(cbFile == 8000, ("cbFile=%u\n", cbFile)); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1, NULL), VERR_EOF); + + /* Same, but using RTFileRead to find out and RTFileWrite to define the size. */ + RTTESTI_CHECK_RC(RTFileSeek(hFile0, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 0), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileWrite(hFile0, abBuf, 5000, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(FsPerfCommsSend("writepattern 0 5000 0 1000" FSPERF_EOF_STR), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 5000), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1, NULL), VERR_EOF); + RTTESTI_CHECK_RC(RTFileQuerySize(hFile0, &cbFile), VINF_SUCCESS); + RTTESTI_CHECK_MSG(cbFile == 5000, ("cbFile=%u\n", cbFile)); + + /* Same, but host truncates rather than adding stuff. */ + RTTESTI_CHECK_RC(RTFileSeek(hFile0, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileWrite(hFile0, abBuf, 16384, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 10000), VINF_SUCCESS); + RTTESTI_CHECK_RC(FsPerfCommsSend("truncate 0 4000" FSPERF_EOF_STR), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileQuerySize(hFile0, &cbFile), VINF_SUCCESS); + RTTESTI_CHECK_MSG(cbFile == 4000, ("cbFile=%u\n", cbFile)); + RTTESTI_CHECK_RC(RTFileRead(hFile0, abBuf, 1, NULL), VERR_EOF); + + /* + * Test noticing remote size changes when opening a file. Need to keep hFile0 + * open here so we're sure to have an inode/FCB for the file in question. + */ + memset(abBuf, 0xe7, sizeof(abBuf)); + RTTESTI_CHECK_RC(RTFileSeek(hFile0, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 0), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileWrite(hFile0, abBuf, 12288, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 12288), VINF_SUCCESS); + + RTTESTI_CHECK_RC(FsPerfCommsSend("writepattern 0 12288 2 4096" FSPERF_EOF_STR), VINF_SUCCESS); + + enmActuallyTaken = RTFILEACTION_END; + RTFILE hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpenEx(InDir(RT_STR_TUPLE("file30")), RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE, + &hFile1, &enmActuallyTaken), VINF_SUCCESS); + RTTESTI_CHECK(enmActuallyTaken == RTFILEACTION_OPENED); + AssertCompile(sizeof(abBuf) >= 16384); + RTTESTI_CHECK_RC(RTFileRead(hFile1, abBuf, 16384, NULL), VINF_SUCCESS); + RTTESTI_CHECK(ASMMemIsAllU8(abBuf, 12288, 0xe7)); + AssertCompile(RT_ELEMENTS(g_abPattern2) == 1); + RTTESTI_CHECK(ASMMemIsAllU8(&abBuf[12288], 4096, g_abPattern2[0])); + RTTESTI_CHECK_RC(RTFileRead(hFile1, abBuf, 1, NULL), VERR_EOF); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + /* Same, but remote end truncates the file: */ + memset(abBuf, 0xe6, sizeof(abBuf)); + RTTESTI_CHECK_RC(RTFileSeek(hFile0, 0, RTFILE_SEEK_BEGIN, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 0), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileWrite(hFile0, abBuf, 12288, NULL), VINF_SUCCESS); + RTTESTI_CHECK_RC(RTFileSetSize(hFile0, 12288), VINF_SUCCESS); + + RTTESTI_CHECK_RC(FsPerfCommsSend("truncate 0 7500" FSPERF_EOF_STR), VINF_SUCCESS); + + enmActuallyTaken = RTFILEACTION_END; + hFile1 = NIL_RTFILE; + RTTESTI_CHECK_RC(RTFileOpenEx(InDir(RT_STR_TUPLE("file30")), RTFILE_O_READWRITE | RTFILE_O_OPEN | RTFILE_O_DENY_NONE, + &hFile1, &enmActuallyTaken), VINF_SUCCESS); + RTTESTI_CHECK(enmActuallyTaken == RTFILEACTION_OPENED); + RTTESTI_CHECK_RC(RTFileRead(hFile1, abBuf, 7500, NULL), VINF_SUCCESS); + RTTESTI_CHECK(ASMMemIsAllU8(abBuf, 7500, 0xe6)); + RTTESTI_CHECK_RC(RTFileRead(hFile1, abBuf, 1, NULL), VERR_EOF); + RTTESTI_CHECK_RC(RTFileClose(hFile1), VINF_SUCCESS); + + RTTESTI_CHECK_RC(RTFileClose(hFile0), VINF_SUCCESS); +} + + + +/** + * Display the usage to @a pStrm. + */ +static void Usage(PRTSTREAM pStrm) +{ + char szExec[FSPERF_MAX_PATH]; + RTStrmPrintf(pStrm, "usage: %s <-d <testdir>> [options]\n", + RTPathFilename(RTProcGetExecutablePath(szExec, sizeof(szExec)))); + RTStrmPrintf(pStrm, "\n"); + RTStrmPrintf(pStrm, "options: \n"); + + for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) + { + char szHelp[80]; + const char *pszHelp; + switch (g_aCmdOptions[i].iShort) + { + case 'd': pszHelp = "The directory to use for testing. default: CWD/fstestdir"; break; + case 'r': pszHelp = "Don't abspath test dir (good for deep dirs). default: disabled"; break; + case 'e': pszHelp = "Enables all tests. default: -e"; break; + case 'z': pszHelp = "Disables all tests. default: -e"; break; + case 's': pszHelp = "Set benchmark duration in seconds. default: 10 sec"; break; + case 'm': pszHelp = "Set benchmark duration in milliseconds. default: 10000 ms"; break; + case 'v': pszHelp = "More verbose execution."; break; + case 'q': pszHelp = "Quiet execution."; break; + case 'h': pszHelp = "Displays this help and exit"; break; + case 'V': pszHelp = "Displays the program revision"; break; + case kCmdOpt_ShowDuration: pszHelp = "Show duration of profile runs. default: --no-show-duration"; break; + case kCmdOpt_NoShowDuration: pszHelp = "Hide duration of profile runs. default: --no-show-duration"; break; + case kCmdOpt_ShowIterations: pszHelp = "Show iteration count for profile runs. default: --no-show-iterations"; break; + case kCmdOpt_NoShowIterations: pszHelp = "Hide iteration count for profile runs. default: --no-show-iterations"; break; + case kCmdOpt_ManyFiles: pszHelp = "Count of files in big test dir. default: --many-files 10000"; break; + case kCmdOpt_NoManyFiles: pszHelp = "Skip big test dir with many files. default: --many-files 10000"; break; + case kCmdOpt_ManyTreeFilesPerDir: pszHelp = "Count of files per directory in test tree. default: 640"; break; + case kCmdOpt_ManyTreeSubdirsPerDir: pszHelp = "Count of subdirs per directory in test tree. default: 16"; break; + case kCmdOpt_ManyTreeDepth: pszHelp = "Depth of test tree (not counting root). default: 1"; break; +#if defined(RT_OS_WINDOWS) + case kCmdOpt_MaxBufferSize: pszHelp = "For avoiding the MDL limit on windows. default: 32MiB"; break; +#else + case kCmdOpt_MaxBufferSize: pszHelp = "For avoiding the MDL limit on windows. default: 0"; break; +#endif + case kCmdOpt_MMapPlacement: pszHelp = "When to do mmap testing (caching effects): first, between (default), last "; break; + case kCmdOpt_IgnoreNoCache: pszHelp = "Ignore error wrt no-cache handle. default: --no-ignore-no-cache"; break; + case kCmdOpt_NoIgnoreNoCache: pszHelp = "Do not ignore error wrt no-cache handle. default: --no-ignore-no-cache"; break; + case kCmdOpt_IoFileSize: pszHelp = "Size of file used for I/O tests. default: 512 MB"; break; + case kCmdOpt_SetBlockSize: pszHelp = "Sets single I/O block size (in bytes)."; break; + case kCmdOpt_AddBlockSize: pszHelp = "Adds an I/O block size (in bytes)."; break; + default: + if (g_aCmdOptions[i].iShort >= kCmdOpt_First) + { + if (RTStrStartsWith(g_aCmdOptions[i].pszLong, "--no-")) + RTStrPrintf(szHelp, sizeof(szHelp), "Disables the '%s' test.", g_aCmdOptions[i].pszLong + 5); + else + RTStrPrintf(szHelp, sizeof(szHelp), "Enables the '%s' test.", g_aCmdOptions[i].pszLong + 2); + pszHelp = szHelp; + } + else + pszHelp = "Option undocumented"; + break; + } + if ((unsigned)g_aCmdOptions[i].iShort < 127U) + { + char szOpt[64]; + RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); + RTStrmPrintf(pStrm, " %-19s %s\n", szOpt, pszHelp); + } + else + RTStrmPrintf(pStrm, " %-19s %s\n", g_aCmdOptions[i].pszLong, pszHelp); + } +} + + +static uint32_t fsPerfCalcManyTreeFiles(void) +{ + uint32_t cDirs = 1; + for (uint32_t i = 0, cDirsAtLevel = 1; i < g_cManyTreeDepth; i++) + { + cDirs += cDirsAtLevel * g_cManyTreeSubdirsPerDir; + cDirsAtLevel *= g_cManyTreeSubdirsPerDir; + } + return g_cManyTreeFilesPerDir * cDirs; +} + + +int main(int argc, char *argv[]) +{ + /* + * Init IPRT and globals. + */ + int rc = RTTestInitAndCreate("FsPerf", &g_hTest); + if (rc) + return rc; + RTListInit(&g_ManyTreeHead); + + /* + * Default values. + */ + char szDefaultDir[RTPATH_MAX]; + const char *pszDir = szDefaultDir; + + /* As default retrieve the system's temporary directory and create a test directory beneath it, + * as this binary might get executed from a read-only medium such as ${CDROM}. */ + rc = RTPathTemp(szDefaultDir, sizeof(szDefaultDir)); + if (RT_SUCCESS(rc)) + { + char szDirName[32]; + RTStrPrintf2(szDirName, sizeof(szDirName), "fstestdir-%u" RTPATH_SLASH_STR, RTProcSelf()); + rc = RTPathAppend(szDefaultDir, sizeof(szDefaultDir), szDirName); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "Unable to append dir name in temp dir, rc=%Rrc\n", rc); + return RTTestSummaryAndDestroy(g_hTest); + } + } + else + { + RTTestFailed(g_hTest, "Unable to retrieve temp dir, rc=%Rrc\n", rc); + return RTTestSummaryAndDestroy(g_hTest); + } + + RTTestIPrintf(RTTESTLVL_INFO, "Default directory is: %s\n", szDefaultDir); + + bool fCommsSlave = false; + + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); + while ((rc = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + switch (rc) + { + case 'c': + if (!g_fRelativeDir) + rc = RTPathAbs(ValueUnion.psz, g_szCommsDir, sizeof(g_szCommsDir) - 128); + else + rc = RTStrCopy(g_szCommsDir, sizeof(g_szCommsDir) - 128, ValueUnion.psz); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "%s(%s) failed: %Rrc\n", g_fRelativeDir ? "RTStrCopy" : "RTAbsPath", pszDir, rc); + return RTTestSummaryAndDestroy(g_hTest); + } + RTPathEnsureTrailingSeparator(g_szCommsDir, sizeof(g_szCommsDir)); + g_cchCommsDir = strlen(g_szCommsDir); + + rc = RTPathJoin(g_szCommsSubDir, sizeof(g_szCommsSubDir) - 128, g_szCommsDir, "comms" RTPATH_SLASH_STR); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "RTPathJoin(%s,,'comms/') failed: %Rrc\n", g_szCommsDir, rc); + return RTTestSummaryAndDestroy(g_hTest); + } + g_cchCommsSubDir = strlen(g_szCommsSubDir); + break; + + case 'C': + fCommsSlave = true; + break; + + case 'd': + pszDir = ValueUnion.psz; + break; + + case 'r': + g_fRelativeDir = true; + break; + + case 's': + if (ValueUnion.u32 == 0) + g_nsTestRun = RT_NS_1SEC_64 * 10; + else + g_nsTestRun = ValueUnion.u32 * RT_NS_1SEC_64; + break; + + case 'm': + if (ValueUnion.u64 == 0) + g_nsTestRun = RT_NS_1SEC_64 * 10; + else + g_nsTestRun = ValueUnion.u64 * RT_NS_1MS; + break; + + case 'e': + g_fManyFiles = true; + g_fOpen = true; + g_fFStat = true; +#ifdef RT_OS_WINDOWS + g_fNtQueryInfoFile = true; + g_fNtQueryVolInfoFile = true; +#endif + g_fFChMod = true; + g_fFUtimes = true; + g_fStat = true; + g_fChMod = true; + g_fUtimes = true; + g_fRename = true; + g_fDirOpen = true; + g_fDirEnum = true; + g_fMkRmDir = true; + g_fStatVfs = true; + g_fRm = true; + g_fChSize = true; + g_fReadTests = true; + g_fReadPerf = true; +#ifdef FSPERF_TEST_SENDFILE + g_fSendFile = true; +#endif +#ifdef RT_OS_LINUX + g_fSplice = true; +#endif + g_fWriteTests = true; + g_fWritePerf = true; + g_fSeek = true; + g_fFSync = true; + g_fMMap = true; + g_fMMapCoherency = true; + g_fCopy = true; + g_fRemote = true; + break; + + case 'z': + g_fManyFiles = false; + g_fOpen = false; + g_fFStat = false; +#ifdef RT_OS_WINDOWS + g_fNtQueryInfoFile = false; + g_fNtQueryVolInfoFile = false; +#endif + g_fFChMod = false; + g_fFUtimes = false; + g_fStat = false; + g_fChMod = false; + g_fUtimes = false; + g_fRename = false; + g_fDirOpen = false; + g_fDirEnum = false; + g_fMkRmDir = false; + g_fStatVfs = false; + g_fRm = false; + g_fChSize = false; + g_fReadTests = false; + g_fReadPerf = false; +#ifdef FSPERF_TEST_SENDFILE + g_fSendFile = false; +#endif +#ifdef RT_OS_LINUX + g_fSplice = false; +#endif + g_fWriteTests = false; + g_fWritePerf = false; + g_fSeek = false; + g_fFSync = false; + g_fMMap = false; + g_fMMapCoherency = false; + g_fCopy = false; + g_fRemote = false; + break; + +#define CASE_OPT(a_Stem) \ + case RT_CONCAT(kCmdOpt_,a_Stem): RT_CONCAT(g_f,a_Stem) = true; break; \ + case RT_CONCAT(kCmdOpt_No,a_Stem): RT_CONCAT(g_f,a_Stem) = false; break + CASE_OPT(Open); + CASE_OPT(FStat); +#ifdef RT_OS_WINDOWS + CASE_OPT(NtQueryInfoFile); + CASE_OPT(NtQueryVolInfoFile); +#endif + CASE_OPT(FChMod); + CASE_OPT(FUtimes); + CASE_OPT(Stat); + CASE_OPT(ChMod); + CASE_OPT(Utimes); + CASE_OPT(Rename); + CASE_OPT(DirOpen); + CASE_OPT(DirEnum); + CASE_OPT(MkRmDir); + CASE_OPT(StatVfs); + CASE_OPT(Rm); + CASE_OPT(ChSize); + CASE_OPT(ReadTests); + CASE_OPT(ReadPerf); +#ifdef FSPERF_TEST_SENDFILE + CASE_OPT(SendFile); +#endif +#ifdef RT_OS_LINUX + CASE_OPT(Splice); +#endif + CASE_OPT(WriteTests); + CASE_OPT(WritePerf); + CASE_OPT(Seek); + CASE_OPT(FSync); + CASE_OPT(MMap); + CASE_OPT(MMapCoherency); + CASE_OPT(IgnoreNoCache); + CASE_OPT(Copy); + CASE_OPT(Remote); + + CASE_OPT(ShowDuration); + CASE_OPT(ShowIterations); +#undef CASE_OPT + + case kCmdOpt_ManyFiles: + g_fManyFiles = ValueUnion.u32 > 0; + g_cManyFiles = ValueUnion.u32; + break; + + case kCmdOpt_NoManyFiles: + g_fManyFiles = false; + break; + + case kCmdOpt_ManyTreeFilesPerDir: + if (ValueUnion.u32 > 0 && ValueUnion.u32 <= _64M) + { + g_cManyTreeFilesPerDir = ValueUnion.u32; + g_cManyTreeFiles = fsPerfCalcManyTreeFiles(); + break; + } + RTTestFailed(g_hTest, "Out of range --files-per-dir value: %u (%#x)\n", ValueUnion.u32, ValueUnion.u32); + return RTTestSummaryAndDestroy(g_hTest); + + case kCmdOpt_ManyTreeSubdirsPerDir: + if (ValueUnion.u32 > 0 && ValueUnion.u32 <= 1024) + { + g_cManyTreeSubdirsPerDir = ValueUnion.u32; + g_cManyTreeFiles = fsPerfCalcManyTreeFiles(); + break; + } + RTTestFailed(g_hTest, "Out of range --subdirs-per-dir value: %u (%#x)\n", ValueUnion.u32, ValueUnion.u32); + return RTTestSummaryAndDestroy(g_hTest); + + case kCmdOpt_ManyTreeDepth: + if (ValueUnion.u32 <= 8) + { + g_cManyTreeDepth = ValueUnion.u32; + g_cManyTreeFiles = fsPerfCalcManyTreeFiles(); + break; + } + RTTestFailed(g_hTest, "Out of range --tree-depth value: %u (%#x)\n", ValueUnion.u32, ValueUnion.u32); + return RTTestSummaryAndDestroy(g_hTest); + + case kCmdOpt_MaxBufferSize: + if (ValueUnion.u32 >= 4096) + g_cbMaxBuffer = ValueUnion.u32; + else if (ValueUnion.u32 == 0) + g_cbMaxBuffer = UINT32_MAX; + else + { + RTTestFailed(g_hTest, "max buffer size is less than 4KB: %#x\n", ValueUnion.u32); + return RTTestSummaryAndDestroy(g_hTest); + } + break; + + case kCmdOpt_IoFileSize: + if (ValueUnion.u64 == 0) + g_cbIoFile = _512M; + else + g_cbIoFile = ValueUnion.u64; + break; + + case kCmdOpt_SetBlockSize: + if (ValueUnion.u32 > 0) + { + g_cIoBlocks = 1; + g_acbIoBlocks[0] = ValueUnion.u32; + } + else + { + RTTestFailed(g_hTest, "Invalid I/O block size: %u (%#x)\n", ValueUnion.u32, ValueUnion.u32); + return RTTestSummaryAndDestroy(g_hTest); + } + break; + + case kCmdOpt_AddBlockSize: + if (g_cIoBlocks >= RT_ELEMENTS(g_acbIoBlocks)) + RTTestFailed(g_hTest, "Too many I/O block sizes: max %u\n", RT_ELEMENTS(g_acbIoBlocks)); + else if (ValueUnion.u32 == 0) + RTTestFailed(g_hTest, "Invalid I/O block size: %u (%#x)\n", ValueUnion.u32, ValueUnion.u32); + else + { + g_acbIoBlocks[g_cIoBlocks++] = ValueUnion.u32; + break; + } + return RTTestSummaryAndDestroy(g_hTest); + + case kCmdOpt_MMapPlacement: + if (strcmp(ValueUnion.psz, "first") == 0) + g_iMMapPlacement = -1; + else if ( strcmp(ValueUnion.psz, "between") == 0 + || strcmp(ValueUnion.psz, "default") == 0) + g_iMMapPlacement = 0; + else if (strcmp(ValueUnion.psz, "last") == 0) + g_iMMapPlacement = 1; + else + { + RTTestFailed(g_hTest, + "Invalid --mmap-placment directive '%s'! Expected 'first', 'last', 'between' or 'default'.\n", + ValueUnion.psz); + return RTTestSummaryAndDestroy(g_hTest); + } + break; + + case 'q': + g_uVerbosity = 0; + break; + + case 'v': + g_uVerbosity++; + break; + + case 'h': + Usage(g_pStdOut); + return RTEXITCODE_SUCCESS; + + case 'V': + { + char szRev[] = "$Revision: 155244 $"; + szRev[RT_ELEMENTS(szRev) - 2] = '\0'; + RTPrintf(RTStrStrip(strchr(szRev, ':') + 1)); + return RTEXITCODE_SUCCESS; + } + + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + /* + * Populate g_szDir. + */ + if (!g_fRelativeDir) + rc = RTPathAbs(pszDir, g_szDir, sizeof(g_szDir) - FSPERF_MAX_NEEDED_PATH); + else + rc = RTStrCopy(g_szDir, sizeof(g_szDir) - FSPERF_MAX_NEEDED_PATH, pszDir); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "%s(%s) failed: %Rrc\n", g_fRelativeDir ? "RTStrCopy" : "RTAbsPath", pszDir, rc); + return RTTestSummaryAndDestroy(g_hTest); + } + RTPathEnsureTrailingSeparator(g_szDir, sizeof(g_szDir)); + g_cchDir = strlen(g_szDir); + + /* + * If communication slave, go do that and be done. + */ + if (fCommsSlave) + { + if (pszDir == szDefaultDir) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "The slave must have a working directory specified (-d)!"); + return FsPerfCommsSlave(); + } + + /* + * Create the test directory with an 'empty' subdirectory under it, + * execute the tests, and remove directory when done. + */ + RTTestBanner(g_hTest); + if (!RTPathExists(g_szDir)) + { + /* The base dir: */ + rc = RTDirCreate(g_szDir, 0755, + RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_DONT_SET | RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_NOT_CRITICAL); + if (RT_SUCCESS(rc)) + { + RTTestIPrintf(RTTESTLVL_ALWAYS, "Test dir: %s\n", g_szDir); + rc = fsPrepTestArea(); + if (RT_SUCCESS(rc)) + { + /* Profile RTTimeNanoTS(). */ + fsPerfNanoTS(); + + /* Do tests: */ + if (g_fManyFiles) + fsPerfManyFiles(); + if (g_fOpen) + fsPerfOpen(); + if (g_fFStat) + fsPerfFStat(); +#ifdef RT_OS_WINDOWS + if (g_fNtQueryInfoFile) + fsPerfNtQueryInfoFile(); + if (g_fNtQueryVolInfoFile) + fsPerfNtQueryVolInfoFile(); +#endif + if (g_fFChMod) + fsPerfFChMod(); + if (g_fFUtimes) + fsPerfFUtimes(); + if (g_fStat) + fsPerfStat(); + if (g_fChMod) + fsPerfChmod(); + if (g_fUtimes) + fsPerfUtimes(); + if (g_fRename) + fsPerfRename(); + if (g_fDirOpen) + vsPerfDirOpen(); + if (g_fDirEnum) + vsPerfDirEnum(); + if (g_fMkRmDir) + fsPerfMkRmDir(); + if (g_fStatVfs) + fsPerfStatVfs(); + if (g_fRm || g_fManyFiles) + fsPerfRm(); /* deletes manyfiles and manytree */ + if (g_fChSize) + fsPerfChSize(); + if ( g_fReadPerf || g_fReadTests || g_fWritePerf || g_fWriteTests +#ifdef FSPERF_TEST_SENDFILE + || g_fSendFile +#endif +#ifdef RT_OS_LINUX + || g_fSplice +#endif + || g_fSeek || g_fFSync || g_fMMap) + fsPerfIo(); + if (g_fCopy) + fsPerfCopy(); + if (g_fRemote && g_szCommsDir[0] != '\0') + fsPerfRemote(); + } + + /* + * Cleanup: + */ + FsPerfCommsShutdownSlave(); + + g_szDir[g_cchDir] = '\0'; + rc = RTDirRemoveRecursive(g_szDir, RTDIRRMREC_F_CONTENT_AND_DIR | (g_fRelativeDir ? RTDIRRMREC_F_NO_ABS_PATH : 0)); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "RTDirRemoveRecursive(%s,) -> %Rrc\n", g_szDir, rc); + } + else + RTTestFailed(g_hTest, "RTDirCreate(%s) -> %Rrc\n", g_szDir, rc); + } + else + RTTestFailed(g_hTest, "Test directory already exists: %s\n", g_szDir); + + FsPerfCommsShutdownSlave(); + + return RTTestSummaryAndDestroy(g_hTest); +} + diff --git a/src/VBox/ValidationKit/utils/fs/Makefile.kmk b/src/VBox/ValidationKit/utils/fs/Makefile.kmk new file mode 100644 index 00000000..270c9caa --- /dev/null +++ b/src/VBox/ValidationKit/utils/fs/Makefile.kmk @@ -0,0 +1,49 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - File system tests. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# File System Performance (think shared folders). +# +PROGRAMS += FsPerf +FsPerf_TEMPLATE = VBoxValidationKitR3 +FsPerf_SOURCES = FsPerf.cpp + +$(evalcall def_vbox_validationkit_process_python_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/misc/Makefile.kmk b/src/VBox/ValidationKit/utils/misc/Makefile.kmk new file mode 100644 index 00000000..693625ac --- /dev/null +++ b/src/VBox/ValidationKit/utils/misc/Makefile.kmk @@ -0,0 +1,84 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Miscellaneous Utilites. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Load generator, optionally with an ring-0 IPI generator. +# +PROGRAMS += LoadGenerator +if1of ($(KBUILD_TARGET_ARCH), amd64) + ifdef VBOX_WITH_R0_MODULES + ifdef VBOX_WITH_VBOXR0_AS_DLL + DLLS += loadgeneratorR0 + else + SYSMODS += loadgeneratorR0 + endif + loadgeneratorR0_TEMPLATE := VBoxValidationKitR0 + loadgeneratorR0_SOURCES := loadgeneratorR0.cpp + endif + LoadGenerator_TEMPLATE := VBoxValidationKitR3SupDrv + LoadGenerator_DEFS := WITH_IPI_LOAD_GEN +else + LoadGenerator_TEMPLATE := VBoxValidationKitR3 +endif +LoadGenerator_SOURCES := loadgenerator.cpp + +# +# vts_rm - Test cleanup utility similar to unix rm. +# +PROGRAMS += vts_rm +vts_rm_TEMPLATE = VBoxValidationKitR3 +vts_rm_SOURCES = vts_rm.cpp + +# +# vts_tar - Tar for untarring and tarring test related stuff. +# +PROGRAMS += vts_tar +vts_tar_TEMPLATE = VBoxValidationKitR3 +vts_tar_SDKS = VBoxZlibStatic +vts_tar_SOURCES = vts_tar.cpp + +# +# vts_shutdown - initiates a guest or host shut down / reboot. +# +PROGRAMS += vts_shutdown +vts_shutdown_TEMPLATE = VBoxValidationKitR3 +vts_shutdown_SOURCES = ../../../Runtime/tools/RTShutdown.cpp + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/misc/loadgenerator.cpp b/src/VBox/ValidationKit/utils/misc/loadgenerator.cpp new file mode 100644 index 00000000..28b7d1af --- /dev/null +++ b/src/VBox/ValidationKit/utils/misc/loadgenerator.cpp @@ -0,0 +1,366 @@ +/* $Id: loadgenerator.cpp $ */ +/** @file + * Load Generator. + */ + +/* + * Copyright (C) 2007-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/errcore.h> +#include <iprt/thread.h> +#include <iprt/time.h> +#include <iprt/initterm.h> +#include <iprt/message.h> +#include <iprt/string.h> +#include <iprt/stream.h> +#include <iprt/param.h> +#include <iprt/path.h> +#include <iprt/process.h> +#include <iprt/mp.h> +#include <iprt/asm.h> +#include <iprt/getopt.h> +#ifdef WITH_IPI_LOAD_GEN +# include <VBox/sup.h> +#endif + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Whether the threads should quit or not. */ +static bool volatile g_fQuit = false; + + +static void LoadGenSpin(uint64_t cNanoSeconds) +{ + const uint64_t u64StartTS = RTTimeNanoTS(); + do + { + for (uint32_t volatile i = 0; i < 10240 && !g_fQuit; i++) + i++; + } while (RTTimeNanoTS() - u64StartTS < cNanoSeconds && !g_fQuit); +} + + +static DECLCALLBACK(int) LoadGenSpinThreadFunction(RTTHREAD hThreadSelf, void *pvUser) +{ + NOREF(hThreadSelf); + LoadGenSpin(*(uint64_t *)pvUser); + return VINF_SUCCESS; +} + +#ifdef WITH_IPI_LOAD_GEN + +static int LoadGenIpiInit(void) +{ + /* + * Try make sure the support library is initialized... + */ + SUPR3Init(NULL); + + /* + * Load the module. + */ + char szPath[RTPATH_MAX]; + int rc = RTPathAppPrivateArchTop(szPath, sizeof(szPath) - sizeof("/loadgenerator.r0")); + if (RT_SUCCESS(rc)) + { + strcat(szPath, "/loadgeneratorR0.r0"); + void *pvImageBase; + rc = SUPR3LoadServiceModule(szPath, "loadgeneratorR0", "LoadGenR0ServiceReqHandler", &pvImageBase); + if (RT_SUCCESS(rc)) + { + /* done */ + } + else + RTMsgError("SUPR3LoadServiceModule(%s): %Rrc", szPath, rc); + } + else + RTMsgError("RTPathAppPrivateArch: %Rrc", rc); + return rc; +} + + +static void LoadGenIpi(uint64_t cNanoSeconds) +{ + const uint64_t u64StartTS = RTTimeNanoTS(); + do + { + int rc = SUPR3CallR0Service("loadgeneratorR0", sizeof("loadgeneratorR0") - 1, + 0 /* uOperation */, 1 /* cIpis */, NULL /* pReqHdr */); + if (RT_FAILURE(rc)) + { + RTMsgError("SUPR3CallR0Service: %Rrc", rc); + break; + } + } while (RTTimeNanoTS() - u64StartTS < cNanoSeconds && !g_fQuit); +} + + +static DECLCALLBACK(int) LoadGenIpiThreadFunction(RTTHREAD hThreadSelf, void *pvUser) +{ + LoadGenIpi(*(uint64_t *)pvUser); + NOREF(hThreadSelf); + return VINF_SUCCESS; +} + +#endif + + +int main(int argc, char **argv) +{ + static const struct LOADGENTYPE + { + const char *pszName; + int (*pfnInit)(void); + PFNRTTHREAD pfnThread; + } s_aLoadTypes[] = + { + { "spin", NULL, LoadGenSpinThreadFunction }, +#ifdef WITH_IPI_LOAD_GEN + { "ipi", LoadGenIpiInit, LoadGenIpiThreadFunction }, +#endif + }; + unsigned iLoadType = 0; + static RTTHREAD s_aThreads[256]; + int rc; + uint32_t cThreads = 1; + bool fScaleByCpus = false; + RTTHREADTYPE enmThreadType = RTTHREADTYPE_DEFAULT; + RTPROCPRIORITY enmProcPriority = RTPROCPRIORITY_DEFAULT; + uint64_t cNanoSeconds = UINT64_MAX; + + RTR3InitExe(argc, &argv, 0); + + /* + * Parse arguments. + */ + static const RTGETOPTDEF s_aOptions[] = + { + { "--number-of-threads", 'n', RTGETOPT_REQ_UINT32 }, + { "--timeout", 't', RTGETOPT_REQ_STRING }, + { "--thread-type", 'p', RTGETOPT_REQ_STRING }, + { "--scale-by-cpus", 'c', RTGETOPT_REQ_NOTHING }, + { "--load", 'l', RTGETOPT_REQ_STRING }, + }; + int ch; + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0 /* fFlags */); + while ((ch = RTGetOpt(&GetState, &ValueUnion))) + { + switch (ch) + { + case 'n': + cThreads = ValueUnion.u64; + if (cThreads == 0 || cThreads > RT_ELEMENTS(s_aThreads)) + return RTMsgSyntax("Requested number of threads, %RU32, is out of range (1..%d).", + cThreads, RT_ELEMENTS(s_aThreads) - 1); + break; + + case 't': + { + char *psz; + rc = RTStrToUInt64Ex(ValueUnion.psz, &psz, 0, &cNanoSeconds); + if (RT_FAILURE(rc)) + return RTMsgSyntax("Failed reading the alleged timeout number '%s' (rc=%Rrc).", + ValueUnion.psz, rc); + while (*psz == ' ' || *psz == '\t') + psz++; + if (*psz) + { + uint64_t u64Factor = 1; + if (!strcmp(psz, "ns")) + u64Factor = 1; + else if (!strcmp(psz, "ms")) + u64Factor = 1000; + else if (!strcmp(psz, "s")) + u64Factor = 1000000000; + else if (!strcmp(psz, "m")) + u64Factor = UINT64_C(60000000000); + else if (!strcmp(psz, "h")) + u64Factor = UINT64_C(3600000000000); + else + return RTMsgSyntax("Unknown time suffix '%s'", psz); + uint64_t u64 = cNanoSeconds * u64Factor; + if (u64 < cNanoSeconds || (u64 < u64Factor && u64)) + return RTMsgSyntax("Time representation overflowed! (%RU64 * %RU64)", + cNanoSeconds, u64Factor); + cNanoSeconds = u64; + } + break; + } + + case 'p': + { + enmProcPriority = RTPROCPRIORITY_NORMAL; + + uint32_t u32 = RTTHREADTYPE_INVALID; + char *psz; + rc = RTStrToUInt32Ex(ValueUnion.psz, &psz, 0, &u32); + if (RT_FAILURE(rc) || *psz) + { + if (!strcmp(ValueUnion.psz, "default")) + { + enmProcPriority = RTPROCPRIORITY_DEFAULT; + enmThreadType = RTTHREADTYPE_DEFAULT; + } + else if (!strcmp(ValueUnion.psz, "idle")) + { + enmProcPriority = RTPROCPRIORITY_LOW; + enmThreadType = RTTHREADTYPE_INFREQUENT_POLLER; + } + else if (!strcmp(ValueUnion.psz, "high")) + { + enmProcPriority = RTPROCPRIORITY_HIGH; + enmThreadType = RTTHREADTYPE_IO; + } + else + return RTMsgSyntax("can't grok thread type '%s'", + ValueUnion.psz); + } + else + { + enmThreadType = (RTTHREADTYPE)u32; + if (enmThreadType <= RTTHREADTYPE_INVALID || enmThreadType >= RTTHREADTYPE_END) + return RTMsgSyntax("thread type '%d' is out of range (%d..%d)", + ValueUnion.psz, RTTHREADTYPE_INVALID + 1, RTTHREADTYPE_END - 1); + } + break; + } + + case 'c': + fScaleByCpus = true; + break; + + case 'l': + { + for (unsigned i = 0; i < RT_ELEMENTS(s_aLoadTypes); i++) + if (!strcmp(s_aLoadTypes[i].pszName, ValueUnion.psz)) + { + ValueUnion.psz = NULL; + iLoadType = i; + break; + } + if (ValueUnion.psz) + return RTMsgSyntax("Unknown load type '%s'.", ValueUnion.psz); + break; + } + + case 'h': + RTPrintf("Usage: %s [-p|--thread-type <type>] [-t|--timeout <sec|xxx[h|m|s|ms|ns]>] \\\n" + " %*s [-n|--number-of-threads <threads>] [-l|--load <loadtype>]\n" + "\n" + "Load types: " + , RTProcShortName(), strlen(RTProcShortName()), ""); + for (size_t i = 0; i < RT_ELEMENTS(s_aLoadTypes); i++) + RTPrintf(i == 0 ? "%s (default)" : ", %s", s_aLoadTypes[i].pszName); + RTPrintf("\n"); + return 1; + + case 'V': + RTPrintf("$Revision: 155244 $\n"); + return 0; + + case VINF_GETOPT_NOT_OPTION: + return RTMsgSyntax("Unknown argument #%d: '%s'", GetState.iNext-1, ValueUnion.psz); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + /* + * Scale thread count by host cpu count. + */ + if (fScaleByCpus) + { + const unsigned cCpus = RTMpGetOnlineCount(); + if (cCpus * cThreads > RT_ELEMENTS(s_aThreads)) + return RTMsgSyntax("Requested number of threads, %RU32, is out of range (1..%d) when scaled by %d.", + cThreads, RT_ELEMENTS(s_aThreads) - 1, cCpus); + cThreads *= cCpus; + } + + /* + * Modify process and thread priority? (ignore failure) + */ + if (enmProcPriority != RTPROCPRIORITY_DEFAULT) + RTProcSetPriority(enmProcPriority); + if (enmThreadType != RTTHREADTYPE_DEFAULT) + RTThreadSetType(RTThreadSelf(), enmThreadType); + + /* + * Load type specific init. + */ + if (s_aLoadTypes[iLoadType].pfnInit) + { + rc = s_aLoadTypes[iLoadType].pfnInit(); + if (RT_FAILURE(rc)) + return 1; + } + + + /* + * Start threads. + */ + for (unsigned i = 1; i < cThreads; i++) + { + s_aThreads[i] = NIL_RTTHREAD; + rc = RTThreadCreate(&s_aThreads[i], s_aLoadTypes[iLoadType].pfnThread, + &cNanoSeconds, 128*1024, enmThreadType, RTTHREADFLAGS_WAITABLE, "spinner"); + if (RT_FAILURE(rc)) + { + ASMAtomicXchgBool(&g_fQuit, true); + RTMsgError("failed to create thread #%d: %Rrc", i, rc); + while (i-- > 1) + RTThreadWait(s_aThreads[i], 1500, NULL); + return 1; + } + } + + /* our selves */ + s_aLoadTypes[iLoadType].pfnThread(RTThreadSelf(), &cNanoSeconds); + + /* + * Wait for threads. + */ + ASMAtomicXchgBool(&g_fQuit, true); + for (unsigned i = 1; i < cThreads; i++) + RTThreadWait(s_aThreads[i], 1500, NULL); + + return 0; +} + diff --git a/src/VBox/ValidationKit/utils/misc/loadgeneratorR0.cpp b/src/VBox/ValidationKit/utils/misc/loadgeneratorR0.cpp new file mode 100644 index 00000000..006e3db8 --- /dev/null +++ b/src/VBox/ValidationKit/utils/misc/loadgeneratorR0.cpp @@ -0,0 +1,101 @@ +/* $Id: loadgeneratorR0.cpp $ */ +/** @file + * Load Generator, Ring-0 Service. + */ + +/* + * Copyright (C) 2008-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/mp.h> +#include <VBox/sup.h> +#include <iprt/errcore.h> + + + + +/** + * Worker for loadgenR0Ipi. + */ +static DECLCALLBACK(void) loadgenR0IpiWorker(RTCPUID idCpu, void *pvUser1, void *pvUser2) +{ + NOREF(idCpu); + NOREF(pvUser1); + NOREF(pvUser2); +} + + +/** + * Generate broadcast inter processor interrupts (IPI), aka cross calls. + * + * @returns VBox status code. + * @param cIpis The number of IPIs to do. + */ +static int loadgenR0Ipi(uint64_t cIpis) +{ + if (cIpis > _1G || !cIpis) + return VERR_INVALID_PARAMETER; + + while (cIpis-- > 0) + { + int rc = RTMpOnAll(loadgenR0IpiWorker, NULL, NULL); + if (RT_FAILURE(rc)) + return rc; + } + return VINF_SUCCESS; +} + + +/** + * Service request handler entry point. + * + * @copydoc FNSUPR0SERVICEREQHANDLER + */ +extern "C" DECLEXPORT(int) LoadGenR0ServiceReqHandler(PSUPDRVSESSION pSession, uint32_t uOperation, + uint64_t u64Arg, PSUPR0SERVICEREQHDR pReqHdr) + +{ + switch (uOperation) + { + case 0: + if (pReqHdr) + return VERR_INVALID_PARAMETER; + return loadgenR0Ipi(u64Arg); + + default: + NOREF(pSession); + return VERR_NOT_SUPPORTED; + } +} + diff --git a/src/VBox/ValidationKit/utils/misc/vts_rm.cpp b/src/VBox/ValidationKit/utils/misc/vts_rm.cpp new file mode 100644 index 00000000..9b59ab24 --- /dev/null +++ b/src/VBox/ValidationKit/utils/misc/vts_rm.cpp @@ -0,0 +1,54 @@ +/* $Id: vts_rm.cpp $ */ +/** @file + * VirtualBox Validation Kit - rm like utility. + */ + +/* + * Copyright (C) 2013-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/path.h> +#include <iprt/errcore.h> +#include <iprt/initterm.h> +#include <iprt/message.h> + + +int main(int argc, char **argv) +{ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + return RTPathRmCmd(argc, argv); +} + diff --git a/src/VBox/ValidationKit/utils/misc/vts_tar.cpp b/src/VBox/ValidationKit/utils/misc/vts_tar.cpp new file mode 100644 index 00000000..1e9c5c3b --- /dev/null +++ b/src/VBox/ValidationKit/utils/misc/vts_tar.cpp @@ -0,0 +1,54 @@ +/* $Id: vts_tar.cpp $ */ +/** @file + * VirtualBox Validation Kit - tar like utility. + */ + +/* + * Copyright (C) 2013-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/zip.h> +#include <iprt/errcore.h> +#include <iprt/initterm.h> +#include <iprt/message.h> + + +int main(int argc, char **argv) +{ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + return RTZipTarCmd(argc, argv); +} + diff --git a/src/VBox/ValidationKit/utils/network/Makefile.kmk b/src/VBox/ValidationKit/utils/network/Makefile.kmk new file mode 100644 index 00000000..7e5d6db3 --- /dev/null +++ b/src/VBox/ValidationKit/utils/network/Makefile.kmk @@ -0,0 +1,49 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Network tests. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Network Performance Benchmark. +# +PROGRAMS += NetPerf +NetPerf_TEMPLATE = VBoxValidationKitR3 +NetPerf_SOURCES = NetPerf.cpp + +$(evalcall def_vbox_validationkit_process_python_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/network/NetPerf.cpp b/src/VBox/ValidationKit/utils/network/NetPerf.cpp new file mode 100644 index 00000000..ceb6a201 --- /dev/null +++ b/src/VBox/ValidationKit/utils/network/NetPerf.cpp @@ -0,0 +1,2008 @@ +/* $Id: NetPerf.cpp $ */ +/** @file + * NetPerf - Network Performance Benchmark. + */ + +/* + * 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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/ctype.h> +#include <iprt/err.h> +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/path.h> +#include <iprt/param.h> +#include <iprt/process.h> +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/tcp.h> +#include <iprt/thread.h> +#include <iprt/test.h> +#include <iprt/time.h> +#include <iprt/timer.h> + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Default TCP port (update help text if you change this) */ +#define NETPERF_DEFAULT_PORT 5002 + +/** Default TCP packet size (bytes) */ +#define NETPERF_DEFAULT_PKT_SIZE_THROUGHPUT 8192 +/** Default TCP packet size (bytes) */ +#define NETPERF_DEFAULT_PKT_SIZE_LATENCY 1024 +/** Maximum packet size possible (bytes). */ +#define NETPERF_MAX_PKT_SIZE _1M +/** Minimum packet size possible (bytes). */ +#define NETPERF_MIN_PKT_SIZE sizeof(NETPERFHDR) + +/** Default timeout in (seconds) */ +#define NETPERF_DEFAULT_TIMEOUT 10 +/** Maximum timeout possible (seconds). */ +#define NETPERF_MAX_TIMEOUT 3600 /* 1h */ +/** Minimum timeout possible (seconds). */ +#define NETPERF_MIN_TIMEOUT 1 + +/** The default warmup time (ms). */ +#define NETPERF_DEFAULT_WARMUP 1000 /* 1s */ +/** The maxium warmup time (ms). */ +#define NETPERF_MAX_WARMUP 60000 /* 60s */ +/** The minimum warmup time (ms). */ +#define NETPERF_MIN_WARMUP 1000 /* 1s */ + +/** The default cool down time (ms). */ +#define NETPERF_DEFAULT_COOL_DOWN 1000 /* 1s */ +/** The maxium cool down time (ms). */ +#define NETPERF_MAX_COOL_DOWN 60000 /* 60s */ +/** The minimum cool down time (ms). */ +#define NETPERF_MIN_COOL_DOWN 1000 /* 1s */ + +/** Maximum socket buffer size possible (bytes). */ +#define NETPERF_MAX_BUF_SIZE _128M +/** Minimum socket buffer size possible (bytes). */ +#define NETPERF_MIN_BUF_SIZE 256 + +/** The length of the length prefix used when submitting parameters and + * results. */ +#define NETPERF_LEN_PREFIX 4 + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef enum NETPERFPROTO +{ + NETPERFPROTO_INVALID = 0, + NETPERFPROTO_TCP + //NETPERFPROTO_UDP +} NETPERFPROTO; + +/** + * What kind of test we're performing. + */ +typedef enum NETPERFMODE +{ + NETPERFMODE_INVALID = 0, + /** Latency of a symmetric packet exchange. */ + NETPERFMODE_LATENCY, + /** Separate throughput measurements for each direction. */ + NETPERFMODE_THROUGHPUT, + /** Transmit throughput. */ + NETPERFMODE_THROUGHPUT_XMIT, + /** Transmit throughput. */ + NETPERFMODE_THROUGHPUT_RECV +} NETPERFMODE; + +/** + * Statistics. + */ +typedef struct NETPERFSTATS +{ + uint64_t cTx; + uint64_t cRx; + uint64_t cEchos; + uint64_t cErrors; + uint64_t cNsElapsed; +} NETPERFSTATS; + +/** + * Settings & a little bit of state. + */ +typedef struct NETPERFPARAMS +{ + /** @name Static settings + * @{ */ + /** The TCP port number. */ + uint32_t uPort; + /** Client: Use server statistcs. */ + bool fServerStats; + /** Server: Quit after the first client. */ + bool fSingleClient; + /** Send and receive buffer sizes for TCP sockets, zero if to use defaults. */ + uint32_t cbBufferSize; + /** @} */ + + /** @name Dynamic settings + * @{ */ + /** Disable send packet coalescing. */ + bool fNoDelay; + /** Detect broken payloads. */ + bool fCheckData; + /** The test mode. */ + NETPERFMODE enmMode; + /** The number of seconds to run each of the test steps. */ + uint32_t cSecTimeout; + /** Number of millisecond to spend warning up before testing. */ + uint32_t cMsWarmup; + /** Number of millisecond to spend cooling down after the testing. */ + uint32_t cMsCoolDown; + /** The packet size. */ + uint32_t cbPacket; + /** @} */ + + /** @name State + * @{ */ + RTSOCKET hSocket; + /** @} */ +} NETPERFPARAMS; + +/** + * Packet header used in tests. + * + * Need to indicate when we've timed out and it's time to reverse the roles or + * stop testing. + */ +typedef struct NETPERFHDR +{ + /** Magic value (little endian). */ + uint32_t u32Magic; + /** State value. */ + uint32_t u32State; + /** Sequence number (little endian). */ + uint32_t u32Seq; + /** Reserved, must be zero. */ + uint32_t u32Reserved; +} NETPERFHDR; + +/** Magic value for NETPERFHDR::u32Magic. */ +#define NETPERFHDR_MAGIC UINT32_C(0xfeedf00d) + +/** @name Packet State (NETPERF::u32Magic) + * @{ */ +/** Warm up. */ +#define NETPERFHDR_WARMUP UINT32_C(0x0c0ffe01) +/** The clock is running. */ +#define NETPERFHDR_TESTING UINT32_C(0x0c0ffe02) +/** Stop the clock but continue the package flow. */ +#define NETPERFHDR_COOL_DOWN UINT32_C(0x0c0ffe03) +/** Done, stop the clock if not done already and reply with results. */ +#define NETPERFHDR_DONE UINT32_C(0x0c0ffe04) +/** @} */ + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Connection start/identifier to make sure other end is NetPerf. */ +static const char g_ConnectStart[] = "yo! waaazzzzzaaaaup dude?"; +/** Start of parameters proposal made by the client. */ +static const char g_szStartParams[] = "deal?"; +/** All okay to start test */ +static const char g_szAck[] = "okay!"; +/** Negative. */ +static const char g_szNegative[] = "nope!"; +AssertCompile(sizeof(g_szAck) == sizeof(g_szNegative)); +/** Start of statistics. */ +static const char g_szStartStats[] = "dude, stats"; + +/** Command line parameters */ +static const RTGETOPTDEF g_aCmdOptions[] = +{ + { "--server", 's', RTGETOPT_REQ_NOTHING }, + { "--client", 'c', RTGETOPT_REQ_STRING }, + { "--interval", 'i', RTGETOPT_REQ_UINT32 }, + { "--port", 'p', RTGETOPT_REQ_UINT32 }, + { "--len", 'l', RTGETOPT_REQ_UINT32 }, + { "--nodelay", 'N', RTGETOPT_REQ_NOTHING }, + { "--mode", 'm', RTGETOPT_REQ_STRING }, + { "--warmup", 'w', RTGETOPT_REQ_UINT32 }, + { "--cool-down", 'W', RTGETOPT_REQ_UINT32 }, + { "--server-stats", 'S', RTGETOPT_REQ_NOTHING }, + { "--single-client", '1', RTGETOPT_REQ_NOTHING }, + { "--daemonize", 'd', RTGETOPT_REQ_NOTHING }, + { "--daemonized", 'D', RTGETOPT_REQ_NOTHING }, + { "--check-data", 'C', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + { "--buffer-size", 'b', RTGETOPT_REQ_UINT32 }, + { "--help", 'h', RTGETOPT_REQ_NOTHING } /* for Usage() */ +}; + +/** The test handle. */ +static RTTEST g_hTest; +/** Verbosity level. */ +static uint32_t g_uVerbosity = 0; + + + +static void Usage(PRTSTREAM pStrm) +{ + char szExec[RTPATH_MAX]; + RTStrmPrintf(pStrm, "usage: %s <-s|-c <host>> [options]\n", + RTPathFilename(RTProcGetExecutablePath(szExec, sizeof(szExec)))); + RTStrmPrintf(pStrm, "\n"); + RTStrmPrintf(pStrm, "options: \n"); + + + for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) + { + const char *pszHelp; + switch (g_aCmdOptions[i].iShort) + { + case 'h': + pszHelp = "Displays this help and exit"; + break; + case 's': + pszHelp = "Run in server mode, waiting for clients (default)"; + break; + case 'c': + pszHelp = "Run in client mode, connecting to <host>"; + break; + case 'i': + pszHelp = "Interval in seconds to run the test (default " RT_XSTR(NETPERF_DEFAULT_TIMEOUT) " s)"; + break; + case 'p': + pszHelp = "Server port to listen/connect to (default " RT_XSTR(NETPERF_DEFAULT_PORT) ")"; + break; + case 'l': + pszHelp = "Packet size in bytes (defaults to " RT_XSTR(NETPERF_DEFAULT_PKT_SIZE_LATENCY) + " for latency and " RT_XSTR(NETPERF_DEFAULT_PKT_SIZE_THROUGHPUT) " for throughput)"; + break; + case 'm': + pszHelp = "Test mode: latency (default), throughput, throughput-xmit or throughput-recv"; + break; + case 'N': + pszHelp = "Set TCP no delay, disabling Nagle's algorithm"; + break; + case 'S': + pszHelp = "Report server stats, ignored if server"; + break; + case '1': + pszHelp = "Stop the server after the first client"; + break; + case 'd': + pszHelp = "Daemonize if server, ignored if client"; + break; + case 'D': + continue; /* internal */ + case 'w': + pszHelp = "Warmup time, in milliseconds (default " RT_XSTR(NETPERF_DEFAULT_WARMUP) " ms)"; + break; + case 'W': + pszHelp = "Cool down time, in milliseconds (default " RT_XSTR(NETPERF_DEFAULT_COOL_DOWN) " ms)"; + break; + case 'C': + pszHelp = "Check payload data at the receiving end"; + break; + case 'b': + pszHelp = "Send and receive buffer sizes for TCP"; + break; + case 'v': + pszHelp = "Verbose execution."; + break; + default: + pszHelp = "Option undocumented"; + break; + } + char szOpt[256]; + RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); + RTStrmPrintf(pStrm, " %-20s%s\n", szOpt, pszHelp); + } +} + +/** + * Timer callback employed to set the stop indicator. + * + * This is used both by the client and server side. + * + * @param hTimer The timer, ignored. + * @param pvUser Pointer to the stop variable. + * @param iTick The tick, ignored. + */ +static DECLCALLBACK(void) netperfStopTimerCallback(RTTIMERLR hTimer, void *pvUser, uint64_t iTick) +{ + bool volatile *pfStop = (bool volatile *)pvUser; + if (g_uVerbosity > 0) + RTPrintf("Time's Up!\n"); + ASMAtomicWriteBool(pfStop, true); + NOREF(hTimer); NOREF(iTick); +} + +/** + * Sends a statistics packet to our peer. + * + * @returns IPRT status code. + * @param pStats The stats to send. + * @param hSocket The TCP socket to send them to. + */ +static int netperfSendStats(NETPERFSTATS const *pStats, RTSOCKET hSocket) +{ + char szBuf[256 + NETPERF_LEN_PREFIX]; + size_t cch = RTStrPrintf(&szBuf[NETPERF_LEN_PREFIX], sizeof(szBuf) - NETPERF_LEN_PREFIX, + "%s:%llu:%llu:%llu:%llu:%llu", + g_szStartStats, + pStats->cTx, + pStats->cRx, + pStats->cEchos, + pStats->cErrors, + pStats->cNsElapsed); + + RTStrPrintf(szBuf, NETPERF_LEN_PREFIX + 1, "%0*u", NETPERF_LEN_PREFIX, cch); + szBuf[NETPERF_LEN_PREFIX] = g_szStartStats[0]; + Assert(strlen(szBuf) == cch + NETPERF_LEN_PREFIX); + + int rc = RTTcpWrite(hSocket, szBuf, cch + NETPERF_LEN_PREFIX); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "stats: Failed to send stats: %Rrc\n", rc); + + /* + * Wait for ACK. + */ + rc = RTTcpRead(hSocket, szBuf, sizeof(g_szAck) - 1, NULL); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "stats: failed to write stats: %Rrc\n", rc); + szBuf[sizeof(g_szAck) - 1] = '\0'; + if (!strcmp(szBuf, g_szNegative)) + return RTTestIFailedRc(rc, "stats: client failed to parse them\n"); + if (strcmp(szBuf, g_szAck)) + return RTTestIFailedRc(rc, "stats: got '%s' in instead of ack/nack\n", szBuf); + + return VINF_SUCCESS; +} + +/** + * Receives a statistics packet from our peer. + * + * @returns IPRT status code. Error signalled. + * @param pStats Where to receive the stats. + * @param hSocket The TCP socket to recevie them from. + */ +static int netperfRecvStats(NETPERFSTATS *pStats, RTSOCKET hSocket) +{ + /* + * Read the stats message. + */ + /* the length prefix */ + char szBuf[256 + NETPERF_LEN_PREFIX]; + int rc = RTTcpRead(hSocket, szBuf, NETPERF_LEN_PREFIX, NULL); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "stats: failed to read stats prefix: %Rrc\n", rc); + + szBuf[NETPERF_LEN_PREFIX] = '\0'; + uint32_t cch; + rc = RTStrToUInt32Full(szBuf, 10, &cch); + if (rc != VINF_SUCCESS) + return RTTestIFailedRc(RT_SUCCESS(rc) ? -rc : rc, "stats: bad stat length prefix: '%s' - %Rrc\n", szBuf, rc); + if (cch >= sizeof(szBuf)) + return RTTestIFailedRc(VERR_BUFFER_OVERFLOW, "stats: too large: %u bytes\n", cch); + + /* the actual message */ + rc = RTTcpRead(hSocket, szBuf, cch, NULL); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "failed to read stats: %Rrc\n", rc); + szBuf[cch] = '\0'; + + /* + * Validate the message header. + */ + if ( strncmp(szBuf, g_szStartStats, sizeof(g_szStartStats) - 1) + || szBuf[sizeof(g_szStartStats) - 1] != ':') + return RTTestIFailedRc(VERR_NET_PROTOCOL_ERROR, "stats: invalid packet start: '%s'\n", szBuf); + char *pszCur = &szBuf[sizeof(g_szStartStats)]; + + /* + * Parse it. + */ + static const char * const s_apszNames[] = + { + "cTx", "cRx", "cEchos", "cErrors", "cNsElapsed" + }; + uint64_t *apu64[RT_ELEMENTS(s_apszNames)] = + { + &pStats->cTx, + &pStats->cRx, + &pStats->cEchos, + &pStats->cErrors, + &pStats->cNsElapsed + }; + + for (unsigned i = 0; i < RT_ELEMENTS(apu64); i++) + { + if (!pszCur) + return RTTestIFailedRc(VERR_PARSE_ERROR, "stats: missing %s\n", s_apszNames[i]); + + char *pszNext = strchr(pszCur, ':'); + if (pszNext) + *pszNext++ = '\0'; + rc = RTStrToUInt64Full(pszCur, 10, apu64[i]); + if (rc != VINF_SUCCESS) + return RTTestIFailedRc(RT_SUCCESS(rc) ? -rc : rc, "stats: bad value for %s: '%s' - %Rrc\n", + s_apszNames[i], pszCur, rc); + + pszCur = pszNext; + } + + if (pszCur) + return RTTestIFailedRc(VERR_PARSE_ERROR, "stats: Unparsed data: '%s'\n", pszCur); + + /* + * Send ACK. + */ + rc = RTTcpWrite(hSocket, g_szAck, sizeof(g_szAck) - 1); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "stats: failed to write ack: %Rrc\n", rc); + + return VINF_SUCCESS; +} + +/** + * TCP Throughput: Print the statistics. + * + * @param pSendStats Send stats. + * @param pRecvStats Receive stats. + * @param cbPacket Packet size. + */ +static void netperfPrintThroughputStats(NETPERFSTATS const *pSendStats, NETPERFSTATS const *pRecvStats, uint32_t cbPacket) +{ + RTTestIValue("Packet size", cbPacket, RTTESTUNIT_BYTES); + + if (pSendStats) + { + double rdSecElapsed = (double)pSendStats->cNsElapsed / 1000000000.0; + RTTestIValue("Sends", pSendStats->cTx, RTTESTUNIT_PACKETS); + RTTestIValue("Send Interval", pSendStats->cNsElapsed, RTTESTUNIT_NS); + RTTestIValue("Send Throughput", (uint64_t)((double)(cbPacket * pSendStats->cTx) / rdSecElapsed), RTTESTUNIT_BYTES_PER_SEC); + RTTestIValue("Send Rate", (uint64_t)((double)pSendStats->cTx / rdSecElapsed), RTTESTUNIT_PACKETS_PER_SEC); + RTTestIValue("Send Latency", (uint64_t)(rdSecElapsed / (double)pSendStats->cTx * 1000000000.0), RTTESTUNIT_NS_PER_PACKET); + } + + if (pRecvStats) + { + double rdSecElapsed = (double)pRecvStats->cNsElapsed / 1000000000.0; + RTTestIValue("Receives", pRecvStats->cRx, RTTESTUNIT_PACKETS); + RTTestIValue("Receive Interval", pRecvStats->cNsElapsed, RTTESTUNIT_NS); + RTTestIValue("Receive Throughput", (uint64_t)((double)(cbPacket * pRecvStats->cRx) / rdSecElapsed), RTTESTUNIT_BYTES_PER_SEC); + RTTestIValue("Receive Rate", (uint64_t)((double)pRecvStats->cRx / rdSecElapsed), RTTESTUNIT_PACKETS_PER_SEC); + RTTestIValue("Receive Latency", (uint64_t)(rdSecElapsed / (double)pRecvStats->cRx * 1000000000.0), RTTESTUNIT_NS_PER_PACKET); + } +} + +/** + * TCP Throughput: Send data to the other party. + * + * @returns IPRT status code. + * @param pParams The TCP parameters block. + * @param pBuf The buffer we're using when sending. + * @param pSendStats Where to return the statistics. + */ +static int netperfTCPThroughputSend(NETPERFPARAMS const *pParams, NETPERFHDR *pBuf, NETPERFSTATS *pSendStats) +{ + RT_ZERO(*pSendStats); + + /* + * Create the timer + */ + RTTIMERLR hTimer; + bool volatile fStop = false; + int rc = RTTimerLRCreateEx(&hTimer, 0 /* nsec */, RTTIMER_FLAGS_CPU_ANY, netperfStopTimerCallback, (void *)&fStop); + if (RT_SUCCESS(rc)) + { + uint32_t u32Seq = 0; + + RT_BZERO(pBuf, pParams->cbPacket); + pBuf->u32Magic = RT_H2LE_U32_C(NETPERFHDR_MAGIC); + pBuf->u32State = 0; + pBuf->u32Seq = 0; + pBuf->u32Reserved = 0; + + /* + * Warm up. + */ + if (g_uVerbosity > 0) + RTPrintf("Warmup...\n"); + pBuf->u32State = RT_H2LE_U32_C(NETPERFHDR_WARMUP); + rc = RTTimerLRStart(hTimer, pParams->cMsWarmup * UINT64_C(1000000) /* nsec */); + if (RT_SUCCESS(rc)) + { + while (!fStop) + { + u32Seq++; + pBuf->u32Seq = RT_H2LE_U32(u32Seq); + rc = RTTcpWrite(pParams->hSocket, pBuf, pParams->cbPacket); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpWrite/warmup: %Rrc\n", rc); + break; + } + } + } + else + RTTestIFailed("RTTimerLRStart/warmup: %Rrc\n", rc); + + /* + * The real thing. + */ + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + RTPrintf("The real thing...\n"); + pBuf->u32State = RT_H2LE_U32_C(NETPERFHDR_TESTING); + fStop = false; + rc = RTTimerLRStart(hTimer, pParams->cSecTimeout * UINT64_C(1000000000) /* nsec */); + if (RT_SUCCESS(rc)) + { + uint64_t u64StartTS = RTTimeNanoTS(); + while (!fStop) + { + u32Seq++; + pBuf->u32Seq = RT_H2LE_U32(u32Seq); + rc = RTTcpWrite(pParams->hSocket, pBuf, pParams->cbPacket); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpWrite/testing: %Rrc\n", rc); + break; + } + pSendStats->cTx++; + } + pSendStats->cNsElapsed = RTTimeNanoTS() - u64StartTS; + } + else + RTTestIFailed("RTTimerLRStart/testing: %Rrc\n", rc); + } + + /* + * Cool down. + */ + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + RTPrintf("Cool down...\n"); + pBuf->u32State = RT_H2LE_U32_C(NETPERFHDR_COOL_DOWN); + fStop = false; + rc = RTTimerLRStart(hTimer, pParams->cMsCoolDown * UINT64_C(1000000) /* nsec */); + if (RT_SUCCESS(rc)) + { + while (!fStop) + { + u32Seq++; + pBuf->u32Seq = RT_H2LE_U32(u32Seq); + rc = RTTcpWrite(pParams->hSocket, pBuf, pParams->cbPacket); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpWrite/cool down: %Rrc\n", rc); + break; + } + } + } + else + RTTestIFailed("RTTimerLRStart/testing: %Rrc\n", rc); + } + + /* + * Send DONE packet. + */ + if (g_uVerbosity > 0) + RTPrintf("Done\n"); + if (RT_SUCCESS(rc)) + { + u32Seq++; + pBuf->u32Seq = RT_H2LE_U32(u32Seq); + pBuf->u32State = RT_H2LE_U32_C(NETPERFHDR_DONE); + rc = RTTcpWrite(pParams->hSocket, pBuf, pParams->cbPacket); + if (RT_FAILURE(rc)) + RTTestIFailed("RTTcpWrite/done: %Rrc\n", rc); + } + + RTTimerLRDestroy(hTimer); + } + else + RTTestIFailed("Failed to create timer object: %Rrc\n", rc); + return rc; +} + + +/** + * TCP Throughput: Receive data from the other party. + * + * @returns IPRT status code. + * @param pParams The TCP parameters block. + * @param pBuf The buffer we're using when sending. + * @param pStats Where to return the statistics. + */ +static int netperfTCPThroughputRecv(NETPERFPARAMS const *pParams, NETPERFHDR *pBuf, NETPERFSTATS *pStats) +{ + RT_ZERO(*pStats); + + int rc; + uint32_t u32Seq = 0; + uint64_t cRx = 0; + uint64_t u64StartTS = 0; + uint32_t uState = RT_H2LE_U32_C(NETPERFHDR_WARMUP); + + for (;;) + { + rc = RTTcpRead(pParams->hSocket, pBuf, pParams->cbPacket, NULL); + if (RT_FAILURE(rc)) + { + pStats->cErrors++; + RTTestIFailed("RTTcpRead failed: %Rrc\n", rc); + break; + } + if (RT_UNLIKELY( pBuf->u32Magic != RT_H2LE_U32_C(NETPERFHDR_MAGIC) + || pBuf->u32Reserved != 0)) + { + pStats->cErrors++; + RTTestIFailed("Invalid magic or reserved field value: %#x %#x\n", RT_H2LE_U32(pBuf->u32Magic), RT_H2LE_U32(pBuf->u32Reserved)); + rc = VERR_INVALID_MAGIC; + break; + } + + u32Seq += 1; + if (RT_UNLIKELY(pBuf->u32Seq != RT_H2LE_U32(u32Seq))) + { + pStats->cErrors++; + RTTestIFailed("Out of sequence: got %#x, expected %#x\n", RT_H2LE_U32(pBuf->u32Seq), u32Seq); + rc = VERR_WRONG_ORDER; + break; + } + + if (pParams->fCheckData && uState == RT_H2LE_U32_C(NETPERFHDR_TESTING)) + { + unsigned i = sizeof(NETPERFHDR); + for (;i < pParams->cbPacket; ++i) + if (((unsigned char *)pBuf)[i]) + break; + if (i != pParams->cbPacket) + { + pStats->cErrors++; + RTTestIFailed("Broken payload: at %#x got %#x, expected %#x\n", i, ((unsigned char *)pBuf)[i], 0); + rc = VERR_NOT_EQUAL; + break; + } + } + if (RT_LIKELY(pBuf->u32State == uState)) + cRx++; + /* + * Validate and act on switch state. + */ + else if ( uState == RT_H2LE_U32_C(NETPERFHDR_WARMUP) + && pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_TESTING)) + { + cRx = 0; + u64StartTS = RTTimeNanoTS(); + uState = pBuf->u32State; + } + else if ( uState == RT_H2LE_U32_C(NETPERFHDR_TESTING) + && ( pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_COOL_DOWN) + || pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_DONE)) ) + { + pStats->cNsElapsed = RTTimeNanoTS() - u64StartTS; + pStats->cRx = cRx + 1; + uState = pBuf->u32State; + if (uState == RT_H2LE_U32_C(NETPERFHDR_DONE)) + break; + } + else if ( uState == RT_H2LE_U32_C(NETPERFHDR_COOL_DOWN) + && pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_DONE)) + { + uState = pBuf->u32State; + break; + } + else + { + pStats->cErrors++; + RTTestIFailed("Protocol error: invalid state transition %#x -> %#x\n", + RT_LE2H_U32(uState), RT_LE2H_U32(pBuf->u32State)); + rc = VERR_INVALID_MAGIC; + break; + } + } + + AssertReturn(uState == RT_H2LE_U32_C(NETPERFHDR_DONE) || RT_FAILURE(rc), VERR_INVALID_STATE); + return rc; +} + + +/** + * Prints the statistics for the latency test. + * + * @param pStats The statistics. + * @param cbPacket The packet size in bytes. + */ +static void netperfPrintLatencyStats(NETPERFSTATS const *pStats, uint32_t cbPacket) +{ + double rdSecElapsed = (double)pStats->cNsElapsed / 1000000000.0; + RTTestIValue("Transmitted", pStats->cTx, RTTESTUNIT_PACKETS); + RTTestIValue("Successful echos", pStats->cEchos, RTTESTUNIT_PACKETS); + RTTestIValue("Errors", pStats->cErrors, RTTESTUNIT_PACKETS); + RTTestIValue("Interval", pStats->cNsElapsed, RTTESTUNIT_NS); + RTTestIValue("Packet size", cbPacket, RTTESTUNIT_BYTES); + RTTestIValue("Average rate", (uint64_t)((double)pStats->cEchos / rdSecElapsed), RTTESTUNIT_PACKETS_PER_SEC); + RTTestIValue("Average throughput", (uint64_t)((double)(cbPacket * pStats->cEchos) / rdSecElapsed), RTTESTUNIT_BYTES_PER_SEC); + RTTestIValue("Average latency", (uint64_t)(rdSecElapsed / (double)pStats->cEchos * 1000000000.0), RTTESTUNIT_NS_PER_ROUND_TRIP); + RTTestISubDone(); +} + + +/** + * NETPERFMODE -> string. + * + * @returns readonly string. + * @param enmMode The mode. + */ +static const char *netperfModeToString(NETPERFMODE enmMode) +{ + switch (enmMode) + { + case NETPERFMODE_LATENCY: return "latency"; + case NETPERFMODE_THROUGHPUT: return "throughput"; + case NETPERFMODE_THROUGHPUT_XMIT: return "throughput-xmit"; + case NETPERFMODE_THROUGHPUT_RECV: return "throughput-recv"; + default: AssertFailed(); return "internal-error"; + } +} + +/** + * String -> NETPERFMODE. + * + * @returns The corresponding NETPERFMODE, NETPERFMODE_INVALID on failure. + * @param pszMode The mode string. + */ +static NETPERFMODE netperfModeFromString(const char *pszMode) +{ + if (!strcmp(pszMode, "latency")) + return NETPERFMODE_LATENCY; + if ( !strcmp(pszMode, "throughput") + || !strcmp(pszMode, "thruput") ) + return NETPERFMODE_THROUGHPUT; + if ( !strcmp(pszMode, "throughput-xmit") + || !strcmp(pszMode, "thruput-xmit") + || !strcmp(pszMode, "xmit") ) + return NETPERFMODE_THROUGHPUT_XMIT; + if ( !strcmp(pszMode, "throughput-recv") + || !strcmp(pszMode, "thruput-recv") + || !strcmp(pszMode, "recv") ) + return NETPERFMODE_THROUGHPUT_RECV; + return NETPERFMODE_INVALID; +} + + + + + +/** + * TCP Server: Throughput test. + * + * @returns IPRT status code. + * @param pParams The parameters to use for this test. + */ +static int netperfTCPServerDoThroughput(NETPERFPARAMS const *pParams) +{ + /* + * Allocate the buffer. + */ + NETPERFHDR *pBuf = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!pBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + + /* + * Receive first, then Send. The reverse of the client. + */ + NETPERFSTATS RecvStats; + int rc = netperfTCPThroughputRecv(pParams, pBuf, &RecvStats); + if (RT_SUCCESS(rc)) + { + rc = netperfSendStats(&RecvStats, pParams->hSocket); + if (RT_SUCCESS(rc)) + { + NETPERFSTATS SendStats; + rc = netperfTCPThroughputSend(pParams, pBuf, &SendStats); + if (RT_SUCCESS(rc)) + { + rc = netperfSendStats(&SendStats, pParams->hSocket); + netperfPrintThroughputStats(&SendStats, &RecvStats, pParams->cbPacket); + } + } + } + + return rc; +} + +/** + * TCP Server: Throughput xmit test (receive from client). + * + * @returns IPRT status code. + * @param pParams The parameters to use for this test. + */ +static int netperfTCPServerDoThroughputXmit(NETPERFPARAMS const *pParams) +{ + /* + * Allocate the buffer. + */ + NETPERFHDR *pBuf = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!pBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + + /* + * Receive the transmitted data (reverse of client). + */ + NETPERFSTATS RecvStats; + int rc = netperfTCPThroughputRecv(pParams, pBuf, &RecvStats); + if (RT_SUCCESS(rc)) + { + rc = netperfSendStats(&RecvStats, pParams->hSocket); + if (RT_SUCCESS(rc)) + netperfPrintThroughputStats(NULL, &RecvStats, pParams->cbPacket); + } + + return rc; +} + +/** + * TCP Server: Throughput recv test (transmit to client). + * + * @returns IPRT status code. + * @param pParams The parameters to use for this test. + */ +static int netperfTCPServerDoThroughputRecv(NETPERFPARAMS const *pParams) +{ + /* + * Allocate the buffer. + */ + NETPERFHDR *pBuf = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!pBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + + /* + * Send data to the client (reverse of client). + */ + NETPERFSTATS SendStats; + int rc = netperfTCPThroughputSend(pParams, pBuf, &SendStats); + if (RT_SUCCESS(rc)) + { + rc = netperfSendStats(&SendStats, pParams->hSocket); + if (RT_SUCCESS(rc)) + netperfPrintThroughputStats(&SendStats, NULL, pParams->cbPacket); + } + + return rc; +} + +/** + * TCP Server: Latency test. + * + * @returns IPRT status code. + * @param pParams The parameters to use for this test. + */ +static int netperfTCPServerDoLatency(NETPERFPARAMS const *pParams) +{ + NETPERFHDR *pBuf = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!pBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Failed to allocated packet buffer of %u bytes.\n", pParams->cbPacket); + + /* + * Ping pong with client. + */ + int rc; + uint32_t uState = RT_H2LE_U32_C(NETPERFHDR_WARMUP); + uint32_t u32Seq = 0; + uint64_t cTx = 0; + uint64_t cRx = 0; + uint64_t u64StartTS = 0; + NETPERFSTATS Stats; + RT_ZERO(Stats); + for (;;) + { + rc = RTTcpRead(pParams->hSocket, pBuf, pParams->cbPacket, NULL); + if (RT_FAILURE(rc)) + { + RTTestIFailed("Failed to read data from client: %Rrc\n", rc); + break; + } + + /* + * Validate the packet + */ + if (RT_UNLIKELY( pBuf->u32Magic != RT_H2LE_U32_C(NETPERFHDR_MAGIC) + || pBuf->u32Reserved != 0)) + { + RTTestIFailed("Invalid magic or reserved field value: %#x %#x\n", RT_H2LE_U32(pBuf->u32Magic), RT_H2LE_U32(pBuf->u32Reserved)); + rc = VERR_INVALID_MAGIC; + break; + } + + u32Seq += 1; + if (RT_UNLIKELY(pBuf->u32Seq != RT_H2LE_U32(u32Seq))) + { + RTTestIFailed("Out of sequence: got %#x, expected %#x\n", RT_H2LE_U32(pBuf->u32Seq), u32Seq); + rc = VERR_WRONG_ORDER; + break; + } + + /* + * Count the packet if the state remains unchanged. + */ + if (RT_LIKELY(pBuf->u32State == uState)) + cRx++; + /* + * Validate and act on the state transition. + */ + else if ( uState == RT_H2LE_U32_C(NETPERFHDR_WARMUP) + && pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_TESTING)) + { + cRx = cTx = 0; + u64StartTS = RTTimeNanoTS(); + uState = pBuf->u32State; + } + else if ( uState == RT_H2LE_U32_C(NETPERFHDR_TESTING) + && ( pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_COOL_DOWN) + || pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_DONE)) ) + { + Stats.cNsElapsed = RTTimeNanoTS() - u64StartTS; + Stats.cEchos = cTx; + Stats.cTx = cTx; + Stats.cRx = cRx; + uState = pBuf->u32State; + if (uState == RT_H2LE_U32_C(NETPERFHDR_DONE)) + break; + } + else if ( uState == RT_H2LE_U32_C(NETPERFHDR_COOL_DOWN) + && pBuf->u32State == RT_H2LE_U32_C(NETPERFHDR_DONE)) + { + uState = pBuf->u32State; + break; + } + else + { + RTTestIFailed("Protocol error: invalid state transition %#x -> %#x\n", + RT_LE2H_U32(uState), RT_LE2H_U32(pBuf->u32State)); + break; + } + + /* + * Write same data back to client. + */ + rc = RTTcpWrite(pParams->hSocket, pBuf, pParams->cbPacket); + if (RT_FAILURE(rc)) + { + RTTestIFailed("Failed to write data to client: %Rrc\n", rc); + break; + } + + cTx++; + } + + /* + * Send stats to client and print them. + */ + if (uState == RT_H2LE_U32_C(NETPERFHDR_DONE)) + netperfSendStats(&Stats, pParams->hSocket); + + if ( uState == RT_H2LE_U32_C(NETPERFHDR_DONE) + || uState == RT_H2LE_U32_C(NETPERFHDR_COOL_DOWN)) + netperfPrintLatencyStats(&Stats, pParams->cbPacket); + + RTMemFree(pBuf); + return rc; +} + +/** + * Parses the parameters the client has sent us. + * + * @returns IPRT status code. Message has been shown on failure. + * @param pParams The parameter structure to store the parameters + * in. + * @param pszParams The parameter string sent by the client. + */ +static int netperfTCPServerParseParams(NETPERFPARAMS *pParams, char *pszParams) +{ + /* + * Set defaults for the dynamic settings. + */ + pParams->fNoDelay = false; + pParams->enmMode = NETPERFMODE_LATENCY; + pParams->cSecTimeout = NETPERF_DEFAULT_WARMUP; + pParams->cMsCoolDown = NETPERF_DEFAULT_COOL_DOWN; + pParams->cMsWarmup = NETPERF_DEFAULT_WARMUP; + pParams->cbPacket = NETPERF_DEFAULT_PKT_SIZE_LATENCY; + + /* + * Parse the client parameters. + */ + /* first arg: transport type. [mandatory] */ + char *pszCur = strchr(pszParams, ':'); + if (!pszCur) + return RTTestIFailedRc(VERR_PARSE_ERROR, "client params: No colon\n"); + char *pszNext = strchr(++pszCur, ':'); + if (pszNext) + *pszNext++ = '\0'; + if (strcmp(pszCur, "TCP")) + return RTTestIFailedRc(VERR_PARSE_ERROR, "client params: Invalid transport type: \"%s\"\n", pszCur); + pszCur = pszNext; + + /* second arg: mode. [mandatory] */ + if (!pszCur) + return RTTestIFailedRc(VERR_PARSE_ERROR, "client params: Missing test mode\n"); + pszNext = strchr(pszCur, ':'); + if (pszNext) + *pszNext++ = '\0'; + pParams->enmMode = netperfModeFromString(pszCur); + if (pParams->enmMode == NETPERFMODE_INVALID) + return RTTestIFailedRc(VERR_PARSE_ERROR, "client params: Invalid test mode: \"%s\"\n", pszCur); + pszCur = pszNext; + + /* + * The remainder are uint32_t or bool. + */ + struct + { + bool fBool; + bool fMandatory; + void *pvValue; + uint32_t uMin; + uint32_t uMax; + const char *pszName; + } aElements[] = + { + { false, true, &pParams->cSecTimeout, NETPERF_MIN_TIMEOUT, NETPERF_MAX_TIMEOUT, "timeout" }, + { false, true, &pParams->cbPacket, NETPERF_MIN_PKT_SIZE, NETPERF_MAX_PKT_SIZE, "packet size" }, + { false, true, &pParams->cMsWarmup, NETPERF_MIN_WARMUP, NETPERF_MAX_WARMUP, "warmup period" }, + { false, true, &pParams->cMsCoolDown, NETPERF_MIN_COOL_DOWN, NETPERF_MAX_COOL_DOWN, "cool down period" }, + { true, true, &pParams->fNoDelay, false, true, "no delay" }, + }; + + for (unsigned i = 0; i < RT_ELEMENTS(aElements); i++) + { + if (!pszCur) + return aElements[i].fMandatory + ? RTTestIFailedRc(VERR_PARSE_ERROR, "client params: missing %s\n", aElements[i].pszName) + : VINF_SUCCESS; + + pszNext = strchr(pszCur, ':'); + if (pszNext) + *pszNext++ = '\0'; + uint32_t u32; + int rc = RTStrToUInt32Full(pszCur, 10, &u32); + if (rc != VINF_SUCCESS) + return RTTestIFailedRc(VERR_PARSE_ERROR, "client params: bad %s value \"%s\": %Rrc\n", + aElements[i].pszName, pszCur, rc); + + if ( u32 < aElements[i].uMin + || u32 > aElements[i].uMax) + return RTTestIFailedRc(VERR_PARSE_ERROR, "client params: %s %u s is out of range (%u..%u)\n", + aElements[i].pszName, u32, aElements[i].uMin, aElements[i].uMax); + if (aElements[i].fBool) + *(bool *)aElements[i].pvValue = u32 ? true : false; + else + *(uint32_t *)aElements[i].pvValue = u32; + + pszCur = pszNext; + } + + /* Fail if too many elements. */ + if (pszCur) + return RTTestIFailedRc(VERR_PARSE_ERROR, "client params: too many elements: \"%s\"\n", + pszCur); + return VINF_SUCCESS; +} + + +/** + * TCP server callback that handles one client connection. + * + * @returns IPRT status code. VERR_TCP_SERVER_STOP is special. + * @param hSocket The client socket. + * @param pvUser Our parameters. + */ +static DECLCALLBACK(int) netperfTCPServerWorker(RTSOCKET hSocket, void *pvUser) +{ + NETPERFPARAMS *pParams = (NETPERFPARAMS *)pvUser; + AssertReturn(pParams, VERR_INVALID_POINTER); + + pParams->hSocket = hSocket; + + RTNETADDR Addr; + int rc = RTTcpGetPeerAddress(hSocket, &Addr); + if (RT_SUCCESS(rc)) + RTTestIPrintf(RTTESTLVL_ALWAYS, "Client connected from %RTnaddr\n", &Addr); + else + { + RTTestIPrintf(RTTESTLVL_ALWAYS, "Failed to get client details: %Rrc\n", rc); + Addr.enmType = RTNETADDRTYPE_INVALID; + } + + /* + * Adjust send and receive buffer sizes if necessary. + */ + if (pParams->cbBufferSize) + { + rc = RTTcpSetBufferSize(hSocket, pParams->cbBufferSize); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to set socket buffer sizes to %#x: %Rrc\n", pParams->cbBufferSize, rc); + } + + /* + * Greet the other dude. + */ + rc = RTTcpWrite(hSocket, g_ConnectStart, sizeof(g_ConnectStart) - 1); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to send connection start Id: %Rrc\n", rc); + + /* + * Read connection parameters. + */ + char szBuf[256]; + rc = RTTcpRead(hSocket, szBuf, NETPERF_LEN_PREFIX, NULL); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to read connection parameters: %Rrc\n", rc); + szBuf[NETPERF_LEN_PREFIX] = '\0'; + uint32_t cchParams; + rc = RTStrToUInt32Full(szBuf, 10, &cchParams); + if (rc != VINF_SUCCESS) + return RTTestIFailedRc(RT_SUCCESS(rc) ? VERR_INTERNAL_ERROR : rc, + "Failed to read connection parameters: %Rrc\n", rc); + if (cchParams >= sizeof(szBuf)) + return RTTestIFailedRc(VERR_TOO_MUCH_DATA, "parameter packet is too big (%u bytes)\n", cchParams); + rc = RTTcpRead(hSocket, szBuf, cchParams, NULL); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to read connection parameters: %Rrc\n", rc); + szBuf[cchParams] = '\0'; + + if (strncmp(szBuf, g_szStartParams, sizeof(g_szStartParams) - 1)) + return RTTestIFailedRc(VERR_NET_PROTOCOL_ERROR, "Invalid connection parameters '%s'\n", szBuf); + + /* + * Parse the parameters and signal whether we've got a deal or not. + */ + rc = netperfTCPServerParseParams(pParams, szBuf); + if (RT_FAILURE(rc)) + { + int rc2 = RTTcpWrite(hSocket, g_szNegative, sizeof(g_szNegative) - 1); + if (RT_FAILURE(rc2)) + RTTestIFailed("Failed to send negative ack: %Rrc\n", rc2); + return rc; + } + + if (Addr.enmType != RTNETADDRTYPE_INVALID) + RTTestISubF("%RTnaddr - %s, %u s, %u bytes", &Addr, + netperfModeToString(pParams->enmMode), pParams->cSecTimeout, pParams->cbPacket); + else + RTTestISubF("Unknown - %s, %u s, %u bytes", + netperfModeToString(pParams->enmMode), pParams->cSecTimeout, pParams->cbPacket); + + rc = RTTcpSetSendCoalescing(hSocket, !pParams->fNoDelay); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to apply no-delay option (%RTbool): %Rrc\n", pParams->fNoDelay, rc); + + rc = RTTcpWrite(hSocket, g_szAck, sizeof(g_szAck) - 1); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to send start test commend to client: %Rrc\n", rc); + + /* + * Take action according to our mode. + */ + switch (pParams->enmMode) + { + case NETPERFMODE_LATENCY: + rc = netperfTCPServerDoLatency(pParams); + break; + + case NETPERFMODE_THROUGHPUT: + rc = netperfTCPServerDoThroughput(pParams); + break; + + case NETPERFMODE_THROUGHPUT_XMIT: + rc = netperfTCPServerDoThroughputXmit(pParams); + break; + + case NETPERFMODE_THROUGHPUT_RECV: + rc = netperfTCPServerDoThroughputRecv(pParams); + break; + + case NETPERFMODE_INVALID: + rc = VERR_INTERNAL_ERROR; + break; + + /* no default! */ + } + if (rc == VERR_NO_MEMORY) + return VERR_TCP_SERVER_STOP; + + /* + * Wait for other clients or quit. + */ + if (pParams->fSingleClient) + return VERR_TCP_SERVER_STOP; + return VINF_SUCCESS; +} + + +/** + * TCP server. + * + * @returns IPRT status code. + * @param pParams The TCP parameter block. + */ +static int netperfTCPServer(NETPERFPARAMS *pParams) +{ + /* + * Spawn the TCP server thread & listen. + */ + PRTTCPSERVER pServer; + int rc = RTTcpServerCreateEx(NULL, pParams->uPort, &pServer); + if (RT_SUCCESS(rc)) + { + RTPrintf("Server listening on TCP port %d\n", pParams->uPort); + rc = RTTcpServerListen(pServer, netperfTCPServerWorker, pParams); + RTTcpServerDestroy(pServer); + } + else + RTPrintf("Failed to create TCP server thread: %Rrc\n", rc); + + return rc; +} + +/** + * The server part. + * + * @returns Exit code. + * @param enmProto The protocol. + * @param pParams The parameter block. + */ +static RTEXITCODE netperfServer(NETPERFPROTO enmProto, NETPERFPARAMS *pParams) +{ + + switch (enmProto) + { + case NETPERFPROTO_TCP: + { + int rc = netperfTCPServer(pParams); + return RT_SUCCESS(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; + } + + default: + RTTestIFailed("Protocol not supported.\n"); + return RTEXITCODE_FAILURE; + } +} + + + + + +/** + * TCP client: Do the throughput test. + * + * @returns IPRT status code + * @param pParams The parameters. + */ +static int netperfTCPClientDoThroughput(NETPERFPARAMS *pParams) +{ + /* + * Allocate the buffer. + */ + NETPERFHDR *pBuf = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!pBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + + /* + * Send first, then Receive. + */ + NETPERFSTATS SendStats; + int rc = netperfTCPThroughputSend(pParams, pBuf, &SendStats); + if (RT_SUCCESS(rc)) + { + NETPERFSTATS SrvSendStats; + rc = netperfRecvStats(&SrvSendStats, pParams->hSocket); + if (RT_SUCCESS(rc)) + { + NETPERFSTATS RecvStats; + rc = netperfTCPThroughputRecv(pParams, pBuf, &RecvStats); + if (RT_SUCCESS(rc)) + { + NETPERFSTATS SrvRecvStats; + rc = netperfRecvStats(&SrvRecvStats, pParams->hSocket); + if (RT_SUCCESS(rc)) + { + if (pParams->fServerStats) + netperfPrintThroughputStats(&SrvSendStats, &SrvRecvStats, pParams->cbPacket); + else + netperfPrintThroughputStats(&SendStats, &RecvStats, pParams->cbPacket); + } + } + } + } + + RTTestISubDone(); + return rc; +} + +/** + * TCP client: Do the throughput xmit test. + * + * @returns IPRT status code + * @param pParams The parameters. + */ +static int netperfTCPClientDoThroughputXmit(NETPERFPARAMS *pParams) +{ + /* + * Allocate the buffer. + */ + NETPERFHDR *pBuf = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!pBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + + /* + * Do the job. + */ + NETPERFSTATS SendStats; + int rc = netperfTCPThroughputSend(pParams, pBuf, &SendStats); + if (RT_SUCCESS(rc)) + { + NETPERFSTATS SrvSendStats; + rc = netperfRecvStats(&SrvSendStats, pParams->hSocket); + if (RT_SUCCESS(rc)) + { + if (pParams->fServerStats) + netperfPrintThroughputStats(&SrvSendStats, NULL, pParams->cbPacket); + else + netperfPrintThroughputStats(&SendStats, NULL, pParams->cbPacket); + } + } + + RTTestISubDone(); + return rc; +} + +/** + * TCP client: Do the throughput recv test. + * + * @returns IPRT status code + * @param pParams The parameters. + */ +static int netperfTCPClientDoThroughputRecv(NETPERFPARAMS *pParams) +{ + /* + * Allocate the buffer. + */ + NETPERFHDR *pBuf = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!pBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + + /* + * Do the job. + */ + NETPERFSTATS RecvStats; + int rc = netperfTCPThroughputRecv(pParams, pBuf, &RecvStats); + if (RT_SUCCESS(rc)) + { + NETPERFSTATS SrvRecvStats; + rc = netperfRecvStats(&SrvRecvStats, pParams->hSocket); + if (RT_SUCCESS(rc)) + { + if (pParams->fServerStats) + netperfPrintThroughputStats(NULL, &SrvRecvStats, pParams->cbPacket); + else + netperfPrintThroughputStats(NULL, &RecvStats, pParams->cbPacket); + } + } + + RTTestISubDone(); + return rc; +} + +/** + * TCP client: Do the latency test. + * + * @returns IPRT status code + * @param pParams The parameters. + */ +static int netperfTCPClientDoLatency(NETPERFPARAMS *pParams) +{ + /* + * Generate a selection of packages before we start, after all we're not + * benchmarking the random number generator, are we. :-) + */ + void *pvReadBuf = RTMemAllocZ(pParams->cbPacket); + if (!pvReadBuf) + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + + size_t i; + NETPERFHDR *apPackets[256]; + for (i = 0; i < RT_ELEMENTS(apPackets); i++) + { + apPackets[i] = (NETPERFHDR *)RTMemAllocZ(pParams->cbPacket); + if (!apPackets[i]) + { + while (i-- > 0) + RTMemFree(apPackets[i]); + RTMemFree(pvReadBuf); + return RTTestIFailedRc(VERR_NO_MEMORY, "Out of memory"); + } + RTRandBytes(apPackets[i], pParams->cbPacket); + apPackets[i]->u32Magic = RT_H2LE_U32_C(NETPERFHDR_MAGIC); + apPackets[i]->u32State = 0; + apPackets[i]->u32Seq = 0; + apPackets[i]->u32Reserved = 0; + } + + /* + * Create & start a timer to eventually disconnect. + */ + bool volatile fStop = false; + RTTIMERLR hTimer; + int rc = RTTimerLRCreateEx(&hTimer, 0 /* nsec */, RTTIMER_FLAGS_CPU_ANY, netperfStopTimerCallback, (void *)&fStop); + if (RT_SUCCESS(rc)) + { + uint32_t u32Seq = 0; + NETPERFSTATS Stats; + RT_ZERO(Stats); + + /* + * Warm up. + */ + if (g_uVerbosity > 0) + RTPrintf("Warmup...\n"); + rc = RTTimerLRStart(hTimer, pParams->cMsWarmup * UINT64_C(1000000) /* nsec */); + if (RT_SUCCESS(rc)) + { + while (!fStop) + { + NETPERFHDR *pPacket = apPackets[u32Seq % RT_ELEMENTS(apPackets)]; + u32Seq++; + pPacket->u32Seq = RT_H2LE_U32(u32Seq); + pPacket->u32State = RT_H2LE_U32_C(NETPERFHDR_WARMUP); + rc = RTTcpWrite(pParams->hSocket, pPacket, pParams->cbPacket); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpWrite/warmup: %Rrc\n", rc); + break; + } + rc = RTTcpRead(pParams->hSocket, pvReadBuf, pParams->cbPacket, NULL); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpRead/warmup: %Rrc\n", rc); + break; + } + } + } + else + RTTestIFailed("RTTimerLRStart/warmup: %Rrc\n", rc); + + /* + * The real thing. + */ + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + RTPrintf("The real thing...\n"); + fStop = false; + rc = RTTimerLRStart(hTimer, pParams->cSecTimeout * UINT64_C(1000000000) /* nsec */); + if (RT_SUCCESS(rc)) + { + uint64_t u64StartTS = RTTimeNanoTS(); + while (!fStop) + { + NETPERFHDR *pPacket = apPackets[u32Seq % RT_ELEMENTS(apPackets)]; + u32Seq++; + pPacket->u32Seq = RT_H2LE_U32(u32Seq); + pPacket->u32State = RT_H2LE_U32_C(NETPERFHDR_TESTING); + rc = RTTcpWrite(pParams->hSocket, pPacket, pParams->cbPacket); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpWrite/testing: %Rrc\n", rc); + break; + } + Stats.cTx++; + + rc = RTTcpRead(pParams->hSocket, pvReadBuf, pParams->cbPacket, NULL); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpRead/testing: %Rrc\n", rc); + break; + } + Stats.cRx++; + + if (!memcmp(pvReadBuf, pPacket, pParams->cbPacket)) + Stats.cEchos++; + else + Stats.cErrors++; + } + Stats.cNsElapsed = RTTimeNanoTS() - u64StartTS; + } + else + RTTestIFailed("RTTimerLRStart/testing: %Rrc\n", rc); + } + + /* + * Cool down. + */ + if (RT_SUCCESS(rc)) + { + if (g_uVerbosity > 0) + RTPrintf("Cool down...\n"); + fStop = false; + rc = RTTimerLRStart(hTimer, pParams->cMsCoolDown * UINT64_C(1000000) /* nsec */); + if (RT_SUCCESS(rc)) + { + while (!fStop) + { + NETPERFHDR *pPacket = apPackets[u32Seq % RT_ELEMENTS(apPackets)]; + u32Seq++; + pPacket->u32Seq = RT_H2LE_U32(u32Seq); + pPacket->u32State = RT_H2LE_U32_C(NETPERFHDR_COOL_DOWN); + rc = RTTcpWrite(pParams->hSocket, pPacket, pParams->cbPacket); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpWrite/warmup: %Rrc\n", rc); + break; + } + rc = RTTcpRead(pParams->hSocket, pvReadBuf, pParams->cbPacket, NULL); + if (RT_FAILURE(rc)) + { + RTTestIFailed("RTTcpRead/warmup: %Rrc\n", rc); + break; + } + } + } + else + RTTestIFailed("RTTimerLRStart/testing: %Rrc\n", rc); + } + + /* + * Send DONE packet. + */ + if (g_uVerbosity > 0) + RTPrintf("Done\n"); + if (RT_SUCCESS(rc)) + { + u32Seq++; + NETPERFHDR *pPacket = apPackets[u32Seq % RT_ELEMENTS(apPackets)]; + pPacket->u32Seq = RT_H2LE_U32(u32Seq); + pPacket->u32State = RT_H2LE_U32_C(NETPERFHDR_DONE); + rc = RTTcpWrite(pParams->hSocket, pPacket, pParams->cbPacket); + if (RT_FAILURE(rc)) + RTTestIFailed("RTTcpWrite/done: %Rrc\n", rc); + } + + + /* + * Get and print stats. + */ + NETPERFSTATS SrvStats; + if (RT_SUCCESS(rc)) + { + rc = netperfRecvStats(&SrvStats, pParams->hSocket); + if (RT_SUCCESS(rc) && pParams->fServerStats) + netperfPrintLatencyStats(&SrvStats, pParams->cbPacket); + else if (!pParams->fServerStats) + netperfPrintLatencyStats(&Stats, pParams->cbPacket); + } + + /* clean up*/ + RTTimerLRDestroy(hTimer); + } + else + RTTestIFailed("Failed to create timer object: %Rrc\n", rc); + for (i = 0; i < RT_ELEMENTS(apPackets); i++) + RTMemFree(apPackets[i]); + + RTMemFree(pvReadBuf); + + return rc; +} + +/** + * TCP client test driver. + * + * @returns IPRT status code + * @param pszServer The server name. + * @param pParams The parameter structure. + */ +static int netperfTCPClient(const char *pszServer, NETPERFPARAMS *pParams) +{ + AssertReturn(pParams, VERR_INVALID_POINTER); + RTTestISubF("TCP - %u s, %u bytes%s", pParams->cSecTimeout, + pParams->cbPacket, pParams->fNoDelay ? ", no delay" : ""); + + RTSOCKET hSocket = NIL_RTSOCKET; + int rc = RTTcpClientConnect(pszServer, pParams->uPort, &hSocket); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to connect to %s on port %u: %Rrc\n", pszServer, pParams->uPort, rc); + pParams->hSocket = hSocket; + + /* + * Disable send coalescing (no-delay). + */ + if (pParams->fNoDelay) + { + rc = RTTcpSetSendCoalescing(hSocket, false /*fEnable*/); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to set no-delay option: %Rrc\n", rc); + } + + /* + * Adjust send and receive buffer sizes if necessary. + */ + if (pParams->cbBufferSize) + { + rc = RTTcpSetBufferSize(hSocket, pParams->cbBufferSize); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to set socket buffer sizes to %#x: %Rrc\n", pParams->cbBufferSize, rc); + } + + /* + * Verify the super secret Start Connect Id to start the connection. + */ + char szBuf[256 + NETPERF_LEN_PREFIX]; + RT_ZERO(szBuf); + rc = RTTcpRead(hSocket, szBuf, sizeof(g_ConnectStart) - 1, NULL); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to read connection initializer: %Rrc\n", rc); + + if (strcmp(szBuf, g_ConnectStart)) + return RTTestIFailedRc(VERR_INVALID_MAGIC, "Invalid connection initializer '%s'\n", szBuf); + + /* + * Send all the dynamic parameters to the server. + * (If the server is newer than the client, it will select default for any + * missing parameters.) + */ + size_t cchParams = RTStrPrintf(&szBuf[NETPERF_LEN_PREFIX], sizeof(szBuf) - NETPERF_LEN_PREFIX, + "%s:%s:%s:%u:%u:%u:%u:%u", + g_szStartParams, + "TCP", + netperfModeToString(pParams->enmMode), + pParams->cSecTimeout, + pParams->cbPacket, + pParams->cMsWarmup, + pParams->cMsCoolDown, + pParams->fNoDelay); + RTStrPrintf(szBuf, NETPERF_LEN_PREFIX + 1, "%0*u", NETPERF_LEN_PREFIX, cchParams); + szBuf[NETPERF_LEN_PREFIX] = g_szStartParams[0]; + Assert(strlen(szBuf) == NETPERF_LEN_PREFIX + cchParams); + rc = RTTcpWrite(hSocket, szBuf, NETPERF_LEN_PREFIX + cchParams); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to send connection parameters: %Rrc\n", rc); + + /* + * Wait for acknowledgment. + */ + rc = RTTcpRead(hSocket, szBuf, sizeof(g_szAck) - 1, NULL); + if (RT_FAILURE(rc)) + return RTTestIFailedRc(rc, "Failed to send parameters: %Rrc\n", rc); + szBuf[sizeof(g_szAck) - 1] = '\0'; + + if (!strcmp(szBuf, g_szNegative)) + return RTTestIFailedRc(VERR_NET_PROTOCOL_ERROR, "Server failed to accept packet size of %u bytes.\n", pParams->cbPacket); + if (strcmp(szBuf, g_szAck)) + return RTTestIFailedRc(VERR_NET_PROTOCOL_ERROR, "Invalid response from server '%s'\n", szBuf); + + /* + * Take action according to our mode. + */ + switch (pParams->enmMode) + { + case NETPERFMODE_LATENCY: + RTTestIPrintf(RTTESTLVL_ALWAYS, "Connected to %s port %u, running the latency test for %u seconds.\n", + pszServer, pParams->uPort, pParams->cSecTimeout); + rc = netperfTCPClientDoLatency(pParams); + break; + + case NETPERFMODE_THROUGHPUT: + RTTestIPrintf(RTTESTLVL_ALWAYS, "Connected to %s port %u, running the throughput test for %u seconds in each direction.\n", + pszServer, pParams->uPort, pParams->cSecTimeout); + rc = netperfTCPClientDoThroughput(pParams); + break; + + case NETPERFMODE_THROUGHPUT_XMIT: + RTTestIPrintf(RTTESTLVL_ALWAYS, "Connected to %s port %u, running the throughput-xmit test for %u seconds.\n", + pszServer, pParams->uPort, pParams->cSecTimeout); + rc = netperfTCPClientDoThroughputXmit(pParams); + break; + + case NETPERFMODE_THROUGHPUT_RECV: + RTTestIPrintf(RTTESTLVL_ALWAYS, "Connected to %s port %u, running the throughput-recv test for %u seconds.\n", + pszServer, pParams->uPort, pParams->cSecTimeout); + rc = netperfTCPClientDoThroughputRecv(pParams); + break; + + case NETPERFMODE_INVALID: + rc = VERR_INTERNAL_ERROR; + break; + + /* no default! */ + } + return rc; +} + +/** + * The client part. + * + * @returns Exit code. + * @param enmProto The protocol. + * @param pszServer The server name. + * @param pvUser The parameter block as opaque user data. + */ +static RTEXITCODE netperfClient(NETPERFPROTO enmProto, const char *pszServer, void *pvUser) +{ + switch (enmProto) + { + case NETPERFPROTO_TCP: + { + NETPERFPARAMS *pParams = (NETPERFPARAMS *)pvUser; + int rc = netperfTCPClient(pszServer, pParams); + if (pParams->hSocket != NIL_RTSOCKET) + { + RTTcpClientClose(pParams->hSocket); + pParams->hSocket = NIL_RTSOCKET; + } + return RT_SUCCESS(rc) ? RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE; + } + + default: + RTTestIFailed("Protocol not supported.\n"); + return RTEXITCODE_FAILURE; + } +} + + +int main(int argc, char *argv[]) +{ + /* + * Init IPRT and globals. + */ + int rc = RTTestInitAndCreate("NetPerf", &g_hTest); + if (rc) + return rc; + + /* + * Special case. + */ + if (argc < 2) + { + RTTestFailed(g_hTest, "No arguments given."); + return RTTestSummaryAndDestroy(g_hTest); + } + + /* + * Default values. + */ + NETPERFPROTO enmProtocol = NETPERFPROTO_TCP; + bool fServer = true; + bool fDaemonize = false; + bool fDaemonized = false; + bool fPacketSizeSet = false; + const char *pszServerAddress= NULL; + + NETPERFPARAMS Params; + Params.uPort = NETPERF_DEFAULT_PORT; + Params.fServerStats = false; + Params.fSingleClient = false; + + Params.fNoDelay = false; + Params.fCheckData = false; + Params.enmMode = NETPERFMODE_LATENCY; + Params.cSecTimeout = NETPERF_DEFAULT_TIMEOUT; + Params.cMsWarmup = NETPERF_DEFAULT_WARMUP; + Params.cMsCoolDown = NETPERF_DEFAULT_COOL_DOWN; + Params.cbPacket = NETPERF_DEFAULT_PKT_SIZE_LATENCY; + Params.cbBufferSize = 0; + + Params.hSocket = NIL_RTSOCKET; + + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); + while ((rc = RTGetOpt(&GetState, &ValueUnion))) + { + switch (rc) + { + case 's': + fServer = true; + break; + + case 'c': + fServer = false; + pszServerAddress = ValueUnion.psz; + break; + + case 'd': + fDaemonize = true; + break; + + case 'D': + fDaemonized = true; + break; + + case 'i': + Params.cSecTimeout = ValueUnion.u32; + if ( Params.cSecTimeout < NETPERF_MIN_TIMEOUT + || Params.cSecTimeout > NETPERF_MAX_TIMEOUT) + { + RTTestFailed(g_hTest, "Invalid interval %u s, valid range: %u-%u\n", + Params.cbPacket, NETPERF_MIN_TIMEOUT, NETPERF_MAX_TIMEOUT); + return RTTestSummaryAndDestroy(g_hTest); + } + break; + + case 'l': + Params.cbPacket = ValueUnion.u32; + if ( Params.cbPacket < NETPERF_MIN_PKT_SIZE + || Params.cbPacket > NETPERF_MAX_PKT_SIZE) + { + RTTestFailed(g_hTest, "Invalid packet size %u bytes, valid range: %u-%u\n", + Params.cbPacket, NETPERF_MIN_PKT_SIZE, NETPERF_MAX_PKT_SIZE); + return RTTestSummaryAndDestroy(g_hTest); + } + fPacketSizeSet = true; + break; + + case 'm': + Params.enmMode = netperfModeFromString(ValueUnion.psz); + if (Params.enmMode == NETPERFMODE_INVALID) + { + RTTestFailed(g_hTest, "Invalid test mode: \"%s\"\n", ValueUnion.psz); + return RTTestSummaryAndDestroy(g_hTest); + } + if (!fPacketSizeSet) + switch (Params.enmMode) + { + case NETPERFMODE_LATENCY: + Params.cbPacket = NETPERF_DEFAULT_PKT_SIZE_LATENCY; + break; + case NETPERFMODE_THROUGHPUT: + case NETPERFMODE_THROUGHPUT_XMIT: + case NETPERFMODE_THROUGHPUT_RECV: + Params.cbPacket = NETPERF_DEFAULT_PKT_SIZE_THROUGHPUT; + break; + case NETPERFMODE_INVALID: + break; + /* no default! */ + } + break; + + case 'p': + Params.uPort = ValueUnion.u32; + break; + + case 'N': + Params.fNoDelay = true; + break; + + case 'S': + Params.fServerStats = true; + break; + + case '1': + Params.fSingleClient = true; + break; + + case 'v': + g_uVerbosity++; + break; + + case 'h': + Usage(g_pStdOut); + return RTEXITCODE_SUCCESS; + + case 'V': + RTPrintf("$Revision: 155244 $\n"); + return RTEXITCODE_SUCCESS; + + case 'w': + Params.cMsWarmup = ValueUnion.u32; + if ( Params.cMsWarmup < NETPERF_MIN_WARMUP + || Params.cMsWarmup > NETPERF_MAX_WARMUP) + { + RTTestFailed(g_hTest, "invalid warmup time %u ms, valid range: %u-%u\n", + Params.cMsWarmup, NETPERF_MIN_WARMUP, NETPERF_MAX_WARMUP); + return RTTestSummaryAndDestroy(g_hTest); + } + break; + + case 'W': + Params.cMsCoolDown = ValueUnion.u32; + if ( Params.cMsCoolDown < NETPERF_MIN_COOL_DOWN + || Params.cMsCoolDown > NETPERF_MAX_COOL_DOWN) + { + RTTestFailed(g_hTest, "invalid cool down time %u ms, valid range: %u-%u\n", + Params.cMsCoolDown, NETPERF_MIN_COOL_DOWN, NETPERF_MAX_COOL_DOWN); + return RTTestSummaryAndDestroy(g_hTest); + } + break; + + case 'C': + Params.fCheckData = true; + break; + + case 'b': + Params.cbBufferSize = ValueUnion.u32; + if ( ( Params.cbBufferSize < NETPERF_MIN_BUF_SIZE + || Params.cbBufferSize > NETPERF_MAX_BUF_SIZE) + && Params.cbBufferSize != 0) + { + RTTestFailed(g_hTest, "Invalid packet size %u bytes, valid range: %u-%u or 0\n", + Params.cbBufferSize, NETPERF_MIN_BUF_SIZE, NETPERF_MAX_BUF_SIZE); + return RTTestSummaryAndDestroy(g_hTest); + } + break; + + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + /* + * Handle the server process daemoniziation. + */ + if (fDaemonize && !fDaemonized && fServer) + { + rc = RTProcDaemonize(argv, "--daemonized"); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcDaemonize failed: %Rrc\n", rc); + return RTEXITCODE_SUCCESS; + } + + /* + * Get down to business. + */ + RTTestBanner(g_hTest); + if (fServer) + rc = netperfServer(enmProtocol, &Params); + else if (pszServerAddress) + rc = netperfClient(enmProtocol, pszServerAddress, &Params); + else + RTTestFailed(g_hTest, "missing server address to connect to\n"); + + RTEXITCODE rc2 = RTTestSummaryAndDestroy(g_hTest); + return rc2 != RTEXITCODE_FAILURE ? (RTEXITCODE)rc2 : rc; +} + diff --git a/src/VBox/ValidationKit/utils/nt/Makefile.kmk b/src/VBox/ValidationKit/utils/nt/Makefile.kmk new file mode 100644 index 00000000..67ab901a --- /dev/null +++ b/src/VBox/ValidationKit/utils/nt/Makefile.kmk @@ -0,0 +1,63 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Windows NT Specific Utilities. +# + +# +# 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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Set Clock Frequency Utility. +# +PROGRAMS.win += ntSetFreq +ntSetFreq_TEMPLATE = VBoxValidationKitR3 +ntSetFreq_SOURCES = ntsetfreq.cpp +ntSetFreq_VBOX_IMPORT_CHECKER.win.x86 = nt350 + +# +# Test coherency among NT time sources. +# +PROGRAMS.win += ntTimeSources +ntTimeSources_TEMPLATE = VBoxValidationKitR3 +ntTimeSources_SOURCES = nttimesources.cpp + +# +# Test NtFlushVirtualMemory. +# +PROGRAMS.win += ntFlushVirtualMemory +ntFlushVirtualMemory_TEMPLATE = VBoxValidationKitR3 +ntFlushVirtualMemory_SOURCES = ntFlushVirtualMemory.cpp + +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/nt/ntFlushVirtualMemory.cpp b/src/VBox/ValidationKit/utils/nt/ntFlushVirtualMemory.cpp new file mode 100644 index 00000000..cb739498 --- /dev/null +++ b/src/VBox/ValidationKit/utils/nt/ntFlushVirtualMemory.cpp @@ -0,0 +1,498 @@ +/* $Id: ntFlushVirtualMemory.cpp $ */ +/** @file + * Memory mapped files testcase - NT. + */ + +/* + * Copyright (C) 2007-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/nt/nt.h> + +#include <iprt/alloca.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/path.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/thread.h> +#include <iprt/err.h> +#include <iprt/x86.h> + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** Create page signature. */ +#define MAKE_PAGE_SIGNATURE(a_iPage) ((a_iPage) | UINT32_C(0x42000000) ) + +/** Number history entries on the page. */ +#define NUM_ROUND_HISTORY 16 + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** How chatty we should be. */ +static uint32_t g_cVerbosity = 0; + + +/** + * Checks if the on-disk file matches our expectations. + * + * @returns IPRT status code, fully bitched. + * @param pszFilename The name of the file. + * @param pu32BufChk Buffer to read the file into + * @param pu32BufOrg Expected file content. + * @param cbBuf The buffer size. + * @param iRound The update round. + */ +static int CheckFile(const char *pszFilename, uint32_t *pu32BufChk, uint32_t const *pu32BufOrg, size_t cbBuf, uint32_t iRound) +{ + /* + * Open and read the file into memory. + */ + HANDLE hFile; + int rc = RTNtPathOpen(pszFilename, + GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_NO_INTERMEDIATE_BUFFERING, + OBJ_CASE_INSENSITIVE, + &hFile, + NULL); + if (RT_SUCCESS(rc)) + { + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + NTSTATUS rcNt = NtReadFile(hFile, NULL /*hEvent*/, NULL /*pfnApc*/, NULL /*pvApcCtx*/, + &Ios, pu32BufChk, (ULONG)cbBuf, NULL /*poffFile*/, NULL /*pvKey*/); + if (NT_SUCCESS(rcNt) || Ios.Information != cbBuf) + { + /* + * See if the content of the file matches our expectations. + */ + if (memcmp(pu32BufChk, pu32BufOrg, cbBuf) == 0) + { /* matches - likely */ } + else + { + RTMsgError("Round %u: Buffer mismatch!\n", iRound); + + /* Try figure where the differences are. */ + size_t const cPages = cbBuf / X86_PAGE_SIZE; + size_t const cItemsPerPage = X86_PAGE_SIZE / sizeof(pu32BufOrg[0]); + for (uint32_t iPage = 0; iPage < cPages; iPage++) + for (uint32_t iItem = 0; iItem < cItemsPerPage; iItem++) + { + uint32_t uValue = pu32BufChk[iPage * cItemsPerPage + iItem]; + uint32_t uExpected = pu32BufOrg[iPage * cItemsPerPage + iItem]; + if (uValue != uExpected) + RTMsgError("Round %u: page #%u, index #%u: %#x, expected %#x\n", + iRound, iPage, iItem, uValue, uExpected); + } + + rc = VERR_MISMATCH; + } + } + else if (NT_SUCCESS(rcNt)) + { + RTMsgError("Round %u: NtReadFile return %zu bytes intead of %zu!\n", iRound, Ios.Information, cbBuf); + rc = VERR_READ_ERROR; + } + else + { + RTMsgError("Round %u: NtReadFile(%#x) failed: %#x (%#x)\n", iRound, cbBuf, rcNt, Ios.Status); + rc = RTErrConvertFromNtStatus(rcNt); + } + + /* + * Close the file and return. + */ + rcNt = NtClose(hFile); + if (!NT_SUCCESS(rcNt)) + { + RTMsgError("Round %u: NtCloseFile() failed: %#x\n", iRound, rcNt); + rc = RTErrConvertFromNtStatus(rcNt); + } + } + else + RTMsgError("Round %u: RTNtPathOpen() failed: %Rrc\n", iRound, rc); + return rc; +} + + +/** + * Manually checks whether the buffer matches up to our expectations. + * + * @returns IPRT status code, fully bitched. + * @param pu32Buf The buffer/mapping to check. + * @param cbBuf The buffer size. + * @param iRound The update round. + * @param cFlushesLeft Number of flushes left in the round. + */ +static int CheckBuffer(uint32_t const *pu32Buf, size_t cbBuf, uint32_t iRound, uint32_t cFlushesLeft) +{ + size_t const cPages = cbBuf / X86_PAGE_SIZE; + size_t const cItemsPerPage = X86_PAGE_SIZE / sizeof(pu32Buf[0]); + size_t const offPage = iRound & (NUM_ROUND_HISTORY - 1); + uint32_t const uValue = iRound | (cFlushesLeft << 20); +//RTPrintf("debug: CheckBuffer: %p %u/%u\n", pu32Buf, iRound, cFlushesLeft); + + for (uint32_t iPage = 0; iPage < cPages; iPage++) + { + uint32_t uActual = pu32Buf[iPage * cItemsPerPage + offPage]; + if (uActual != uValue) + { + RTMsgError("Round %u/%u: page #%u: last entry is corrupted: %#x, expected %#x\n", + iRound, cFlushesLeft, iPage, uActual, uValue); + return VERR_MISMATCH; + } + + uActual = pu32Buf[iPage * cItemsPerPage + cItemsPerPage - 1]; + if (uActual != MAKE_PAGE_SIGNATURE(iPage)) + { + RTMsgError("Round %u/%u: page #%u magic corrupted: %#x, expected %#x\n", + iRound, cFlushesLeft, iPage, uActual, MAKE_PAGE_SIGNATURE(iPage)); + return VERR_INVALID_MAGIC; + } + } + + /* + * Check previous rounds. + */ + for (uint32_t cRoundsAgo = 1; cRoundsAgo < NUM_ROUND_HISTORY - 1 && cRoundsAgo <= iRound; cRoundsAgo++) + { + uint32_t iOldRound = iRound - cRoundsAgo; + size_t const offOldPage = iOldRound & (NUM_ROUND_HISTORY - 1); + for (uint32_t iPage = 0; iPage < cPages; iPage++) + { + uint32_t uActual = pu32Buf[iPage * cItemsPerPage + offOldPage]; + if (uActual != iOldRound) + { + RTMsgError("Round %u/%u: page #%u: entry from %u rounds ago is corrupted: %#x, expected %#x\n", + iRound, cFlushesLeft, iPage, cRoundsAgo, uActual, uValue); + return VERR_MISMATCH; + } + } + } + + return VINF_SUCCESS; +} + + +/** + * Updates the buffer. + * + * @param pu32Buf The buffer/mapping to update. + * @param cbBuf The buffer size. + * @param iRound The update round. + * @param cFlushesLeft Number of flushes left in this round. + */ +static void UpdateBuffer(uint32_t *pu32Buf, size_t cbBuf, uint32_t iRound, uint32_t cFlushesLeft) +{ + size_t const cPages = cbBuf / X86_PAGE_SIZE; + size_t const cItemsPerPage = X86_PAGE_SIZE / sizeof(pu32Buf[0]); + size_t const offPage = iRound & (NUM_ROUND_HISTORY - 1); + uint32_t const uValue = iRound | (cFlushesLeft << 20); +//RTPrintf("debug: UpdateBuffer: %p %u/%u\n", pu32Buf, iRound, cFlushesLeft); + + for (uint32_t iPage = 0; iPage < cPages; iPage++) + pu32Buf[iPage * cItemsPerPage + offPage] = uValue; +} + + + +/** + * Modifies the file via memory mapping. + * + * @returns IPRT status code, fully bitched. + * @param pszFilename The file we're using as a test bed. + * @param pu32BufOrg The sane copy of the file that gets updated in + * parallel. + * @param cbBuf The size of the file and bufer. + * @param iRound The current round number. + * @param fCheckFirst Whether to read from the mapping the mapping + * before dirtying it the first time around. + * @param fCheckAfterFlush Whether to read from the mapping the mapping + * before dirtying it after a flush. + * @param cFlushes How many times we modify the mapping and flush + * it before one final modification and unmapping. + * @param fLargePages Whether to use large pages. + */ +static int MakeModifications(const char *pszFilename, uint32_t *pu32BufOrg, size_t cbBuf, uint32_t iRound, + bool fCheckFirst, bool fCheckAfterFlush, uint32_t cFlushes, bool fLargePages) +{ + + HANDLE hFile = RTNT_INVALID_HANDLE_VALUE; + int rc = RTNtPathOpen(pszFilename, + GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_NO_INTERMEDIATE_BUFFERING, + OBJ_CASE_INSENSITIVE, + &hFile, + NULL); + if (RT_SUCCESS(rc)) + { + HANDLE hSection; + NTSTATUS rcNt = NtCreateSection(&hSection, + SECTION_ALL_ACCESS, + NULL, /*pObjAttrs*/ + NULL, /*pcbMax*/ + PAGE_READWRITE, + SEC_COMMIT, + hFile); + NtClose(hFile); + if (NT_SUCCESS(rcNt)) + { + PVOID pvMapping = NULL; + SIZE_T cbMapping = 0; + rcNt = NtMapViewOfSection(hSection, NtCurrentProcess(), + &pvMapping, + 0, /* ZeroBits */ + 0, /* CommitSize */ + NULL, /* SectionOffset */ + &cbMapping, + ViewUnmap, + fLargePages ? MEM_LARGE_PAGES : 0, + PAGE_READWRITE); + if (NT_SUCCESS(rcNt)) + { + /* + * Make the modifications. + */ + if (g_cVerbosity >= 2) + RTPrintf("debug: pvMapping=%p LB %#x\n", pvMapping, cbBuf); + + for (uint32_t iInner = 0;; iInner++) + { + if (iInner ? fCheckAfterFlush : fCheckFirst) + { + if (iInner == 0) + rc = CheckBuffer((uint32_t *)pvMapping, cbBuf, iRound - 1, 0); + else + rc = CheckBuffer((uint32_t *)pvMapping, cbBuf, iRound, cFlushes - iInner + 1); + if (RT_FAILURE(rc)) + { + RTMsgError("Round %u/%u: NtUnmapViewOfSection failed: %#x\n", iRound, rcNt); + break; + } + } + + UpdateBuffer((uint32_t *)pvMapping, cbBuf, iRound, cFlushes - iInner); + UpdateBuffer(pu32BufOrg, cbBuf, iRound, cFlushes - iInner); + + if (iInner >= cFlushes) + break; + + IO_STATUS_BLOCK Ios = RTNT_IO_STATUS_BLOCK_INITIALIZER; + SIZE_T cbBuf2 = cbBuf; + PVOID pvMapping2 = pvMapping; + rcNt = NtFlushVirtualMemory(NtCurrentProcess(), &pvMapping2, &cbBuf2, &Ios); + if (!NT_SUCCESS(rcNt)) + { + RTMsgError("Round %u: NtFlushVirtualMemory failed: %#x\n", iRound, rcNt); + rc = RTErrConvertFromNtStatus(rcNt); + break; + } + } + + /* + * Cleanup. + */ + rcNt = NtUnmapViewOfSection(NtCurrentProcess(), pvMapping); + if (!NT_SUCCESS(rcNt)) + { + RTMsgError("Round %u: NtUnmapViewOfSection failed: %#x\n", iRound, rcNt); + rc = RTErrConvertFromNtStatus(rcNt); + } + } + else + { + RTMsgError("Round %u: NtMapViewOfSection failed: %#x\n", iRound, rcNt); + rc = RTErrConvertFromNtStatus(rcNt); + } + + rcNt = NtClose(hSection); + if (!NT_SUCCESS(rcNt)) + { + RTMsgError("Round %u: NtClose(hSection) failed: %#x\n", iRound, rcNt); + rc = RTErrConvertFromNtStatus(rcNt); + } + } + else + { + RTMsgError("Round %u: NtCreateSection failed: %#x\n", iRound, rcNt); + rc = RTErrConvertFromNtStatus(rcNt); + } + } + else + RTMsgError("Round %u: Error opening file '%s' for memory mapping: %Rrc\n", iRound, pszFilename, rc); + return rc; +} + + +int main(int argc, char **argv) +{ + /* + * Init IPRT. + */ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Parse arguments. + */ + const char *pszFilename = NULL; + uint32_t cRounds = 4096; + uint32_t cPages = 128; + bool fLargePages = false; + + static const RTGETOPTDEF s_aOptions[] = + { + { "--rounds", 'r', RTGETOPT_REQ_UINT32 }, + { "--pages", 'p', RTGETOPT_REQ_UINT32 }, + { "--filename", 'f', RTGETOPT_REQ_STRING }, + { "--large-pages", 'l', RTGETOPT_REQ_NOTHING }, + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + }; + + RTGETOPTSTATE State; + RTGetOptInit(&State, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0); + RTGETOPTUNION ValueUnion; + int chOpt; + while ((chOpt = RTGetOpt(&State, &ValueUnion)) != 0) + { + switch (chOpt) + { + case 'r': cRounds = ValueUnion.u32; break; + case 'p': cPages = ValueUnion.u32; break; + case 'f': pszFilename = ValueUnion.psz; break; + case 'l': fLargePages = true; break; + case 'q': g_cVerbosity = 0; break; + case 'v': g_cVerbosity += 1; break; + case 'h': + RTPrintf("usage: ntFlushVirtualMemory [-c <times>] [-p <pages>] [-l|--large-pages] [-f <filename>]\n" + "\n" + "Aims at testing memory mapped files on NT w/ NtFlushVirtualMemory / FlushViewOfFile.\n"); + return 0; + + default: + return RTGetOptPrintError(chOpt, &ValueUnion); + } + } + + /* + * Allocate buffers and initialize the original with page numbers. + * + * We keep a original copy that gets updated in parallel to the memory + * mapping, allowing for simple file initialization and memcpy checking. + * + * The second buffer is for reading the file from disk and check. + */ + size_t const cbBuf = cPages * X86_PAGE_SIZE; + size_t const cItemsPerPage = X86_PAGE_SIZE / sizeof(uint32_t); + uint32_t *pu32BufOrg = (uint32_t *)RTMemPageAllocZ(cbBuf); + uint32_t *pu32BufChk = (uint32_t *)RTMemPageAllocZ(cbBuf); + if (pu32BufOrg == NULL || pu32BufChk == NULL) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to allocate two %zu sized buffers!\n", cbBuf); + + for (uint32_t iPage = 0; iPage < cPages; iPage++) + pu32BufOrg[iPage * cItemsPerPage + cItemsPerPage - 1] = MAKE_PAGE_SIGNATURE(iPage); + + rc = CheckBuffer(pu32BufOrg, cbBuf, 0, 0); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Internal error: CheckBuffer failed on virgin buffer: %Rrc\n", rc); + + /* + * Open the file and write out the orignal one. + */ + RTFILE hFile; + if (!pszFilename) + { + char *pszBuf = (char *)alloca(RTPATH_MAX); + rc = RTFileOpenTemp(&hFile, pszBuf, RTPATH_MAX, RTFILE_O_READWRITE | RTFILE_O_CREATE | RTFILE_O_DENY_NONE); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to create temporary file: %Rrc\n", rc); + pszFilename = pszBuf; + } + else + { + rc = RTFileOpen(&hFile, pszFilename, RTFILE_O_READWRITE | RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to open '%s': %Rrc\n", rc); + } + + RTEXITCODE rcExit = RTEXITCODE_SUCCESS; + + rc = RTFileWrite(hFile, pu32BufOrg, cbBuf, NULL); + if (RT_SUCCESS(rc)) + { + RTFileClose(hFile); + + /* + * Do the rounds. We count from 1 here to make verifying the previous round simpler. + */ + for (uint32_t iRound = 1; iRound <= cRounds; iRound++) + { + rc = MakeModifications(pszFilename, pu32BufOrg, cbBuf, iRound, + ((iRound >> 5) & 1) == 1, ((iRound >> 5) & 3) == 3, (iRound >> 3) & 31, fLargePages); + if (RT_SUCCESS(rc)) + { + rc = CheckBuffer(pu32BufOrg, cbBuf, iRound, 0); + if (RT_SUCCESS(rc)) + { + rc = CheckFile(pszFilename, pu32BufChk, pu32BufOrg, cbBuf, iRound); + if (RT_SUCCESS(rc)) + continue; + } + } + break; + } + } + else + { + rcExit = RTMsgErrorExit(RTEXITCODE_FAILURE, "Error writing initial %zu bytes to '%s': %Rrc\n", cbBuf, rc); + RTFileClose(hFile); + } + RTFileDelete(pszFilename); + return rcExit; +} + diff --git a/src/VBox/ValidationKit/utils/nt/ntsetfreq.cpp b/src/VBox/ValidationKit/utils/nt/ntsetfreq.cpp new file mode 100644 index 00000000..c2a936dd --- /dev/null +++ b/src/VBox/ValidationKit/utils/nt/ntsetfreq.cpp @@ -0,0 +1,161 @@ +/* $Id: ntsetfreq.cpp $ */ +/** @file + * Set the NT timer frequency. + */ + +/* + * Copyright (C) 2007-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/nt/nt.h> + +#include <iprt/initterm.h> +#include <iprt/getopt.h> +#include <iprt/message.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/thread.h> +#include <iprt/errcore.h> + + +int main(int argc, char **argv) +{ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Parse arguments. + */ + bool fVerbose = true; + uint32_t u32NewRes = 0; + uint32_t cSecsSleep = UINT32_MAX; + + static const RTGETOPTDEF s_aOptions[] = + { + { "--resolution", 'r', RTGETOPT_REQ_UINT32 }, + { "--sleep", 's', RTGETOPT_REQ_UINT32 }, + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + }; + + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0); + while ((rc = RTGetOpt(&GetState, &ValueUnion))) + { + switch (rc) + { + case 'r': + u32NewRes = ValueUnion.u32; + if (u32NewRes > 16*10000 /* 16 ms */ || u32NewRes < 1000 /* 100 microsec */) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, + "syntax error: the new timer resolution (%RU32) is out of range\n", + u32NewRes); + break; + + case 's': + cSecsSleep = ValueUnion.u32; + break; + + case 'q': + fVerbose = false; + break; + + case 'v': + fVerbose = true; + break; + + case 'h': + RTPrintf("Usage: ntsetfreq [-q|--quiet] [-v|--verbose] [-r|--resolution <100ns>] [-s|--sleep <1s>]\n"); + return RTEXITCODE_SUCCESS; + + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + + /* + * Query the current resolution. + */ + ULONG Cur = UINT32_MAX; + ULONG Min = UINT32_MAX; + ULONG Max = UINT32_MAX; + NTSTATUS rcNt = STATUS_SUCCESS; + if (fVerbose || !u32NewRes) + { + rcNt = NtQueryTimerResolution(&Min, &Max, &Cur); + if (NT_SUCCESS(rcNt)) + RTMsgInfo("cur: %u (%u.%02u Hz) min: %u (%u.%02u Hz) max: %u (%u.%02u Hz)\n", + Cur, 10000000 / Cur, (10000000 / (Cur * 100)) % 100, + Min, 10000000 / Min, (10000000 / (Min * 100)) % 100, + Max, 10000000 / Max, (10000000 / (Max * 100)) % 100); + else + RTMsgError("NTQueryTimerResolution failed with status %#x\n", rcNt); + } + + if (u32NewRes) + { + rcNt = NtSetTimerResolution(u32NewRes, TRUE, &Cur); + if (!NT_SUCCESS(rcNt)) + RTMsgError("NTSetTimerResolution(%RU32,,) failed with status %#x\n", u32NewRes, rcNt); + else if (fVerbose) + { + Cur = Min = Max = UINT32_MAX; + rcNt = NtQueryTimerResolution(&Min, &Max, &Cur); + if (NT_SUCCESS(rcNt)) + RTMsgInfo("new: %u (%u.%02u Hz) requested %RU32 (%u.%02u Hz)\n", + Cur, 10000000 / Cur, (10000000 / (Cur * 100)) % 100, + u32NewRes, 10000000 / u32NewRes, (10000000 / (u32NewRes * 100)) % 100); + else + RTMsgError("NTSetTimerResolution succeeded but the NTQueryTimerResolution call failed with status %#x (ignored)\n", + rcNt); + rcNt = STATUS_SUCCESS; + } + } + + if (u32NewRes && NT_SUCCESS(rcNt)) + { + if (cSecsSleep == UINT32_MAX) + for (;;) + RTThreadSleep(RT_INDEFINITE_WAIT); + else + while (cSecsSleep-- > 0) + RTThreadSleep(1000); + } + + return NT_SUCCESS(rcNt) ? 0 : 1; +} + diff --git a/src/VBox/ValidationKit/utils/nt/nttimesources.cpp b/src/VBox/ValidationKit/utils/nt/nttimesources.cpp new file mode 100644 index 00000000..dfa86b79 --- /dev/null +++ b/src/VBox/ValidationKit/utils/nt/nttimesources.cpp @@ -0,0 +1,246 @@ +/* $Id: nttimesources.cpp $ */ +/** @file + * Check the various time sources on Windows NT. + */ + +/* + * Copyright (C) 2009-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/win/windows.h> + +#include <iprt/asm.h> +#include <iprt/asm-amd64-x86.h> +#include <iprt/errcore.h> +#include <iprt/string.h> +#include <iprt/test.h> + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +typedef struct _MY_KSYSTEM_TIME +{ + ULONG LowPart; + LONG High1Time; + LONG High2Time; +} MY_KSYSTEM_TIME; + +typedef struct _MY_KUSER_SHARED_DATA +{ + ULONG TickCountLowDeprecated; + ULONG TickCountMultiplier; + volatile MY_KSYSTEM_TIME InterruptTime; + volatile MY_KSYSTEM_TIME SystemTime; + volatile MY_KSYSTEM_TIME TimeZoneBias; + /* The rest is not relevant. */ +} MY_KUSER_SHARED_DATA; + +/** The fixed pointer to the user shared data. */ +#define MY_USER_SHARED_DATA ((MY_KUSER_SHARED_DATA *)0x7ffe0000) + +/** Spins until GetTickCount() changes. */ +static void SpinUntilTick(void) +{ + /* spin till GetTickCount changes. */ + DWORD dwMsTick = GetTickCount(); + while (GetTickCount() == dwMsTick) + /* nothing */; +} + +/** Delay function that tries to return right after GetTickCount changed. */ +static void DelayMillies(DWORD dwMsStart, DWORD cMillies) +{ + /* Delay cMillies - 1. */ + Sleep(cMillies - 1); + while (GetTickCount() - dwMsStart < cMillies - 1U) + Sleep(1); + + SpinUntilTick(); +} + + +int main(int argc, char **argv) +{ + RT_NOREF1(argv); + + /* + * Init, create a test instance and "parse" arguments. + */ + RTTEST hTest; + int rc = RTTestInitAndCreate("nttimesources", &hTest); + if (rc) + return rc; + if (argc > 1) + { + RTTestFailed(hTest, "Syntax error! no arguments expected"); + return RTTestSummaryAndDestroy(hTest); + } + + /* + * Guess MHz using GetTickCount. + */ + RTTestSub(hTest, "Guess MHz"); + DWORD dwTickStart, dwTickEnd, cMsTicks; + uint64_t u64TscStart, u64TscEnd, cTscTicks; + + /* get a good start time. */ + SpinUntilTick(); + do + { + dwTickStart = GetTickCount(); + ASMCompilerBarrier(); + ASMSerializeInstruction(); + u64TscStart = ASMReadTSC(); + ASMCompilerBarrier(); + } while (GetTickCount() != dwTickStart); + + /* delay a good while. */ + DelayMillies(dwTickStart, 256); + + /* get a good end time. */ + do + { + dwTickEnd = GetTickCount(); + ASMCompilerBarrier(); + ASMSerializeInstruction(); + u64TscEnd = ASMReadTSC(); + ASMCompilerBarrier(); + } while (GetTickCount() != dwTickEnd); + cMsTicks = dwTickEnd - dwTickStart; + cTscTicks = u64TscEnd - u64TscStart; + + /* Calc an approximate TSC frequency: + cTscTicks / uTscHz = cMsTicks / 1000 + 1 / uTscHz = (cMsTicks / 1000) / cTscTicks + uTscHz = cTscTicks / (cMsTicks / 1000) */ + uint64_t u64TscHz = (long double)cTscTicks / ((long double)cMsTicks / 1000.0); + if ( u64TscHz > _1M*3 + && u64TscHz < _1T) + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, "u64TscHz=%'llu", u64TscHz); + else + { + RTTestFailed(hTest, "u64TscHz=%'llu - out of range", u64TscHz); + u64TscHz = 0; + } + + + /* + * Pit GetTickCount, InterruptTime, Performance Counters and TSC against each other. + */ + LARGE_INTEGER PrfHz; + LARGE_INTEGER PrfStart, PrfEnd, cPrfTicks; + LARGE_INTEGER IntStart, IntEnd, cIntTicks; + for (uint32_t i = 0; i < 7; i++) + { + RTTestSubF(hTest, "The whole bunch - pass #%u", i + 1); + + if (!QueryPerformanceFrequency(&PrfHz)) + { + RTTestFailed(hTest, "QueryPerformanceFrequency failed (%u)", GetLastError()); + return RTTestSummaryAndDestroy(hTest); + } + + /* get a good start time. */ + SpinUntilTick(); + do + { + IntStart.HighPart = MY_USER_SHARED_DATA->InterruptTime.High1Time; + IntStart.LowPart = MY_USER_SHARED_DATA->InterruptTime.LowPart; + dwTickStart = GetTickCount(); + if (!QueryPerformanceCounter(&PrfStart)) + { + RTTestFailed(hTest, "QueryPerformanceCounter failed (%u)", GetLastError()); + return RTTestSummaryAndDestroy(hTest); + } + ASMCompilerBarrier(); + ASMSerializeInstruction(); + u64TscStart = ASMReadTSC(); + ASMCompilerBarrier(); + } while ( MY_USER_SHARED_DATA->InterruptTime.High2Time != IntStart.HighPart + || MY_USER_SHARED_DATA->InterruptTime.LowPart != IntStart.LowPart + || GetTickCount() != dwTickStart); + + /* delay a good while. */ + DelayMillies(dwTickStart, 256); + + /* get a good end time. */ + do + { + IntEnd.HighPart = MY_USER_SHARED_DATA->InterruptTime.High1Time; + IntEnd.LowPart = MY_USER_SHARED_DATA->InterruptTime.LowPart; + dwTickEnd = GetTickCount(); + if (!QueryPerformanceCounter(&PrfEnd)) + { + RTTestFailed(hTest, "QueryPerformanceCounter failed (%u)", GetLastError()); + return RTTestSummaryAndDestroy(hTest); + } + ASMCompilerBarrier(); + ASMSerializeInstruction(); + u64TscEnd = ASMReadTSC(); + ASMCompilerBarrier(); + } while ( MY_USER_SHARED_DATA->InterruptTime.High2Time != IntEnd.HighPart + || MY_USER_SHARED_DATA->InterruptTime.LowPart != IntEnd.LowPart + || GetTickCount() != dwTickEnd); + + cMsTicks = dwTickEnd - dwTickStart; + cTscTicks = u64TscEnd - u64TscStart; + cIntTicks.QuadPart = IntEnd.QuadPart - IntStart.QuadPart; + cPrfTicks.QuadPart = PrfEnd.QuadPart - PrfStart.QuadPart; + + /* Recalc to micro seconds. */ + uint64_t u64MicroSecMs = (uint64_t)cMsTicks * 1000; + uint64_t u64MicroSecTsc = u64TscHz ? (long double)cTscTicks / (long double)u64TscHz * 1000000 : u64MicroSecMs; + uint64_t u64MicroSecPrf = (long double)cPrfTicks.QuadPart / (long double)PrfHz.QuadPart * 1000000; + uint64_t u64MicroSecInt = cIntTicks.QuadPart / 10; /* 100ns units*/ + + /* check how much they differ using the millisecond tick count as the standard candle. */ + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, " %9llu / %7lld us - GetTickCount\n", u64MicroSecMs, 0); + + int64_t off = u64MicroSecTsc - u64MicroSecMs; + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, " %9llu / %7lld us - TSC\n", u64MicroSecTsc, off); + RTTEST_CHECK(hTest, RT_ABS(off) < 50000 /*us*/); /* some extra uncertainty with TSC. */ + + off = u64MicroSecInt - u64MicroSecMs; + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, " %9llu / %7lld us - InterruptTime\n", u64MicroSecInt, off); + RTTEST_CHECK(hTest, RT_ABS(off) < 25000 /*us*/); + + off = u64MicroSecPrf - u64MicroSecMs; + RTTestPrintf(hTest, RTTESTLVL_ALWAYS, " %9llu / %7lld us - QueryPerformanceCounter\n", u64MicroSecPrf, off); + RTTEST_CHECK(hTest, RT_ABS(off) < 25000 /*us*/); + } + + return RTTestSummaryAndDestroy(hTest); +} + diff --git a/src/VBox/ValidationKit/utils/serial/Makefile.kmk b/src/VBox/ValidationKit/utils/serial/Makefile.kmk new file mode 100644 index 00000000..57c331fc --- /dev/null +++ b/src/VBox/ValidationKit/utils/serial/Makefile.kmk @@ -0,0 +1,49 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Serial port tests. +# + +# +# Copyright (C) 2017-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Serial port testing utility. +# +PROGRAMS += SerialTest +SerialTest_TEMPLATE = VBoxValidationKitR3 +SerialTest_SOURCES = SerialTest.cpp + +$(evalcall def_vbox_validationkit_process_python_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/serial/SerialTest.cpp b/src/VBox/ValidationKit/utils/serial/SerialTest.cpp new file mode 100644 index 00000000..4e7354c8 --- /dev/null +++ b/src/VBox/ValidationKit/utils/serial/SerialTest.cpp @@ -0,0 +1,1115 @@ +/* $Id: SerialTest.cpp $ */ +/** @file + * SerialTest - Serial port testing utility. + */ + +/* + * Copyright (C) 2017-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/errcore.h> +#include <iprt/getopt.h> +#include <iprt/mem.h> +#include <iprt/path.h> +#include <iprt/param.h> +#include <iprt/process.h> +#include <iprt/rand.h> +#include <iprt/serialport.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ + +/** Number of times to toggle the status lines during the test. */ +#define SERIALTEST_STS_LINE_TOGGLE_COUNT 100 + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + + +/** + * Serial test mode. + */ +typedef enum SERIALTESTMODE +{ + /** Invalid mode. */ + SERIALTESTMODE_INVALID = 0, + /** Serial port is looped back to itself */ + SERIALTESTMODE_LOOPBACK, + /** A secondary serial port is used with a null modem cable in between. */ + SERIALTESTMODE_SECONDARY, + /** The serial port is connected externally over which we have no control. */ + SERIALTESTMODE_EXTERNAL, + /** Usual 32bit hack. */ + SERIALTESTMODE_32BIT_HACK = 0x7fffffff +} SERIALTESTMODE; +/** Pointer to a serial test mode. */ +typedef SERIALTESTMODE *PSERIALTESTMDOE; + +/** Pointer to the serial test data instance. */ +typedef struct SERIALTEST *PSERIALTEST; + +/** + * Test callback function. + * + * @returns IPRT status code. + * @param pSerialTest The serial test instance data. + */ +typedef DECLCALLBACKTYPE(int, FNSERIALTESTRUN,(PSERIALTEST pSerialTest)); +/** Pointer to the serial test callback. */ +typedef FNSERIALTESTRUN *PFNSERIALTESTRUN; + + +/** + * The serial test instance data. + */ +typedef struct SERIALTEST +{ + /** The assigned test handle. */ + RTTEST hTest; + /** The assigned serial port. */ + RTSERIALPORT hSerialPort; + /** The currently active config. */ + PCRTSERIALPORTCFG pSerialCfg; +} SERIALTEST; + + +/** + * Test descriptor. + */ +typedef struct SERIALTESTDESC +{ + /** Test ID. */ + const char *pszId; + /** Test description. */ + const char *pszDesc; + /** Test run callback. */ + PFNSERIALTESTRUN pfnRun; +} SERIALTESTDESC; +/** Pointer to a test descriptor. */ +typedef SERIALTESTDESC *PSERIALTESTDESC; +/** Pointer to a constant test descriptor. */ +typedef const SERIALTESTDESC *PCSERIALTESTDESC; + + +/** + * TX/RX buffer containing a simple counter. + */ +typedef struct SERIALTESTTXRXBUFCNT +{ + /** The current counter value. */ + uint32_t iCnt; + /** Number of bytes left to receive/transmit. */ + size_t cbTxRxLeft; + /** The offset into the buffer to receive to/send from. */ + size_t offBuf; + /** Maximum size to send/receive before processing is needed again. */ + size_t cbTxRxMax; + /** The data buffer. */ + uint8_t abBuf[_1K]; +} SERIALTESTTXRXBUFCNT; +/** Pointer to a TX/RX buffer. */ +typedef SERIALTESTTXRXBUFCNT *PSERIALTESTTXRXBUFCNT; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + + +/** Command line parameters */ +static const RTGETOPTDEF g_aCmdOptions[] = +{ + {"--device", 'd', RTGETOPT_REQ_STRING }, + {"--baudrate", 'b', RTGETOPT_REQ_UINT32 }, + {"--parity", 'p', RTGETOPT_REQ_STRING }, + {"--databits", 'c', RTGETOPT_REQ_UINT32 }, + {"--stopbits", 's', RTGETOPT_REQ_STRING }, + {"--mode", 'm', RTGETOPT_REQ_STRING }, + {"--secondarydevice", 'l', RTGETOPT_REQ_STRING }, + {"--tests", 't', RTGETOPT_REQ_STRING }, + {"--txbytes", 'x', RTGETOPT_REQ_UINT32 }, + {"--abort-on-error", 'a', RTGETOPT_REQ_NOTHING}, + {"--verbose", 'v', RTGETOPT_REQ_NOTHING}, + {"--help", 'h', RTGETOPT_REQ_NOTHING} +}; + + +static DECLCALLBACK(int) serialTestRunReadWrite(PSERIALTEST pSerialTest); +static DECLCALLBACK(int) serialTestRunWrite(PSERIALTEST pSerialTest); +static DECLCALLBACK(int) serialTestRunReadVerify(PSERIALTEST pSerialTest); +static DECLCALLBACK(int) serialTestRunStsLines(PSERIALTEST pSerialTest); +static DECLCALLBACK(int) serialTestRunEcho(PSERIALTEST pSerialTest); + +/** Implemented tests. */ +static const SERIALTESTDESC g_aSerialTests[] = +{ + {"readwrite", "Simple Read/Write test on the same serial port", serialTestRunReadWrite }, + {"write", "Simple write test (verification done somewhere else)", serialTestRunWrite }, + {"readverify", "Counterpart to write test (reads and verifies data)", serialTestRunReadVerify }, + {"stslines", "Testing the status line setting and receiving", serialTestRunStsLines }, + {"echo", "Echoes received data back to the sender (not real test)", serialTestRunEcho }, +}; + +/** Verbosity value. */ +static unsigned g_cVerbosity = 0; +/** The test handle. */ +static RTTEST g_hTest = NIL_RTTEST; +/** The serial test mode. */ +static SERIALTESTMODE g_enmMode = SERIALTESTMODE_LOOPBACK; +/** Random number generator. */ +static RTRAND g_hRand = NIL_RTRAND; +/** The serial port handle. */ +static RTSERIALPORT g_hSerialPort = NIL_RTSERIALPORT; +/** The loopback serial port handle if configured. */ +static RTSERIALPORT g_hSerialPortSecondary = NIL_RTSERIALPORT; +/** Number of bytes to transmit for read/write tests. */ +static size_t g_cbTx = _1M; +/** Flag whether to abort the tool when encountering the first error. */ +static bool g_fAbortOnError = false; +/** The config used. */ +static RTSERIALPORTCFG g_SerialPortCfg = +{ + /* uBaudRate */ + 115200, + /* enmParity */ + RTSERIALPORTPARITY_NONE, + /* enmDataBitCount */ + RTSERIALPORTDATABITS_8BITS, + /* enmStopBitCount */ + RTSERIALPORTSTOPBITS_ONE +}; + + +/** + * RTTestFailed() wrapper which aborts the program if the option is set. + */ +static void serialTestFailed(RTTEST hTest, const char *pszFmt, ...) +{ + va_list va; + va_start(va, pszFmt); + RTTestFailedV(hTest, pszFmt, va); + va_end(va); + if (g_fAbortOnError) + RT_BREAKPOINT(); +} + + +/** + * Initializes a TX buffer. + * + * @param pSerBuf The serial buffer to initialize. + * @param cbTx Maximum number of bytes to transmit. + */ +static void serialTestTxBufInit(PSERIALTESTTXRXBUFCNT pSerBuf, size_t cbTx) +{ + pSerBuf->iCnt = 0; + pSerBuf->offBuf = 0; + pSerBuf->cbTxRxMax = 0; + pSerBuf->cbTxRxLeft = cbTx; + RT_ZERO(pSerBuf->abBuf); +} + + +/** + * Initializes a RX buffer. + * + * @param pSerBuf The serial buffer to initialize. + * @param cbRx Maximum number of bytes to receive. + */ +static void serialTestRxBufInit(PSERIALTESTTXRXBUFCNT pSerBuf, size_t cbRx) +{ + pSerBuf->iCnt = 0; + pSerBuf->offBuf = 0; + pSerBuf->cbTxRxMax = sizeof(pSerBuf->abBuf); + pSerBuf->cbTxRxLeft = cbRx; + RT_ZERO(pSerBuf->abBuf); +} + + +/** + * Prepares the given TX buffer with data for sending it out. + * + * @param pSerBuf The TX buffer pointer. + */ +static void serialTestTxBufPrepare(PSERIALTESTTXRXBUFCNT pSerBuf) +{ + /* Move the data to the front to make room at the end to fill. */ + if (pSerBuf->offBuf) + { + memmove(&pSerBuf->abBuf[0], &pSerBuf->abBuf[pSerBuf->offBuf], sizeof(pSerBuf->abBuf) - pSerBuf->offBuf); + pSerBuf->offBuf = 0; + } + + /* Fill up with data. */ + uint32_t offData = 0; + while (pSerBuf->cbTxRxMax + sizeof(uint32_t) <= sizeof(pSerBuf->abBuf)) + { + pSerBuf->iCnt++; + *(uint32_t *)&pSerBuf->abBuf[pSerBuf->offBuf + offData] = pSerBuf->iCnt; + pSerBuf->cbTxRxMax += sizeof(uint32_t); + offData += sizeof(uint32_t); + } +} + + +/** + * Sends a new batch of data from the TX buffer preapring new data if required. + * + * @returns IPRT status code. + * @param hSerialPort The serial port handle to send the data to. + * @param pSerBuf The TX buffer pointer. + */ +static int serialTestTxBufSend(RTSERIALPORT hSerialPort, PSERIALTESTTXRXBUFCNT pSerBuf) +{ + int rc = VINF_SUCCESS; + + if (pSerBuf->cbTxRxLeft) + { + if (!pSerBuf->cbTxRxMax) + serialTestTxBufPrepare(pSerBuf); + + size_t cbToWrite = RT_MIN(pSerBuf->cbTxRxMax, pSerBuf->cbTxRxLeft); + size_t cbWritten = 0; + rc = RTSerialPortWriteNB(hSerialPort, &pSerBuf->abBuf[pSerBuf->offBuf], cbToWrite, &cbWritten); + if (RT_SUCCESS(rc)) + { + pSerBuf->cbTxRxMax -= cbWritten; + pSerBuf->offBuf += cbWritten; + pSerBuf->cbTxRxLeft -= cbWritten; + } + } + + return rc; +} + + +/** + * Receives dat from the given serial port into the supplied RX buffer and does some validity checking. + * + * @returns IPRT status code. + * @param hSerialPort The serial port handle to receive data from. + * @param pSerBuf The RX buffer pointer. + */ +static int serialTestRxBufRecv(RTSERIALPORT hSerialPort, PSERIALTESTTXRXBUFCNT pSerBuf) +{ + int rc = VINF_SUCCESS; + + if (pSerBuf->cbTxRxLeft) + { + size_t cbToRead = RT_MIN(pSerBuf->cbTxRxMax, pSerBuf->cbTxRxLeft); + size_t cbRead = 0; + rc = RTSerialPortReadNB(hSerialPort, &pSerBuf->abBuf[pSerBuf->offBuf], cbToRead, &cbRead); + if (RT_SUCCESS(rc)) + { + pSerBuf->offBuf += cbRead; + pSerBuf->cbTxRxMax -= cbRead; + pSerBuf->cbTxRxLeft -= cbRead; + } + } + + return rc; +} + + +/** + * Verifies the data in the given RX buffer for correct transmission. + * + * @returns Flag whether verification failed. + * @param hTest The test handle to report errors to. + * @param pSerBuf The RX buffer pointer. + * @param iCntTx The current TX counter value the RX buffer should never get ahead of, + * UINT32_MAX disables this check. + */ +static bool serialTestRxBufVerify(RTTEST hTest, PSERIALTESTTXRXBUFCNT pSerBuf, uint32_t iCntTx) +{ + uint32_t offRx = 0; + bool fFailed = false; + + while (offRx + sizeof(uint32_t) < pSerBuf->offBuf) + { + uint32_t u32Val = *(uint32_t *)&pSerBuf->abBuf[offRx]; + offRx += sizeof(uint32_t); + + if (RT_UNLIKELY(u32Val != ++pSerBuf->iCnt)) + { + fFailed = true; + if (g_cVerbosity > 0) + serialTestFailed(hTest, "Data corruption/loss detected, expected counter value %u got %u\n", + pSerBuf->iCnt, u32Val); + } + } + + if (RT_UNLIKELY(pSerBuf->iCnt > iCntTx)) + { + fFailed = true; + serialTestFailed(hTest, "Overtook the send buffer, expected maximum counter value %u got %u\n", + iCntTx, pSerBuf->iCnt); + } + + /* Remove processed data from the buffer and move the rest to the front. */ + if (offRx) + { + memmove(&pSerBuf->abBuf[0], &pSerBuf->abBuf[offRx], sizeof(pSerBuf->abBuf) - offRx); + pSerBuf->offBuf -= offRx; + pSerBuf->cbTxRxMax += offRx; + } + + return fFailed; +} + + +DECLINLINE(bool) serialTestRndTrue(void) +{ + return RTRandAdvU32Ex(g_hRand, 0, 1) == 1; +} + + +/** + * Runs a simple read/write test. + * + * @returns IPRT status code. + * @param pSerialTest The serial test configuration. + */ +static DECLCALLBACK(int) serialTestRunReadWrite(PSERIALTEST pSerialTest) +{ + uint64_t tsStart = RTTimeNanoTS(); + bool fFailed = false; + SERIALTESTTXRXBUFCNT SerBufTx; + SERIALTESTTXRXBUFCNT SerBufRx; + + serialTestTxBufInit(&SerBufTx, g_cbTx); + serialTestRxBufInit(&SerBufRx, g_cbTx); + + int rc = serialTestTxBufSend(pSerialTest->hSerialPort, &SerBufTx); + while ( RT_SUCCESS(rc) + && ( SerBufTx.cbTxRxLeft + || SerBufRx.cbTxRxLeft)) + { + uint32_t fEvts = 0; + uint32_t fEvtsQuery = 0; + if (SerBufTx.cbTxRxLeft) + fEvtsQuery |= RTSERIALPORT_EVT_F_DATA_TX; + if (SerBufRx.cbTxRxLeft) + fEvtsQuery |= RTSERIALPORT_EVT_F_DATA_RX; + + rc = RTSerialPortEvtPoll(pSerialTest->hSerialPort, fEvtsQuery, &fEvts, RT_INDEFINITE_WAIT); + if (RT_FAILURE(rc)) + break; + + if (fEvts & RTSERIALPORT_EVT_F_DATA_RX) + { + rc = serialTestRxBufRecv(pSerialTest->hSerialPort, &SerBufRx); + if (RT_FAILURE(rc)) + break; + + bool fRes = serialTestRxBufVerify(pSerialTest->hTest, &SerBufRx, SerBufTx.iCnt); + if (fRes && !fFailed) + { + fFailed = true; + serialTestFailed(pSerialTest->hTest, "Data corruption/loss detected\n"); + } + } + if ( RT_SUCCESS(rc) + && (fEvts & RTSERIALPORT_EVT_F_DATA_TX)) + rc = serialTestTxBufSend(pSerialTest->hSerialPort, &SerBufTx); + } + + uint64_t tsRuntime = RTTimeNanoTS() - tsStart; + size_t cNsPerByte = tsRuntime / g_cbTx; + uint64_t cbBytesPerSec = RT_NS_1SEC / cNsPerByte; + RTTestValue(pSerialTest->hTest, "Throughput", cbBytesPerSec, RTTESTUNIT_BYTES_PER_SEC); + + return rc; +} + + +/** + * Runs a simple write test without doing any verification. + * + * @returns IPRT status code. + * @param pSerialTest The serial test configuration. + */ +static DECLCALLBACK(int) serialTestRunWrite(PSERIALTEST pSerialTest) +{ + uint64_t tsStart = RTTimeNanoTS(); + SERIALTESTTXRXBUFCNT SerBufTx; + + serialTestTxBufInit(&SerBufTx, g_cbTx); + + int rc = serialTestTxBufSend(pSerialTest->hSerialPort, &SerBufTx); + while ( RT_SUCCESS(rc) + && SerBufTx.cbTxRxLeft) + { + uint32_t fEvts = 0; + + rc = RTSerialPortEvtPoll(pSerialTest->hSerialPort, RTSERIALPORT_EVT_F_DATA_TX, &fEvts, RT_INDEFINITE_WAIT); + if (RT_FAILURE(rc)) + break; + + if (fEvts & RTSERIALPORT_EVT_F_DATA_TX) + rc = serialTestTxBufSend(pSerialTest->hSerialPort, &SerBufTx); + } + + uint64_t tsRuntime = RTTimeNanoTS() - tsStart; + size_t cNsPerByte = tsRuntime / g_cbTx; + uint64_t cbBytesPerSec = RT_NS_1SEC / cNsPerByte; + RTTestValue(pSerialTest->hTest, "Throughput", cbBytesPerSec, RTTESTUNIT_BYTES_PER_SEC); + + return rc; +} + + +/** + * Runs the counterpart to the write test, reading and verifying data. + * + * @returns IPRT status code. + * @param pSerialTest The serial test configuration. + */ +static DECLCALLBACK(int) serialTestRunReadVerify(PSERIALTEST pSerialTest) +{ + int rc = VINF_SUCCESS; + uint64_t tsStart = RTTimeNanoTS(); + bool fFailed = false; + SERIALTESTTXRXBUFCNT SerBufRx; + + serialTestRxBufInit(&SerBufRx, g_cbTx); + + while ( RT_SUCCESS(rc) + && SerBufRx.cbTxRxLeft) + { + uint32_t fEvts = 0; + uint32_t fEvtsQuery = RTSERIALPORT_EVT_F_DATA_RX; + + rc = RTSerialPortEvtPoll(pSerialTest->hSerialPort, fEvtsQuery, &fEvts, RT_INDEFINITE_WAIT); + if (RT_FAILURE(rc)) + break; + + if (fEvts & RTSERIALPORT_EVT_F_DATA_RX) + { + rc = serialTestRxBufRecv(pSerialTest->hSerialPort, &SerBufRx); + if (RT_FAILURE(rc)) + break; + + bool fRes = serialTestRxBufVerify(pSerialTest->hTest, &SerBufRx, UINT32_MAX); + if (fRes && !fFailed) + { + fFailed = true; + serialTestFailed(pSerialTest->hTest, "Data corruption/loss detected\n"); + } + } + } + + uint64_t tsRuntime = RTTimeNanoTS() - tsStart; + size_t cNsPerByte = tsRuntime / g_cbTx; + uint64_t cbBytesPerSec = RT_NS_1SEC / cNsPerByte; + RTTestValue(pSerialTest->hTest, "Throughput", cbBytesPerSec, RTTESTUNIT_BYTES_PER_SEC); + + return rc; +} + + +/** + * Tests setting status lines and getting notified about status line changes. + * + * @returns IPRT status code. + * @param pSerialTest The serial test configuration. + */ +static DECLCALLBACK(int) serialTestRunStsLines(PSERIALTEST pSerialTest) +{ + int rc = VINF_SUCCESS; + + if (g_enmMode == SERIALTESTMODE_LOOPBACK) + { + uint32_t fStsLinesQueriedOld = 0; + + rc = RTSerialPortChgStatusLines(pSerialTest->hSerialPort, + RTSERIALPORT_CHG_STS_LINES_F_RTS | RTSERIALPORT_CHG_STS_LINES_F_DTR, + 0); + if (RT_SUCCESS(rc)) + { + rc = RTSerialPortQueryStatusLines(pSerialTest->hSerialPort, &fStsLinesQueriedOld); + if (RT_SUCCESS(rc)) + { + /* Everything should be clear at this stage. */ + if (!fStsLinesQueriedOld) + { + uint32_t fStsLinesSetOld = 0; + + for (uint32_t i = 0; i < SERIALTEST_STS_LINE_TOGGLE_COUNT; i++) + { + uint32_t fStsLinesSet = 0; + uint32_t fStsLinesClear = 0; + + /* Change RTS? */ + if (serialTestRndTrue()) + { + /* Clear, if set previously otherwise set it. */ + if (fStsLinesSetOld & RTSERIALPORT_CHG_STS_LINES_F_RTS) + fStsLinesClear |= RTSERIALPORT_CHG_STS_LINES_F_RTS; + else + fStsLinesSet |= RTSERIALPORT_CHG_STS_LINES_F_RTS; + } + + /* Change DTR? */ + if (serialTestRndTrue()) + { + /* Clear, if set previously otherwise set it. */ + if (fStsLinesSetOld & RTSERIALPORT_CHG_STS_LINES_F_DTR) + fStsLinesClear |= RTSERIALPORT_CHG_STS_LINES_F_DTR; + else + fStsLinesSet |= RTSERIALPORT_CHG_STS_LINES_F_DTR; + } + + rc = RTSerialPortChgStatusLines(pSerialTest->hSerialPort, fStsLinesClear, fStsLinesSet); + if (RT_FAILURE(rc)) + { + serialTestFailed(g_hTest, "Changing status lines failed with %Rrc on iteration %u (fSet=%#x fClear=%#x)\n", + rc, i, fStsLinesSet, fStsLinesClear); + break; + } + + /* Wait for status line monitor event. */ + uint32_t fEvtsRecv = 0; + rc = RTSerialPortEvtPoll(pSerialTest->hSerialPort, RTSERIALPORT_EVT_F_STATUS_LINE_CHANGED, + &fEvtsRecv, RT_MS_1SEC); + if ( RT_FAILURE(rc) + && (rc != VERR_TIMEOUT && !fStsLinesSet && !fStsLinesClear)) + { + serialTestFailed(g_hTest, "Waiting for status line change failed with %Rrc on iteration %u\n", + rc, i); + break; + } + + uint32_t fStsLinesQueried = 0; + rc = RTSerialPortQueryStatusLines(pSerialTest->hSerialPort, &fStsLinesQueried); + if (RT_FAILURE(rc)) + { + serialTestFailed(g_hTest, "Querying status lines failed with %Rrc on iteration %u\n", + rc, i); + break; + } + + /* Compare expected and real result. */ + if ( (fStsLinesQueried & RTSERIALPORT_STS_LINE_DSR) + != (fStsLinesQueriedOld & RTSERIALPORT_STS_LINE_DSR)) + { + if ( (fStsLinesQueried & RTSERIALPORT_STS_LINE_DSR) + && !(fStsLinesSet & RTSERIALPORT_CHG_STS_LINES_F_DTR)) + serialTestFailed(g_hTest, "DSR line got set when it shouldn't be on iteration %u\n", i); + else if ( !(fStsLinesQueried & RTSERIALPORT_STS_LINE_DSR) + && !(fStsLinesClear & RTSERIALPORT_CHG_STS_LINES_F_DTR)) + serialTestFailed(g_hTest, "DSR line got cleared when it shouldn't be on iteration %u\n", i); + } + else if ( (fStsLinesSet & RTSERIALPORT_CHG_STS_LINES_F_DTR) + || (fStsLinesClear & RTSERIALPORT_CHG_STS_LINES_F_DTR)) + serialTestFailed(g_hTest, "DSR line didn't change when it should have on iteration %u\n", i); + + if ( (fStsLinesQueried & RTSERIALPORT_STS_LINE_DCD) + != (fStsLinesQueriedOld & RTSERIALPORT_STS_LINE_DCD)) + { + if ( (fStsLinesQueried & RTSERIALPORT_STS_LINE_DCD) + && !(fStsLinesSet & RTSERIALPORT_CHG_STS_LINES_F_DTR)) + serialTestFailed(g_hTest, "DCD line got set when it shouldn't be on iteration %u\n", i); + else if ( !(fStsLinesQueried & RTSERIALPORT_STS_LINE_DCD) + && !(fStsLinesClear & RTSERIALPORT_CHG_STS_LINES_F_DTR)) + serialTestFailed(g_hTest, "DCD line got cleared when it shouldn't be on iteration %u\n", i); + } + else if ( (fStsLinesSet & RTSERIALPORT_CHG_STS_LINES_F_DTR) + || (fStsLinesClear & RTSERIALPORT_CHG_STS_LINES_F_DTR)) + serialTestFailed(g_hTest, "DCD line didn't change when it should have on iteration %u\n", i); + + if ( (fStsLinesQueried & RTSERIALPORT_STS_LINE_CTS) + != (fStsLinesQueriedOld & RTSERIALPORT_STS_LINE_CTS)) + { + if ( (fStsLinesQueried & RTSERIALPORT_STS_LINE_CTS) + && !(fStsLinesSet & RTSERIALPORT_CHG_STS_LINES_F_RTS)) + serialTestFailed(g_hTest, "CTS line got set when it shouldn't be on iteration %u\n", i); + else if ( !(fStsLinesQueried & RTSERIALPORT_STS_LINE_CTS) + && !(fStsLinesClear & RTSERIALPORT_CHG_STS_LINES_F_RTS)) + serialTestFailed(g_hTest, "CTS line got cleared when it shouldn't be on iteration %u\n", i); + } + else if ( (fStsLinesSet & RTSERIALPORT_CHG_STS_LINES_F_RTS) + || (fStsLinesClear & RTSERIALPORT_CHG_STS_LINES_F_RTS)) + serialTestFailed(g_hTest, "CTS line didn't change when it should have on iteration %u\n", i); + + if (RTTestErrorCount(g_hTest) > 0) + break; + + fStsLinesSetOld |= fStsLinesSet; + fStsLinesSetOld &= ~fStsLinesClear; + fStsLinesQueriedOld = fStsLinesQueried; + } + } + else + serialTestFailed(g_hTest, "Status lines active which should be clear (%#x, but expected %#x)\n", + fStsLinesQueriedOld, 0); + } + else + serialTestFailed(g_hTest, "Querying status lines failed with %Rrc\n", rc); + } + else + serialTestFailed(g_hTest, "Clearing status lines failed with %Rrc\n", rc); + } + else + rc = VERR_NOT_IMPLEMENTED; + + return rc; +} + + +/** + * Runs a simple echo service (not a real test on its own). + * + * @returns IPRT status code. + * @param pSerialTest The serial test configuration. + */ +static DECLCALLBACK(int) serialTestRunEcho(PSERIALTEST pSerialTest) +{ + int rc = VINF_SUCCESS; + uint64_t tsStart = RTTimeNanoTS(); + uint8_t abBuf[_1K]; + size_t cbLeft = g_cbTx; + size_t cbInBuf = 0; + + while ( RT_SUCCESS(rc) + && ( cbLeft + || cbInBuf)) + { + uint32_t fEvts = 0; + uint32_t fEvtsQuery = 0; + if (cbInBuf) + fEvtsQuery |= RTSERIALPORT_EVT_F_DATA_TX; + if (cbLeft && cbInBuf < sizeof(abBuf)) + fEvtsQuery |= RTSERIALPORT_EVT_F_DATA_RX; + + rc = RTSerialPortEvtPoll(pSerialTest->hSerialPort, fEvtsQuery, &fEvts, RT_INDEFINITE_WAIT); + if (RT_FAILURE(rc)) + break; + + if (fEvts & RTSERIALPORT_EVT_F_DATA_RX) + { + size_t cbThisRead = RT_MIN(cbLeft, sizeof(abBuf) - cbInBuf); + size_t cbRead = 0; + rc = RTSerialPortReadNB(pSerialTest->hSerialPort, &abBuf[cbInBuf], cbThisRead, &cbRead); + if (RT_SUCCESS(rc)) + { + cbInBuf += cbRead; + cbLeft -= cbRead; + } + else if (RT_FAILURE(rc)) + break; + } + + if (fEvts & RTSERIALPORT_EVT_F_DATA_TX) + { + size_t cbWritten = 0; + rc = RTSerialPortWriteNB(pSerialTest->hSerialPort, &abBuf[0], cbInBuf, &cbWritten); + if (RT_SUCCESS(rc)) + { + memmove(&abBuf[0], &abBuf[cbWritten], cbInBuf - cbWritten); + cbInBuf -= cbWritten; + } + } + } + + uint64_t tsRuntime = RTTimeNanoTS() - tsStart; + size_t cNsPerByte = tsRuntime / g_cbTx; + uint64_t cbBytesPerSec = RT_NS_1SEC / cNsPerByte; + RTTestValue(pSerialTest->hTest, "Throughput", cbBytesPerSec, RTTESTUNIT_BYTES_PER_SEC); + + return rc; +} + + +/** + * Returns an array of test descriptors get from the given string. + * + * @returns Pointer to the array of test descriptors. + * @param pszTests The string containing the tests separated with ':'. + */ +static PSERIALTESTDESC serialTestSelectFromCmdLine(const char *pszTests) +{ + size_t cTests = 1; + + const char *pszNext = strchr(pszTests, ':'); + while (pszNext) + { + pszNext++; + cTests++; + pszNext = strchr(pszNext, ':'); + } + + PSERIALTESTDESC paTests = (PSERIALTESTDESC)RTMemAllocZ((cTests + 1) * sizeof(SERIALTESTDESC)); + if (RT_LIKELY(paTests)) + { + uint32_t iTest = 0; + + pszNext = strchr(pszTests, ':'); + while (pszNext) + { + bool fFound = false; + + pszNext++; /* Skip : character. */ + + for (unsigned i = 0; i < RT_ELEMENTS(g_aSerialTests); i++) + { + if (!RTStrNICmp(pszTests, g_aSerialTests[i].pszId, pszNext - pszTests - 1)) + { + memcpy(&paTests[iTest], &g_aSerialTests[i], sizeof(SERIALTESTDESC)); + fFound = true; + break; + } + } + + if (RT_UNLIKELY(!fFound)) + { + RTPrintf("Testcase \"%.*s\" not known\n", pszNext - pszTests - 1, pszTests); + RTMemFree(paTests); + return NULL; + } + + pszTests = pszNext; + pszNext = strchr(pszTests, ':'); + } + + /* Fill last descriptor. */ + bool fFound = false; + for (unsigned i = 0; i < RT_ELEMENTS(g_aSerialTests); i++) + { + if (!RTStrICmp(pszTests, g_aSerialTests[i].pszId)) + { + memcpy(&paTests[iTest], &g_aSerialTests[i], sizeof(SERIALTESTDESC)); + fFound = true; + break; + } + } + + if (RT_UNLIKELY(!fFound)) + { + RTPrintf("Testcase \"%s\" not known\n", pszTests); + RTMemFree(paTests); + paTests = NULL; + } + } + else + RTPrintf("Failed to allocate test descriptors for %u selected tests\n", cTests); + + return paTests; +} + + +/** + * Shows tool usage text. + */ +static void serialTestUsage(PRTSTREAM pStrm) +{ + char szExec[RTPATH_MAX]; + RTStrmPrintf(pStrm, "usage: %s [options]\n", + RTPathFilename(RTProcGetExecutablePath(szExec, sizeof(szExec)))); + RTStrmPrintf(pStrm, "\n"); + RTStrmPrintf(pStrm, "options: \n"); + + + for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) + { + const char *pszHelp; + switch (g_aCmdOptions[i].iShort) + { + case 'h': + pszHelp = "Displays this help and exit"; + break; + case 'd': + pszHelp = "Use the specified serial port device"; + break; + case 'b': + pszHelp = "Use the given baudrate"; + break; + case 'p': + pszHelp = "Use the given parity, valid modes are: none, even, odd, mark, space"; + break; + case 'c': + pszHelp = "Use the given data bitcount, valid are: 5, 6, 7, 8"; + break; + case 's': + pszHelp = "Use the given stop bitcount, valid are: 1, 1.5, 2"; + break; + case 'm': + pszHelp = "Mode of the serial port, valid are: loopback, secondary, external"; + break; + case 'l': + pszHelp = "Use the given serial port device as the secondary device"; + break; + case 't': + pszHelp = "The tests to run separated by ':'"; + break; + case 'x': + pszHelp = "Number of bytes to transmit during read/write tests"; + break; + default: + pszHelp = "Option undocumented"; + break; + } + char szOpt[256]; + RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); + RTStrmPrintf(pStrm, " %-30s%s\n", szOpt, pszHelp); + } +} + + +int main(int argc, char *argv[]) +{ + /* + * Init IPRT and globals. + */ + int rc = RTTestInitAndCreate("SerialTest", &g_hTest); + if (rc) + return rc; + + /* + * Default values. + */ + const char *pszDevice = NULL; + const char *pszDeviceSecondary = NULL; + PSERIALTESTDESC paTests = NULL; + + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); + while ((rc = RTGetOpt(&GetState, &ValueUnion))) + { + switch (rc) + { + case 'h': + serialTestUsage(g_pStdOut); + return RTEXITCODE_SUCCESS; + case 'v': + g_cVerbosity++; + break; + case 'd': + pszDevice = ValueUnion.psz; + break; + case 'l': + pszDeviceSecondary = ValueUnion.psz; + break; + case 'b': + g_SerialPortCfg.uBaudRate = ValueUnion.u32; + break; + case 'p': + if (!RTStrICmp(ValueUnion.psz, "none")) + g_SerialPortCfg.enmParity = RTSERIALPORTPARITY_NONE; + else if (!RTStrICmp(ValueUnion.psz, "even")) + g_SerialPortCfg.enmParity = RTSERIALPORTPARITY_EVEN; + else if (!RTStrICmp(ValueUnion.psz, "odd")) + g_SerialPortCfg.enmParity = RTSERIALPORTPARITY_ODD; + else if (!RTStrICmp(ValueUnion.psz, "mark")) + g_SerialPortCfg.enmParity = RTSERIALPORTPARITY_MARK; + else if (!RTStrICmp(ValueUnion.psz, "space")) + g_SerialPortCfg.enmParity = RTSERIALPORTPARITY_SPACE; + else + { + RTPrintf("Unknown parity \"%s\" given\n", ValueUnion.psz); + return RTEXITCODE_FAILURE; + } + break; + case 'c': + if (ValueUnion.u32 == 5) + g_SerialPortCfg.enmDataBitCount = RTSERIALPORTDATABITS_5BITS; + else if (ValueUnion.u32 == 6) + g_SerialPortCfg.enmDataBitCount = RTSERIALPORTDATABITS_6BITS; + else if (ValueUnion.u32 == 7) + g_SerialPortCfg.enmDataBitCount = RTSERIALPORTDATABITS_7BITS; + else if (ValueUnion.u32 == 8) + g_SerialPortCfg.enmDataBitCount = RTSERIALPORTDATABITS_8BITS; + else + { + RTPrintf("Unknown data bitcount \"%u\" given\n", ValueUnion.u32); + return RTEXITCODE_FAILURE; + } + break; + case 's': + if (!RTStrICmp(ValueUnion.psz, "1")) + g_SerialPortCfg.enmStopBitCount = RTSERIALPORTSTOPBITS_ONE; + else if (!RTStrICmp(ValueUnion.psz, "1.5")) + g_SerialPortCfg.enmStopBitCount = RTSERIALPORTSTOPBITS_ONEPOINTFIVE; + else if (!RTStrICmp(ValueUnion.psz, "2")) + g_SerialPortCfg.enmStopBitCount = RTSERIALPORTSTOPBITS_TWO; + else + { + RTPrintf("Unknown stop bitcount \"%s\" given\n", ValueUnion.psz); + return RTEXITCODE_FAILURE; + } + break; + case 'm': + if (!RTStrICmp(ValueUnion.psz, "loopback")) + g_enmMode = SERIALTESTMODE_LOOPBACK; + else if (!RTStrICmp(ValueUnion.psz, "secondary")) + g_enmMode = SERIALTESTMODE_SECONDARY; + else if (!RTStrICmp(ValueUnion.psz, "external")) + g_enmMode = SERIALTESTMODE_EXTERNAL; + else + { + RTPrintf("Unknown serial test mode \"%s\" given\n", ValueUnion.psz); + return RTEXITCODE_FAILURE; + } + break; + case 't': + paTests = serialTestSelectFromCmdLine(ValueUnion.psz); + if (!paTests) + return RTEXITCODE_FAILURE; + break; + case 'x': + g_cbTx = ValueUnion.u32; + break; + case 'a': + g_fAbortOnError = true; + break; + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + if (g_enmMode == SERIALTESTMODE_SECONDARY && !pszDeviceSecondary) + { + RTPrintf("Mode set to secondary device but no secondary device given\n"); + return RTEXITCODE_FAILURE; + } + + if (!paTests) + { + /* Select all. */ + paTests = (PSERIALTESTDESC)RTMemAllocZ((RT_ELEMENTS(g_aSerialTests) + 1) * sizeof(SERIALTESTDESC)); + if (RT_UNLIKELY(!paTests)) + { + RTPrintf("Failed to allocate memory for test descriptors\n"); + return RTEXITCODE_FAILURE; + } + memcpy(paTests, &g_aSerialTests[0], RT_ELEMENTS(g_aSerialTests) * sizeof(SERIALTESTDESC)); + } + + rc = RTRandAdvCreateParkMiller(&g_hRand); + if (RT_FAILURE(rc)) + { + RTPrintf("Failed to create random number generator: %Rrc\n", rc); + return RTEXITCODE_FAILURE; + } + + rc = RTRandAdvSeed(g_hRand, UINT64_C(0x123456789abcdef)); + AssertRC(rc); + + /* + * Start testing. + */ + RTTestBanner(g_hTest); + + if (pszDevice) + { + uint32_t fFlags = RTSERIALPORT_OPEN_F_READ + | RTSERIALPORT_OPEN_F_WRITE + | RTSERIALPORT_OPEN_F_SUPPORT_STATUS_LINE_MONITORING; + + RTTestSub(g_hTest, "Opening device"); + rc = RTSerialPortOpen(&g_hSerialPort, pszDevice, fFlags); + if (RT_SUCCESS(rc)) + { + if (g_enmMode == SERIALTESTMODE_SECONDARY) + { + RTTestSub(g_hTest, "Opening secondary device"); + rc = RTSerialPortOpen(&g_hSerialPortSecondary, pszDeviceSecondary, fFlags); + if (RT_FAILURE(rc)) + serialTestFailed(g_hTest, "Opening secondary device \"%s\" failed with %Rrc\n", pszDevice, rc); + } + + if (RT_SUCCESS(rc)) + { + RTTestSub(g_hTest, "Setting serial port configuration"); + + rc = RTSerialPortCfgSet(g_hSerialPort, &g_SerialPortCfg ,NULL); + if (RT_SUCCESS(rc)) + { + if (g_enmMode == SERIALTESTMODE_SECONDARY) + { + RTTestSub(g_hTest, "Setting serial port configuration for secondary device"); + rc = RTSerialPortCfgSet(g_hSerialPortSecondary, &g_SerialPortCfg, NULL); + if (RT_FAILURE(rc)) + serialTestFailed(g_hTest, "Setting configuration of secondary device \"%s\" failed with %Rrc\n", pszDevice, rc); + } + + if (RT_SUCCESS(rc)) + { + SERIALTEST Test; + PSERIALTESTDESC pTest = &paTests[0]; + + Test.hTest = g_hTest; + Test.hSerialPort = g_hSerialPort; + Test.pSerialCfg = &g_SerialPortCfg; + + while (pTest->pszId) + { + RTTestSub(g_hTest, pTest->pszDesc); + rc = pTest->pfnRun(&Test); + if ( RT_FAILURE(rc) + || RTTestErrorCount(g_hTest) > 0) + serialTestFailed(g_hTest, "Running test \"%s\" failed (%Rrc, cErrors=%u)\n", + pTest->pszId, rc, RTTestErrorCount(g_hTest)); + + RTTestSubDone(g_hTest); + pTest++; + } + } + } + else + serialTestFailed(g_hTest, "Setting configuration of device \"%s\" failed with %Rrc\n", pszDevice, rc); + + RTSerialPortClose(g_hSerialPort); + } + } + else + serialTestFailed(g_hTest, "Opening device \"%s\" failed with %Rrc\n", pszDevice, rc); + } + else + serialTestFailed(g_hTest, "No device given on command line\n"); + + RTRandAdvDestroy(g_hRand); + RTMemFree(paTests); + RTEXITCODE rcExit = RTTestSummaryAndDestroy(g_hTest); + return rcExit; +} + diff --git a/src/VBox/ValidationKit/utils/storage/IoPerf.cpp b/src/VBox/ValidationKit/utils/storage/IoPerf.cpp new file mode 100644 index 00000000..db3b7211 --- /dev/null +++ b/src/VBox/ValidationKit/utils/storage/IoPerf.cpp @@ -0,0 +1,1405 @@ +/* $Id: IoPerf.cpp $ */ +/** @file + * IoPerf - Storage I/O Performance Benchmark. + */ + +/* + * Copyright (C) 2019-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/err.h> +#include <iprt/dir.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/initterm.h> +#include <iprt/ioqueue.h> +#include <iprt/list.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/param.h> +#include <iprt/path.h> +#include <iprt/process.h> +#include <iprt/rand.h> +#include <iprt/string.h> +#include <iprt/stream.h> +#include <iprt/system.h> +#include <iprt/test.h> +#include <iprt/time.h> +#include <iprt/thread.h> +#include <iprt/zero.h> + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ + +/** Size multiplier for the random data buffer to seek around. */ +#define IOPERF_RAND_DATA_BUF_FACTOR 3 + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + +/** Forward declaration of the master. */ +typedef struct IOPERFMASTER *PIOPERFMASTER; + +/** + * I/O perf supported tests. + */ +typedef enum IOPERFTEST +{ + /** Invalid test handle. */ + IOPERFTEST_INVALID = 0, + /** The test was disabled. */ + IOPERFTEST_DISABLED, + IOPERFTEST_FIRST_WRITE, + IOPERFTEST_SEQ_READ, + IOPERFTEST_SEQ_WRITE, + IOPERFTEST_REV_READ, + IOPERFTEST_REV_WRITE, + IOPERFTEST_RND_READ, + IOPERFTEST_RND_WRITE, + IOPERFTEST_SEQ_READWRITE, + IOPERFTEST_RND_READWRITE, + /** Special shutdown test which lets the workers exit, must be LAST. */ + IOPERFTEST_SHUTDOWN, + IOPERFTEST_32BIT_HACK = 0x7fffffff +} IOPERFTEST; + + +/** + * I/O perf test set preparation method. + */ +typedef enum IOPERFTESTSETPREP +{ + IOPERFTESTSETPREP_INVALID = 0, + /** Just create the file and don't set any sizes. */ + IOPERFTESTSETPREP_JUST_CREATE, + /** Standard RTFileSetSize() call which might create a sparse file. */ + IOPERFTESTSETPREP_SET_SZ, + /** Uses RTFileSetAllocationSize() to ensure storage is allocated for the file. */ + IOPERFTESTSETPREP_SET_ALLOC_SZ, + /** 32bit hack. */ + IOPERFTESTSETPREP_32BIT_HACK = 0x7fffffff +} IOPERFTESTSETPREP; + + +/** + * Statistics values for a single request kept around until the + * test completed for statistics collection. + */ +typedef struct IOPERFREQSTAT +{ + /** Start timestamp for the request. */ + uint64_t tsStart; + /** Completion timestamp for the request. */ + uint64_t tsComplete; +} IOPERFREQSTAT; +/** Pointer to a request statistics record. */ +typedef IOPERFREQSTAT *PIOPERFREQSTAT; + + +/** + * I/O perf request. + */ +typedef struct IOPERFREQ +{ + /** Request operation code. */ + RTIOQUEUEOP enmOp; + /** Start offset. */ + uint64_t offXfer; + /** Transfer size for the request. */ + size_t cbXfer; + /** The buffer used for the transfer. */ + void *pvXfer; + /** This is the statically assigned destination buffer for read requests for this request. */ + void *pvXferRead; + /** Size of the read buffer. */ + size_t cbXferRead; + /** Pointer to statistics record. */ + PIOPERFREQSTAT pStats; +} IOPERFREQ; +/** Pointer to an I/O perf request. */ +typedef IOPERFREQ *PIOPERFREQ; +/** Pointer to a constant I/O perf request. */ +typedef const IOPERFREQ *PCIOPERFREQ; + + +/** + * I/O perf job data. + */ +typedef struct IOPERFJOB +{ + /** Pointer to the master if multiple jobs are running. */ + PIOPERFMASTER pMaster; + /** Job ID. */ + uint32_t idJob; + /** The test this job is executing. */ + volatile IOPERFTEST enmTest; + /** The thread executing the job. */ + RTTHREAD hThread; + /** The I/O queue for the job. */ + RTIOQUEUE hIoQueue; + /** The file path used. */ + char *pszFilename; + /** The handle to use for the I/O queue. */ + RTHANDLE Hnd; + /** Multi event semaphore to synchronise with other jobs. */ + RTSEMEVENTMULTI hSemEvtMultiRendezvous; + /** The test set size. */ + uint64_t cbTestSet; + /** Size of one I/O block. */ + size_t cbIoBlock; + /** Maximum number of requests to queue. */ + uint32_t cReqsMax; + /** Pointer to the array of request specific data. */ + PIOPERFREQ paIoReqs; + /** Page aligned chunk of memory assigned as read buffers for the individual requests. */ + void *pvIoReqReadBuf; + /** Size of the read memory buffer. */ + size_t cbIoReqReadBuf; + /** Random number generator used. */ + RTRAND hRand; + /** The random data buffer used for writes. */ + uint8_t *pbRandWrite; + /** Size of the random write buffer in 512 byte blocks. */ + uint32_t cRandWriteBlocks512B; + /** Chance in percent to get a write. */ + unsigned uWriteChance; + /** Flag whether to verify read data. */ + bool fVerifyReads; + /** Start timestamp. */ + uint64_t tsStart; + /** End timestamp. for the job. */ + uint64_t tsFinish; + /** Number of request statistic records. */ + uint32_t cReqStats; + /** Index of the next free statistics record to use. */ + uint32_t idxReqStatNext; + /** Array of request statistic records for the whole test. */ + PIOPERFREQSTAT paReqStats; + /** Test dependent data. */ + union + { + /** Sequential read write. */ + uint64_t offNextSeq; + /** Data for random acess. */ + struct + { + /** Number of valid entries in the bitmap. */ + uint32_t cBlocks; + /** Pointer to the bitmap marking accessed blocks. */ + uint8_t *pbMapAccessed; + /** Number of unaccessed blocks. */ + uint32_t cBlocksLeft; + } Rnd; + } Tst; +} IOPERFJOB; +/** Pointer to an I/O Perf job. */ +typedef IOPERFJOB *PIOPERFJOB; + + +/** + * I/O perf master instance coordinating the job execution. + */ +typedef struct IOPERFMASTER +{ + /** Event semaphore. */ + /** Number of jobs. */ + uint32_t cJobs; + /** Job instances, variable in size. */ + IOPERFJOB aJobs[1]; +} IOPERFMASTER; + + +enum +{ + kCmdOpt_First = 128, + + kCmdOpt_FirstWrite = kCmdOpt_First, + kCmdOpt_NoFirstWrite, + kCmdOpt_SeqRead, + kCmdOpt_NoSeqRead, + kCmdOpt_SeqWrite, + kCmdOpt_NoSeqWrite, + kCmdOpt_RndRead, + kCmdOpt_NoRndRead, + kCmdOpt_RndWrite, + kCmdOpt_NoRndWrite, + kCmdOpt_RevRead, + kCmdOpt_NoRevRead, + kCmdOpt_RevWrite, + kCmdOpt_NoRevWrite, + kCmdOpt_SeqReadWrite, + kCmdOpt_NoSeqReadWrite, + kCmdOpt_RndReadWrite, + kCmdOpt_NoRndReadWrite, + + kCmdOpt_End +}; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Command line parameters */ +static const RTGETOPTDEF g_aCmdOptions[] = +{ + { "--dir", 'd', RTGETOPT_REQ_STRING }, + { "--relative-dir", 'r', RTGETOPT_REQ_NOTHING }, + + { "--jobs", 'j', RTGETOPT_REQ_UINT32 }, + { "--io-engine", 'i', RTGETOPT_REQ_STRING }, + { "--test-set-size", 's', RTGETOPT_REQ_UINT64 }, + { "--block-size", 'b', RTGETOPT_REQ_UINT32 }, + { "--maximum-requests", 'm', RTGETOPT_REQ_UINT32 }, + { "--verify-reads", 'y', RTGETOPT_REQ_BOOL }, + { "--use-cache", 'c', RTGETOPT_REQ_BOOL }, + + { "--first-write", kCmdOpt_FirstWrite, RTGETOPT_REQ_NOTHING }, + { "--no-first-write", kCmdOpt_NoFirstWrite, RTGETOPT_REQ_NOTHING }, + { "--seq-read", kCmdOpt_SeqRead, RTGETOPT_REQ_NOTHING }, + { "--no-seq-read", kCmdOpt_NoSeqRead, RTGETOPT_REQ_NOTHING }, + { "--seq-write", kCmdOpt_SeqWrite, RTGETOPT_REQ_NOTHING }, + { "--no-seq-write", kCmdOpt_NoSeqWrite, RTGETOPT_REQ_NOTHING }, + { "--rnd-read", kCmdOpt_RndRead, RTGETOPT_REQ_NOTHING }, + { "--no-rnd-read", kCmdOpt_NoRndRead, RTGETOPT_REQ_NOTHING }, + { "--rnd-write", kCmdOpt_RndWrite, RTGETOPT_REQ_NOTHING }, + { "--no-rnd-write", kCmdOpt_NoRndWrite, RTGETOPT_REQ_NOTHING }, + { "--rev-read", kCmdOpt_RevRead, RTGETOPT_REQ_NOTHING }, + { "--no-rev-read", kCmdOpt_NoRevRead, RTGETOPT_REQ_NOTHING }, + { "--rev-write", kCmdOpt_RevWrite, RTGETOPT_REQ_NOTHING }, + { "--no-rev-write", kCmdOpt_NoRevWrite, RTGETOPT_REQ_NOTHING }, + { "--seq-read-write", kCmdOpt_SeqReadWrite, RTGETOPT_REQ_NOTHING }, + { "--no-seq-read-write", kCmdOpt_NoSeqReadWrite, RTGETOPT_REQ_NOTHING }, + { "--rnd-read-write", kCmdOpt_RndReadWrite, RTGETOPT_REQ_NOTHING }, + { "--no-rnd-read-write", kCmdOpt_NoRndReadWrite, RTGETOPT_REQ_NOTHING }, + + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + { "--version", 'V', RTGETOPT_REQ_NOTHING }, + { "--help", 'h', RTGETOPT_REQ_NOTHING } /* for Usage() */ +}; + +/** The test handle. */ +static RTTEST g_hTest; +/** Verbosity level. */ +static uint32_t g_uVerbosity = 0; +/** Selected I/O engine for the tests, NULL means pick best default one. */ +static const char *g_pszIoEngine = NULL; +/** Number of jobs to run concurrently. */ +static uint32_t g_cJobs = 1; +/** Size of each test set (file) in bytes. */ +static uint64_t g_cbTestSet = _2G; +/** Block size for each request. */ +static size_t g_cbIoBlock = _4K; +/** Maximum number of concurrent requests for each job. */ +static uint32_t g_cReqsMax = 16; +/** Flag whether to open the file without caching enabled. */ +static bool g_fNoCache = true; +/** Write chance for mixed read/write tests. */ +static unsigned g_uWriteChance = 50; +/** Flag whether to verify read data. */ +static bool g_fVerifyReads = true; + +/** @name Configured tests, this must match the IOPERFTEST order. + * @{ */ +static IOPERFTEST g_aenmTests[] = +{ + IOPERFTEST_DISABLED, /** @< The invalid test value is disabled of course. */ + IOPERFTEST_DISABLED, + IOPERFTEST_FIRST_WRITE, + IOPERFTEST_SEQ_READ, + IOPERFTEST_SEQ_WRITE, + IOPERFTEST_REV_READ, + IOPERFTEST_REV_WRITE, + IOPERFTEST_RND_READ, + IOPERFTEST_RND_WRITE, + IOPERFTEST_SEQ_READWRITE, + IOPERFTEST_RND_READWRITE, + IOPERFTEST_SHUTDOWN +}; +/** The test index being selected next. */ +static uint32_t g_idxTest = 2; +/** @} */ + +/** Set if g_szDir and friends are path relative to CWD rather than absolute. */ +static bool g_fRelativeDir = false; +/** The length of g_szDir. */ +static size_t g_cchDir; + +/** The test directory (absolute). This will always have a trailing slash. */ +static char g_szDir[RTPATH_BIG_MAX]; + + +/********************************************************************************************************************************* +* Tests * +*********************************************************************************************************************************/ + + +/** + * Selects the next test to run. + * + * @return Next test to run. + */ +static IOPERFTEST ioPerfJobTestSelectNext() +{ + AssertReturn(g_idxTest < RT_ELEMENTS(g_aenmTests), IOPERFTEST_SHUTDOWN); + + while ( g_idxTest < RT_ELEMENTS(g_aenmTests) + && g_aenmTests[g_idxTest] == IOPERFTEST_DISABLED) + g_idxTest++; + + AssertReturn(g_idxTest < RT_ELEMENTS(g_aenmTests), IOPERFTEST_SHUTDOWN); + + return g_aenmTests[g_idxTest++]; +} + + +/** + * Returns the I/O queue operation for the next request. + * + * @returns I/O queue operation enum. + * @param pJob The job data for the current worker. + */ +static RTIOQUEUEOP ioPerfJobTestGetIoQOp(PIOPERFJOB pJob) +{ + switch (pJob->enmTest) + { + case IOPERFTEST_FIRST_WRITE: + case IOPERFTEST_SEQ_WRITE: + case IOPERFTEST_REV_WRITE: + case IOPERFTEST_RND_WRITE: + return RTIOQUEUEOP_WRITE; + + case IOPERFTEST_SEQ_READ: + case IOPERFTEST_RND_READ: + case IOPERFTEST_REV_READ: + return RTIOQUEUEOP_READ; + + case IOPERFTEST_SEQ_READWRITE: + case IOPERFTEST_RND_READWRITE: + { + uint32_t uRnd = RTRandAdvU32Ex(pJob->hRand, 0, 100); + return (uRnd < pJob->uWriteChance) ? RTIOQUEUEOP_WRITE : RTIOQUEUEOP_READ; + } + + default: + AssertMsgFailed(("Invalid/unknown test selected: %d\n", pJob->enmTest)); + break; + } + + return RTIOQUEUEOP_INVALID; +} + + +/** + * Returns the offset to use for the next request. + * + * @returns Offset to use. + * @param pJob The job data for the current worker. + */ +static uint64_t ioPerfJobTestGetOffsetNext(PIOPERFJOB pJob) +{ + uint64_t offNext = 0; + + switch (pJob->enmTest) + { + case IOPERFTEST_FIRST_WRITE: + case IOPERFTEST_SEQ_WRITE: + case IOPERFTEST_SEQ_READ: + case IOPERFTEST_SEQ_READWRITE: + offNext = pJob->Tst.offNextSeq; + pJob->Tst.offNextSeq += pJob->cbIoBlock; + break; + case IOPERFTEST_REV_WRITE: + case IOPERFTEST_REV_READ: + offNext = pJob->Tst.offNextSeq; + if (pJob->Tst.offNextSeq == 0) + pJob->Tst.offNextSeq = pJob->cbTestSet; + else + pJob->Tst.offNextSeq -= pJob->cbIoBlock; + break; + case IOPERFTEST_RND_WRITE: + case IOPERFTEST_RND_READ: + case IOPERFTEST_RND_READWRITE: + { + int idx = -1; + + idx = ASMBitFirstClear(pJob->Tst.Rnd.pbMapAccessed, pJob->Tst.Rnd.cBlocks); + + /* In case this is the last request we don't need to search further. */ + if (pJob->Tst.Rnd.cBlocksLeft > 1) + { + int idxIo; + idxIo = RTRandAdvU32Ex(pJob->hRand, idx, pJob->Tst.Rnd.cBlocks - 1); + + /* + * If the bit is marked free use it, otherwise search for the next free bit + * and if that doesn't work use the first free bit. + */ + if (ASMBitTest(pJob->Tst.Rnd.pbMapAccessed, idxIo)) + { + idxIo = ASMBitNextClear(pJob->Tst.Rnd.pbMapAccessed, pJob->Tst.Rnd.cBlocks, idxIo); + if (idxIo != -1) + idx = idxIo; + } + else + idx = idxIo; + } + + Assert(idx != -1); + offNext = (uint64_t)idx * pJob->cbIoBlock; + pJob->Tst.Rnd.cBlocksLeft--; + ASMBitSet(pJob->Tst.Rnd.pbMapAccessed, idx); + break; + } + default: + AssertMsgFailed(("Invalid/unknown test selected: %d\n", pJob->enmTest)); + break; + } + + return offNext; +} + + +/** + * Returns a pointer to the write buffer with random data for the given offset which + * is predictable for data verification. + * + * @returns Pointer to I/O block sized data buffer with random data. + * @param pJob The job data for the current worker. + * @param off The offset to get the buffer for. + */ +static void *ioPerfJobTestGetWriteBufForOffset(PIOPERFJOB pJob, uint64_t off) +{ + /* + * Dividing the file into 512 byte blocks so buffer pointers are at least + * 512 byte aligned to work with async I/O on some platforms (Linux and O_DIRECT for example). + */ + uint64_t uBlock = off / 512; + uint32_t idxBuf = uBlock % pJob->cRandWriteBlocks512B; + return pJob->pbRandWrite + idxBuf * 512; +} + + +/** + * Initialize the given request for submission. + * + * @param pJob The job data for the current worker. + * @param pIoReq The request to initialize. + */ +static void ioPerfJobTestReqInit(PIOPERFJOB pJob, PIOPERFREQ pIoReq) +{ + pIoReq->enmOp = ioPerfJobTestGetIoQOp(pJob); + pIoReq->offXfer = ioPerfJobTestGetOffsetNext(pJob); + pIoReq->cbXfer = pJob->cbIoBlock; + if (pIoReq->enmOp == RTIOQUEUEOP_READ) + pIoReq->pvXfer = pIoReq->pvXferRead; + else if (pIoReq->enmOp == RTIOQUEUEOP_WRITE) + pIoReq->pvXfer = ioPerfJobTestGetWriteBufForOffset(pJob, pIoReq->offXfer); + else /* Flush */ + pIoReq->pvXfer = NULL; + + Assert(pJob->idxReqStatNext < pJob->cReqStats); + if (RT_LIKELY(pJob->idxReqStatNext < pJob->cReqStats)) + { + pIoReq->pStats = &pJob->paReqStats[pJob->idxReqStatNext++]; + pIoReq->pStats->tsStart = RTTimeNanoTS(); + } + else + pIoReq->pStats = NULL; +} + + +/** + * Returns a stringified version of the test given. + * + * @returns Pointer to string representation of the test. + * @param enmTest The test to stringify. + */ +static const char *ioPerfJobTestStringify(IOPERFTEST enmTest) +{ + switch (enmTest) + { + case IOPERFTEST_FIRST_WRITE: + return "FirstWrite"; + case IOPERFTEST_SEQ_WRITE: + return "SequentialWrite"; + case IOPERFTEST_SEQ_READ: + return "SequentialRead"; + case IOPERFTEST_REV_WRITE: + return "ReverseWrite"; + case IOPERFTEST_REV_READ: + return "ReverseRead"; + case IOPERFTEST_RND_WRITE: + return "RandomWrite"; + case IOPERFTEST_RND_READ: + return "RandomRead"; + case IOPERFTEST_SEQ_READWRITE: + return "SequentialReadWrite"; + case IOPERFTEST_RND_READWRITE: + return "RandomReadWrite"; + default: + AssertMsgFailed(("Invalid/unknown test selected: %d\n", enmTest)); + break; + } + + return "INVALID_TEST"; +} + + +/** + * Initializes the test state for the current test. + * + * @returns IPRT status code. + * @param pJob The job data for the current worker. + */ +static int ioPerfJobTestInit(PIOPERFJOB pJob) +{ + int rc = VINF_SUCCESS; + + pJob->idxReqStatNext = 0; + + switch (pJob->enmTest) + { + case IOPERFTEST_FIRST_WRITE: + case IOPERFTEST_SEQ_WRITE: + case IOPERFTEST_SEQ_READ: + case IOPERFTEST_SEQ_READWRITE: + pJob->Tst.offNextSeq = 0; + break; + case IOPERFTEST_REV_WRITE: + case IOPERFTEST_REV_READ: + pJob->Tst.offNextSeq = pJob->cbTestSet - pJob->cbIoBlock; + break; + case IOPERFTEST_RND_WRITE: + case IOPERFTEST_RND_READ: + case IOPERFTEST_RND_READWRITE: + { + pJob->Tst.Rnd.cBlocks = (uint32_t)( pJob->cbTestSet / pJob->cbIoBlock + + (pJob->cbTestSet % pJob->cbIoBlock ? 1 : 0)); + pJob->Tst.Rnd.cBlocksLeft = pJob->Tst.Rnd.cBlocks; + pJob->Tst.Rnd.pbMapAccessed = (uint8_t *)RTMemAllocZ( pJob->Tst.Rnd.cBlocks / 8 + + ((pJob->Tst.Rnd.cBlocks % 8) + ? 1 + : 0)); + if (!pJob->Tst.Rnd.pbMapAccessed) + rc = VERR_NO_MEMORY; + break; + } + default: + AssertMsgFailed(("Invalid/unknown test selected: %d\n", pJob->enmTest)); + break; + } + + pJob->tsStart = RTTimeNanoTS(); + return rc; +} + + +/** + * Frees allocated resources specific for the current test. + * + * @param pJob The job data for the current worker. + */ +static void ioPerfJobTestFinish(PIOPERFJOB pJob) +{ + pJob->tsFinish = RTTimeNanoTS(); + + switch (pJob->enmTest) + { + case IOPERFTEST_FIRST_WRITE: + case IOPERFTEST_SEQ_WRITE: + case IOPERFTEST_SEQ_READ: + case IOPERFTEST_REV_WRITE: + case IOPERFTEST_REV_READ: + case IOPERFTEST_SEQ_READWRITE: + break; /* Nothing to do. */ + + case IOPERFTEST_RND_WRITE: + case IOPERFTEST_RND_READ: + case IOPERFTEST_RND_READWRITE: + RTMemFree(pJob->Tst.Rnd.pbMapAccessed); + break; + default: + AssertMsgFailed(("Invalid/unknown test selected: %d\n", pJob->enmTest)); + break; + } +} + + +/** + * Returns whether the current test is done with submitting new requests (reached test set size). + * + * @returns True when the test has submitted all required requests, false if there are still requests required + */ +static bool ioPerfJobTestIsDone(PIOPERFJOB pJob) +{ + switch (pJob->enmTest) + { + case IOPERFTEST_FIRST_WRITE: + case IOPERFTEST_SEQ_WRITE: + case IOPERFTEST_SEQ_READ: + case IOPERFTEST_REV_WRITE: + case IOPERFTEST_REV_READ: + case IOPERFTEST_SEQ_READWRITE: + return pJob->Tst.offNextSeq == pJob->cbTestSet; + case IOPERFTEST_RND_WRITE: + case IOPERFTEST_RND_READ: + case IOPERFTEST_RND_READWRITE: + return pJob->Tst.Rnd.cBlocksLeft == 0; + break; + default: + AssertMsgFailed(("Invalid/unknown test selected: %d\n", pJob->enmTest)); + break; + } + + return true; +} + + +/** + * The test I/O loop pumping I/O. + * + * @returns IPRT status code. + * @param pJob The job data for the current worker. + */ +static int ioPerfJobTestIoLoop(PIOPERFJOB pJob) +{ + int rc = ioPerfJobTestInit(pJob); + if (RT_SUCCESS(rc)) + { + /* Allocate the completion event array. */ + uint32_t cReqsQueued = 0; + PRTIOQUEUECEVT paIoQCEvt = (PRTIOQUEUECEVT)RTMemAllocZ(pJob->cReqsMax * sizeof(RTIOQUEUECEVT)); + if (RT_LIKELY(paIoQCEvt)) + { + /* Queue requests up to the maximum. */ + while ( (cReqsQueued < pJob->cReqsMax) + && !ioPerfJobTestIsDone(pJob) + && RT_SUCCESS(rc)) + { + PIOPERFREQ pReq = &pJob->paIoReqs[cReqsQueued]; + ioPerfJobTestReqInit(pJob, pReq); + RTTESTI_CHECK_RC(RTIoQueueRequestPrepare(pJob->hIoQueue, &pJob->Hnd, pReq->enmOp, + pReq->offXfer, pReq->pvXfer, pReq->cbXfer, 0 /*fReqFlags*/, + pReq), VINF_SUCCESS); + cReqsQueued++; + } + + /* Commit the prepared requests. */ + if ( RT_SUCCESS(rc) + && cReqsQueued) + { + RTTESTI_CHECK_RC(RTIoQueueCommit(pJob->hIoQueue), VINF_SUCCESS); + } + + /* Enter wait loop and process completed requests. */ + while ( RT_SUCCESS(rc) + && cReqsQueued) + { + uint32_t cCEvtCompleted = 0; + + RTTESTI_CHECK_RC(RTIoQueueEvtWait(pJob->hIoQueue, paIoQCEvt, pJob->cReqsMax, 1 /*cMinWait*/, + &cCEvtCompleted, 0 /*fFlags*/), VINF_SUCCESS); + if (RT_SUCCESS(rc)) + { + uint32_t cReqsThisQueued = 0; + + /* Process any completed event and continue to fill the queue as long as there is stuff to do. */ + for (uint32_t i = 0; i < cCEvtCompleted; i++) + { + PIOPERFREQ pReq = (PIOPERFREQ)paIoQCEvt[i].pvUser; + + if (RT_SUCCESS(paIoQCEvt[i].rcReq)) + { + Assert(paIoQCEvt[i].cbXfered == pReq->cbXfer); + + if (pReq->pStats) + pReq->pStats->tsComplete = RTTimeNanoTS(); + + if ( pJob->fVerifyReads + && pReq->enmOp == RTIOQUEUEOP_READ) + { + const void *pvBuf = ioPerfJobTestGetWriteBufForOffset(pJob, pReq->offXfer); + if (memcmp(pReq->pvXferRead, pvBuf, pReq->cbXfer)) + { + if (g_uVerbosity > 1) + RTTestIFailed("IoPerf: Corrupted data detected by read at offset %#llu (sz: %zu)", pReq->offXfer, pReq->cbXfer); + else + RTTestIErrorInc(); + } + } + + if (!ioPerfJobTestIsDone(pJob)) + { + ioPerfJobTestReqInit(pJob, pReq); + RTTESTI_CHECK_RC(RTIoQueueRequestPrepare(pJob->hIoQueue, &pJob->Hnd, pReq->enmOp, + pReq->offXfer, pReq->pvXfer, pReq->cbXfer, 0 /*fReqFlags*/, + pReq), VINF_SUCCESS); + cReqsThisQueued++; + } + else + cReqsQueued--; + } + else + RTTestIErrorInc(); + } + + if ( cReqsThisQueued + && RT_SUCCESS(rc)) + { + RTTESTI_CHECK_RC(RTIoQueueCommit(pJob->hIoQueue), VINF_SUCCESS); + } + } + } + + RTMemFree(paIoQCEvt); + } + + ioPerfJobTestFinish(pJob); + } + + return rc; +} + + +/** + * Calculates the statistic values for the given job after a + * test finished. + * + * @param pJob The job data. + */ +static void ioPerfJobStats(PIOPERFJOB pJob) +{ + const char *pszTest = ioPerfJobTestStringify(pJob->enmTest); + uint64_t nsJobRuntime = pJob->tsFinish - pJob->tsStart; + RTTestIValueF(nsJobRuntime, RTTESTUNIT_NS, "%s/Job/%RU32/Runtime", pszTest, pJob->idJob); + + uint64_t *paReqRuntimeNs = (uint64_t *)RTMemAllocZ(pJob->cReqStats * sizeof(uint64_t)); + if (RT_LIKELY(paReqRuntimeNs)) + { + /* Calculate runtimes for each request first. */ + for (uint32_t i = 0; i < pJob->cReqStats; i++) + { + PIOPERFREQSTAT pStat = &pJob->paReqStats[i]; + paReqRuntimeNs[i] = pStat->tsComplete - pStat->tsStart; + } + + /* Get average bandwidth for the job. */ + RTTestIValueF((uint64_t)((double)pJob->cbTestSet / ((double)nsJobRuntime / RT_NS_1SEC)), + RTTESTUNIT_BYTES_PER_SEC, "%s/Job/%RU32/AvgBandwidth", pszTest, pJob->idJob); + + RTTestIValueF((uint64_t)(pJob->cReqStats / ((double)nsJobRuntime / RT_NS_1SEC)), + RTTESTUNIT_OCCURRENCES_PER_SEC, "%s/Job/%RU32/AvgIops", pszTest, pJob->idJob); + + /* Calculate the average latency for the requests. */ + uint64_t uLatency = 0; + for (uint32_t i = 0; i < pJob->cReqStats; i++) + uLatency += paReqRuntimeNs[i]; + RTTestIValueF(uLatency / pJob->cReqStats, RTTESTUNIT_NS, "%s/Job/%RU32/AvgLatency", pszTest, pJob->idJob); + + RTMemFree(paReqRuntimeNs); + } + else + RTTestIErrorInc(); +} + + +/** + * Synchronizes with the other jobs and waits for the current test to execute. + * + * @returns IPRT status. + * @param pJob The job data for the current worker. + */ +static int ioPerfJobSync(PIOPERFJOB pJob) +{ + if (pJob->pMaster) + { + /* Enter the rendezvous semaphore. */ + int rc = VINF_SUCCESS; + + return rc; + } + + /* Single threaded run, collect the results from our current test and select the next test. */ + /** @todo Results and statistics collection. */ + pJob->enmTest = ioPerfJobTestSelectNext(); + return VINF_SUCCESS; +} + + +/** + * I/O perf job main work loop. + * + * @returns IPRT status code. + * @param pJob The job data for the current worker. + */ +static int ioPerfJobWorkLoop(PIOPERFJOB pJob) +{ + int rc = VINF_SUCCESS; + + for (;;) + { + /* Synchronize with the other jobs and the master. */ + rc = ioPerfJobSync(pJob); + if (RT_FAILURE(rc)) + break; + + if (pJob->enmTest == IOPERFTEST_SHUTDOWN) + break; + + rc = ioPerfJobTestIoLoop(pJob); + if (RT_FAILURE(rc)) + break; + + /* + * Do the statistics here for a single job run, + * the master will do this for each job and combined statistics + * otherwise. + */ + if (!pJob->pMaster) + ioPerfJobStats(pJob); + } + + return rc; +} + + +/** + * Job thread entry point. + */ +static DECLCALLBACK(int) ioPerfJobThread(RTTHREAD hThrdSelf, void *pvUser) +{ + RT_NOREF(hThrdSelf); + + PIOPERFJOB pJob = (PIOPERFJOB)pvUser; + return ioPerfJobWorkLoop(pJob); +} + + +/** + * Prepares the test set by laying out the files and filling them with data. + * + * @returns IPRT status code. + * @param pJob The job to initialize. + */ +static int ioPerfJobTestSetPrep(PIOPERFJOB pJob) +{ + int rc = RTRandAdvCreateParkMiller(&pJob->hRand); + if (RT_SUCCESS(rc)) + { + rc = RTRandAdvSeed(pJob->hRand, RTTimeNanoTS()); + if (RT_SUCCESS(rc)) + { + /* + * Create a random data buffer for writes, we'll use multiple of the I/O block size to + * be able to seek in the buffer quite a bit to make the file content as random as possible + * to avoid mechanisms like compression or deduplication for now which can influence storage + * benchmarking unpredictably. + */ + pJob->cRandWriteBlocks512B = (uint32_t)(((IOPERF_RAND_DATA_BUF_FACTOR - 1) * pJob->cbIoBlock) / 512); + pJob->pbRandWrite = (uint8_t *)RTMemPageAllocZ(IOPERF_RAND_DATA_BUF_FACTOR * pJob->cbIoBlock); + if (RT_LIKELY(pJob->pbRandWrite)) + { + RTRandAdvBytes(pJob->hRand, pJob->pbRandWrite, IOPERF_RAND_DATA_BUF_FACTOR * pJob->cbIoBlock); + + /* Write the content here if the first write test is disabled. */ + if (g_aenmTests[IOPERFTEST_FIRST_WRITE] == IOPERFTEST_DISABLED) + { + for (uint64_t off = 0; off < pJob->cbTestSet && RT_SUCCESS(rc); off += pJob->cbIoBlock) + { + void *pvWrite = ioPerfJobTestGetWriteBufForOffset(pJob, off); + rc = RTFileWriteAt(pJob->Hnd.u.hFile, off, pvWrite, pJob->cbIoBlock, NULL); + } + } + + if (RT_SUCCESS(rc)) + return rc; + + RTMemPageFree(pJob->pbRandWrite, IOPERF_RAND_DATA_BUF_FACTOR * pJob->cbIoBlock); + } + } + RTRandAdvDestroy(pJob->hRand); + } + + return rc; +} + + +/** + * Initializes the given job instance. + * + * @returns IPRT status code. + * @param pJob The job to initialize. + * @param pMaster The coordination master if any. + * @param idJob ID of the job. + * @param pszIoEngine I/O queue engine for this job, NULL for best default. + * @param pszTestDir The test directory to create the file in - requires a slash a the end. + * @param enmPrepMethod Test set preparation method to use. + * @param cbTestSet Size of the test set ofr this job. + * @param cbIoBlock I/O block size for the given job. + * @param cReqsMax Maximum number of concurrent requests for this job. + * @param uWriteChance The write chance for mixed read/write tests. + * @param fVerifyReads Flag whether to verify read data. + */ +static int ioPerfJobInit(PIOPERFJOB pJob, PIOPERFMASTER pMaster, uint32_t idJob, + const char *pszIoEngine, const char *pszTestDir, + IOPERFTESTSETPREP enmPrepMethod, + uint64_t cbTestSet, size_t cbIoBlock, uint32_t cReqsMax, + unsigned uWriteChance, bool fVerifyReads) +{ + pJob->pMaster = pMaster; + pJob->idJob = idJob; + pJob->enmTest = IOPERFTEST_INVALID; + pJob->hThread = NIL_RTTHREAD; + pJob->Hnd.enmType = RTHANDLETYPE_FILE; + pJob->cbTestSet = cbTestSet; + pJob->cbIoBlock = cbIoBlock; + pJob->cReqsMax = cReqsMax; + pJob->cbIoReqReadBuf = cReqsMax * cbIoBlock; + pJob->uWriteChance = uWriteChance; + pJob->fVerifyReads = fVerifyReads; + pJob->cReqStats = (uint32_t)(pJob->cbTestSet / pJob->cbIoBlock + ((pJob->cbTestSet % pJob->cbIoBlock) ? 1 : 0)); + pJob->idxReqStatNext = 0; + + int rc = VINF_SUCCESS; + pJob->paIoReqs = (PIOPERFREQ)RTMemAllocZ(cReqsMax * sizeof(IOPERFREQ)); + if (RT_LIKELY(pJob->paIoReqs)) + { + pJob->paReqStats = (PIOPERFREQSTAT)RTMemAllocZ(pJob->cReqStats * sizeof(IOPERFREQSTAT)); + if (RT_LIKELY(pJob->paReqStats)) + { + pJob->pvIoReqReadBuf = RTMemPageAlloc(pJob->cbIoReqReadBuf); + if (RT_LIKELY(pJob->pvIoReqReadBuf)) + { + uint8_t *pbReadBuf = (uint8_t *)pJob->pvIoReqReadBuf; + + for (uint32_t i = 0; i < cReqsMax; i++) + { + pJob->paIoReqs[i].pvXferRead = pbReadBuf; + pJob->paIoReqs[i].cbXferRead = cbIoBlock; + pbReadBuf += cbIoBlock; + } + + /* Create the file. */ + pJob->pszFilename = RTStrAPrintf2("%sioperf-%u.file", pszTestDir, idJob); + if (RT_LIKELY(pJob->pszFilename)) + { + uint32_t fOpen = RTFILE_O_CREATE_REPLACE | RTFILE_O_DENY_NONE | RTFILE_O_READWRITE | RTFILE_O_ASYNC_IO; + if (g_fNoCache) + fOpen |= RTFILE_O_NO_CACHE; + rc = RTFileOpen(&pJob->Hnd.u.hFile, pJob->pszFilename, fOpen); + if (RT_SUCCESS(rc)) + { + switch (enmPrepMethod) + { + case IOPERFTESTSETPREP_JUST_CREATE: + break; + case IOPERFTESTSETPREP_SET_SZ: + rc = RTFileSetSize(pJob->Hnd.u.hFile, pJob->cbTestSet); + break; + case IOPERFTESTSETPREP_SET_ALLOC_SZ: + rc = RTFileSetAllocationSize(pJob->Hnd.u.hFile, pJob->cbTestSet, RTFILE_ALLOC_SIZE_F_DEFAULT); + break; + default: + AssertMsgFailed(("Invalid file preparation method: %d\n", enmPrepMethod)); + } + + if (RT_SUCCESS(rc)) + { + rc = ioPerfJobTestSetPrep(pJob); + if (RT_SUCCESS(rc)) + { + /* Create I/O queue. */ + PCRTIOQUEUEPROVVTABLE pIoQProv = NULL; + if (!pszIoEngine) + pIoQProv = RTIoQueueProviderGetBestForHndType(RTHANDLETYPE_FILE); + else + pIoQProv = RTIoQueueProviderGetById(pszIoEngine); + + if (RT_LIKELY(pIoQProv)) + { + rc = RTIoQueueCreate(&pJob->hIoQueue, pIoQProv, 0 /*fFlags*/, cReqsMax, cReqsMax); + if (RT_SUCCESS(rc)) + { + rc = RTIoQueueHandleRegister(pJob->hIoQueue, &pJob->Hnd); + if (RT_SUCCESS(rc)) + { + /* Spin up the worker thread. */ + if (pMaster) + rc = RTThreadCreateF(&pJob->hThread, ioPerfJobThread, pJob, 0, + RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "ioperf-%u", idJob); + + if (RT_SUCCESS(rc)) + return VINF_SUCCESS; + } + } + } + else + rc = VERR_NOT_SUPPORTED; + } + + RTRandAdvDestroy(pJob->hRand); + } + + RTFileClose(pJob->Hnd.u.hFile); + RTFileDelete(pJob->pszFilename); + } + + RTStrFree(pJob->pszFilename); + } + else + rc = VERR_NO_STR_MEMORY; + + RTMemPageFree(pJob->pvIoReqReadBuf, pJob->cbIoReqReadBuf); + } + else + rc = VERR_NO_MEMORY; + + RTMemFree(pJob->paReqStats); + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VERR_NO_MEMORY; + + return rc; +} + + +/** + * Teardown a job instance and free all associated resources. + * + * @returns IPRT status code. + * @param pJob The job to teardown. + */ +static int ioPerfJobTeardown(PIOPERFJOB pJob) +{ + if (pJob->pMaster) + { + int rc = RTThreadWait(pJob->hThread, RT_INDEFINITE_WAIT, NULL); + AssertRC(rc); RT_NOREF(rc); + } + + RTIoQueueHandleDeregister(pJob->hIoQueue, &pJob->Hnd); + RTIoQueueDestroy(pJob->hIoQueue); + RTRandAdvDestroy(pJob->hRand); + RTMemPageFree(pJob->pbRandWrite, IOPERF_RAND_DATA_BUF_FACTOR * pJob->cbIoBlock); + RTFileClose(pJob->Hnd.u.hFile); + RTFileDelete(pJob->pszFilename); + RTStrFree(pJob->pszFilename); + RTMemPageFree(pJob->pvIoReqReadBuf, pJob->cbIoReqReadBuf); + RTMemFree(pJob->paIoReqs); + RTMemFree(pJob->paReqStats); + return VINF_SUCCESS; +} + + +/** + * Single job testing entry point. + * + * @returns IPRT status code. + */ +static int ioPerfDoTestSingle(void) +{ + IOPERFJOB Job; + + int rc = ioPerfJobInit(&Job, NULL, 0, g_pszIoEngine, + g_szDir, IOPERFTESTSETPREP_SET_SZ, + g_cbTestSet, g_cbIoBlock, g_cReqsMax, + g_uWriteChance, g_fVerifyReads); + if (RT_SUCCESS(rc)) + { + rc = ioPerfJobWorkLoop(&Job); + if (RT_SUCCESS(rc)) + { + rc = ioPerfJobTeardown(&Job); + AssertRC(rc); RT_NOREF(rc); + } + } + + return rc; +} + + +/** + * Multi job testing entry point. + * + * @returns IPRT status code. + */ +static int ioPerfDoTestMulti(void) +{ + return VERR_NOT_IMPLEMENTED; +} + + +/** + * Display the usage to @a pStrm. + */ +static void Usage(PRTSTREAM pStrm) +{ + char szExec[RTPATH_MAX]; + RTStrmPrintf(pStrm, "usage: %s <-d <testdir>> [options]\n", + RTPathFilename(RTProcGetExecutablePath(szExec, sizeof(szExec)))); + RTStrmPrintf(pStrm, "\n"); + RTStrmPrintf(pStrm, "options: \n"); + + for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) + { + char szHelp[80]; + const char *pszHelp; + switch (g_aCmdOptions[i].iShort) + { + case 'd': pszHelp = "The directory to use for testing. default: CWD/fstestdir"; break; + case 'r': pszHelp = "Don't abspath test dir (good for deep dirs). default: disabled"; break; + case 'y': pszHelp = "Flag whether to verify read data. default: enabled"; break; + case 'c': pszHelp = "Flag whether to use the filesystem cache. default: disabled"; break; + case 'v': pszHelp = "More verbose execution."; break; + case 'q': pszHelp = "Quiet execution."; break; + case 'h': pszHelp = "Displays this help and exit"; break; + case 'V': pszHelp = "Displays the program revision"; break; + default: + if (g_aCmdOptions[i].iShort >= kCmdOpt_First) + { + if (RTStrStartsWith(g_aCmdOptions[i].pszLong, "--no-")) + RTStrPrintf(szHelp, sizeof(szHelp), "Disables the '%s' test.", g_aCmdOptions[i].pszLong + 5); + else + RTStrPrintf(szHelp, sizeof(szHelp), "Enables the '%s' test.", g_aCmdOptions[i].pszLong + 2); + pszHelp = szHelp; + } + else + pszHelp = "Option undocumented"; + break; + } + if ((unsigned)g_aCmdOptions[i].iShort < 127U) + { + char szOpt[64]; + RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); + RTStrmPrintf(pStrm, " %-19s %s\n", szOpt, pszHelp); + } + else + RTStrmPrintf(pStrm, " %-19s %s\n", g_aCmdOptions[i].pszLong, pszHelp); + } +} + + +int main(int argc, char *argv[]) +{ + /* + * Init IPRT and globals. + */ + int rc = RTTestInitAndCreate("IoPerf", &g_hTest); + if (rc) + return rc; + + /* + * Default values. + */ + char szDefaultDir[32]; + const char *pszDir = szDefaultDir; + RTStrPrintf(szDefaultDir, sizeof(szDefaultDir), "ioperfdir-%u" RTPATH_SLASH_STR, RTProcSelf()); + + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); + while ((rc = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + switch (rc) + { + case 'd': + pszDir = ValueUnion.psz; + break; + + case 'r': + g_fRelativeDir = true; + break; + + case 'i': + g_pszIoEngine = ValueUnion.psz; + break; + + case 's': + g_cbTestSet = ValueUnion.u64; + break; + + case 'b': + g_cbIoBlock = ValueUnion.u32; + break; + + case 'm': + g_cReqsMax = ValueUnion.u32; + break; + + case 'y': + g_fVerifyReads = ValueUnion.f; + break; + + case 'c': + g_fNoCache = !ValueUnion.f; + break; + + case kCmdOpt_FirstWrite: + g_aenmTests[IOPERFTEST_FIRST_WRITE] = IOPERFTEST_FIRST_WRITE; + break; + case kCmdOpt_NoFirstWrite: + g_aenmTests[IOPERFTEST_FIRST_WRITE] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_SeqRead: + g_aenmTests[IOPERFTEST_SEQ_READ] = IOPERFTEST_SEQ_READ; + break; + case kCmdOpt_NoSeqRead: + g_aenmTests[IOPERFTEST_SEQ_READ] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_SeqWrite: + g_aenmTests[IOPERFTEST_SEQ_WRITE] = IOPERFTEST_SEQ_WRITE; + break; + case kCmdOpt_NoSeqWrite: + g_aenmTests[IOPERFTEST_SEQ_WRITE] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_RndRead: + g_aenmTests[IOPERFTEST_RND_READ] = IOPERFTEST_RND_READ; + break; + case kCmdOpt_NoRndRead: + g_aenmTests[IOPERFTEST_RND_READ] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_RndWrite: + g_aenmTests[IOPERFTEST_RND_WRITE] = IOPERFTEST_RND_WRITE; + break; + case kCmdOpt_NoRndWrite: + g_aenmTests[IOPERFTEST_RND_WRITE] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_RevRead: + g_aenmTests[IOPERFTEST_REV_READ] = IOPERFTEST_REV_READ; + break; + case kCmdOpt_NoRevRead: + g_aenmTests[IOPERFTEST_REV_READ] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_RevWrite: + g_aenmTests[IOPERFTEST_REV_WRITE] = IOPERFTEST_REV_WRITE; + break; + case kCmdOpt_NoRevWrite: + g_aenmTests[IOPERFTEST_REV_WRITE] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_SeqReadWrite: + g_aenmTests[IOPERFTEST_SEQ_READWRITE] = IOPERFTEST_SEQ_READWRITE; + break; + case kCmdOpt_NoSeqReadWrite: + g_aenmTests[IOPERFTEST_SEQ_READWRITE] = IOPERFTEST_DISABLED; + break; + case kCmdOpt_RndReadWrite: + g_aenmTests[IOPERFTEST_RND_READWRITE] = IOPERFTEST_RND_READWRITE; + break; + case kCmdOpt_NoRndReadWrite: + g_aenmTests[IOPERFTEST_RND_READWRITE] = IOPERFTEST_DISABLED; + break; + + case 'q': + g_uVerbosity = 0; + break; + + case 'v': + g_uVerbosity++; + break; + + case 'h': + Usage(g_pStdOut); + return RTEXITCODE_SUCCESS; + + case 'V': + { + char szRev[] = "$Revision: 157380 $"; + szRev[RT_ELEMENTS(szRev) - 2] = '\0'; + RTPrintf(RTStrStrip(strchr(szRev, ':') + 1)); + return RTEXITCODE_SUCCESS; + } + + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + /* + * Populate g_szDir. + */ + if (!g_fRelativeDir) + rc = RTPathAbs(pszDir, g_szDir, sizeof(g_szDir)); + else + rc = RTStrCopy(g_szDir, sizeof(g_szDir), pszDir); + if (RT_FAILURE(rc)) + { + RTTestFailed(g_hTest, "%s(%s) failed: %Rrc\n", g_fRelativeDir ? "RTStrCopy" : "RTAbsPath", pszDir, rc); + return RTTestSummaryAndDestroy(g_hTest); + } + RTPathEnsureTrailingSeparator(g_szDir, sizeof(g_szDir)); + g_cchDir = strlen(g_szDir); + + /* + * Create the test directory with an 'empty' subdirectory under it, + * execute the tests, and remove directory when done. + */ + RTTestBanner(g_hTest); + if (!RTPathExists(g_szDir)) + { + /* The base dir: */ + rc = RTDirCreate(g_szDir, 0755, + RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_DONT_SET | RTDIRCREATE_FLAGS_NOT_CONTENT_INDEXED_NOT_CRITICAL); + if (RT_SUCCESS(rc)) + { + RTTestIPrintf(RTTESTLVL_ALWAYS, "Test dir: %s\n", g_szDir); + + if (g_cJobs == 1) + rc = ioPerfDoTestSingle(); + else + rc = ioPerfDoTestMulti(); + + g_szDir[g_cchDir] = '\0'; + rc = RTDirRemoveRecursive(g_szDir, RTDIRRMREC_F_CONTENT_AND_DIR | (g_fRelativeDir ? RTDIRRMREC_F_NO_ABS_PATH : 0)); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "RTDirRemoveRecursive(%s,) -> %Rrc\n", g_szDir, rc); + } + else + RTTestFailed(g_hTest, "RTDirCreate(%s) -> %Rrc\n", g_szDir, rc); + } + else + RTTestFailed(g_hTest, "Test directory already exists: %s\n", g_szDir); + + return RTTestSummaryAndDestroy(g_hTest); +} + diff --git a/src/VBox/ValidationKit/utils/storage/Makefile.kmk b/src/VBox/ValidationKit/utils/storage/Makefile.kmk new file mode 100644 index 00000000..7042541f --- /dev/null +++ b/src/VBox/ValidationKit/utils/storage/Makefile.kmk @@ -0,0 +1,49 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Storage I/O performance tests. +# + +# +# Copyright (C) 2019-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# Storage I/O Performance. +# +PROGRAMS += IoPerf +IoPerf_TEMPLATE = VBoxValidationKitR3 +IoPerf_SOURCES = IoPerf.cpp + +$(evalcall def_vbox_validationkit_process_python_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/usb/Makefile.kmk b/src/VBox/ValidationKit/utils/usb/Makefile.kmk new file mode 100644 index 00000000..e8dc1923 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/Makefile.kmk @@ -0,0 +1,74 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - USB test helpers. +# + +# +# Copyright (C) 2014-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 <https://www.gnu.org/licenses>. +# +# The contents of this file may alternatively be used under the terms +# of the Common Development and Distribution License Version 1.0 +# (CDDL), a copy of it is provided in the "COPYING.CDDL" file included +# in the VirtualBox distribution, in which case the provisions of the +# CDDL are applicable instead of those of the GPL. +# +# You may elect to license modified versions of this file under the +# terms and conditions of either the GPL or the CDDL or both. +# +# SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + +# +# USB Linux test frontend. +# +ifeq ($(KBUILD_TARGET),linux) + PROGRAMS += UsbTest + UsbTest_TEMPLATE = VBoxValidationKitR3 + UsbTest_SOURCES = UsbTest.cpp +endif + +PROGRAMS += UsbTestService +UsbTestService_TEMPLATE = VBoxValidationKitR3 +ifdef VBOX_WITH_AUTOMATIC_DEFS_QUOTING + UsbTestService_DEFS = \ + KBUILD_TARGET="$(KBUILD_TARGET)" \ + KBUILD_TARGET_ARCH="$(KBUILD_TARGET_ARCH)" +else + UsbTestService_DEFS = \ + KBUILD_TARGET=\"$(KBUILD_TARGET)\" \ + KBUILD_TARGET_ARCH=\"$(KBUILD_TARGET_ARCH)\" +endif +UsbTestService_SOURCES = \ + UsbTestService.cpp \ + UsbTestServiceGadgetCfg.cpp \ + UsbTestServiceGadgetClassTest.cpp \ + UsbTestServiceGadgetHost.cpp \ + UsbTestServiceGadgetHostUsbIp.cpp \ + UsbTestServiceGadget.cpp \ + UsbTestServiceProtocol.cpp \ + UsbTestServiceTcp.cpp +UsbTestService_SOURCES.linux = \ + UsbTestServicePlatform-linux.cpp + +$(evalcall def_vbox_validationkit_process_python_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTest.cpp b/src/VBox/ValidationKit/utils/usb/UsbTest.cpp new file mode 100644 index 00000000..809e3fe1 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTest.cpp @@ -0,0 +1,667 @@ +/* $Id: UsbTest.cpp $ */ +/** @file + * UsbTest - User frontend for the Linux usbtest USB test and benchmarking module. + * Integrates with our test framework for nice outputs. + */ + +/* + * Copyright (C) 2014-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/dir.h> +#include <iprt/err.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/path.h> +#include <iprt/param.h> +#include <iprt/process.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#include <iprt/linux/sysfs.h> + +#include <unistd.h> +#include <errno.h> +#include <limits.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include <sys/ioctl.h> +#include <linux/usbdevice_fs.h> + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * USB test request data. + * There is no public header with this information so we define it ourself here. + */ +typedef struct USBTESTPARMS +{ + /** Specifies the test to run. */ + uint32_t idxTest; + /** How many iterations the test should be executed. */ + uint32_t cIterations; + /** Size of the data packets. */ + uint32_t cbData; + /** Size of */ + uint32_t cbVariation; + /** Length of the S/G list for the test. */ + uint32_t cSgLength; + /** Returned time data after completing the test. */ + struct timeval TimeTest; +} USBTESTPARAMS; +/** Pointer to a test parameter structure. */ +typedef USBTESTPARAMS *PUSBTESTPARAMS; + +/** + * USB device descriptor. Used to search for the test device based + * on the vendor and product id. + */ +#pragma pack(1) +typedef struct USBDEVDESC +{ + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t bcdUSB; + uint8_t bDeviceClass; + uint8_t bDeviceSubClass; + uint8_t bDeviceProtocol; + uint8_t bMaxPacketSize0; + uint16_t idVendor; + uint16_t idProduct; + uint16_t bcdDevice; + uint8_t iManufacturer; + uint8_t iProduct; + uint8_t iSerialNumber; + uint8_t bNumConfigurations; +} USBDEVDESC; +#pragma pack() + +#define USBTEST_REQUEST _IOWR('U', 100, USBTESTPARMS) + +/** + * Callback to set up the test parameters for a specific test. + * + * @returns IPRT status code. + * @retval VINF_SUCCESS if setting the parameters up succeeded. Any other error code + * otherwise indicating the kind of error. + * @param idxTest The test index. + * @param pszTest Test name. + * @param pParams The USB test parameters to set up. + */ +typedef DECLCALLBACKTYPE(int, FNUSBTESTPARAMSSETUP,(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams)); +/** Pointer to a USB test parameters setup callback. */ +typedef FNUSBTESTPARAMSSETUP *PFNUSBTESTPARAMSSETUP; + +/** + * USB test descriptor. + */ +typedef struct USBTESTDESC +{ + /** (Sort of) Descriptive test name. */ + const char *pszName; + /** Flag whether the test is excluded. */ + bool fExcluded; + /** The parameter setup callback. */ + PFNUSBTESTPARAMSSETUP pfnParamsSetup; +} USBTESTDESC; +/** Pointer a USB test descriptor. */ +typedef USBTESTDESC *PUSBTESTDESC; + +/** + * USB speed values. + */ +typedef enum USBTESTSPEED +{ + USBTESTSPEED_ANY = 0, + USBTESTSPEED_UNKNOWN, + USBTESTSPEED_LOW, + USBTESTSPEED_FULL, + USBTESTSPEED_HIGH, + USBTESTSPEED_SUPER +} USBTESTSPEED; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +/** Some forward method declarations. */ +static DECLCALLBACK(int) usbTestParamsSetupReadWrite(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams); +static DECLCALLBACK(int) usbTestParamsSetupControlWrites(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams); + +/** Command line parameters */ +static const RTGETOPTDEF g_aCmdOptions[] = +{ + {"--device", 'd', RTGETOPT_REQ_STRING }, + {"--help", 'h', RTGETOPT_REQ_NOTHING}, + {"--exclude", 'e', RTGETOPT_REQ_UINT32}, + {"--exclude-all", 'a', RTGETOPT_REQ_NOTHING}, + {"--include", 'i', RTGETOPT_REQ_UINT32}, + {"--expected-speed", 's', RTGETOPT_REQ_STRING } +}; + +static USBTESTDESC g_aTests[] = +{ + /* pszTest fExcluded pfnParamsSetup */ + {"NOP", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk write", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk read", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk write variabe size", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk read variabe size", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk write", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk read", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk write variabe size", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk read variabe size", false, usbTestParamsSetupReadWrite}, + {"Chapter 9 Control Test", false, usbTestParamsSetupReadWrite}, + {"Queued control messaging", false, usbTestParamsSetupReadWrite}, + {"Unlink reads", false, usbTestParamsSetupReadWrite}, + {"Unlink writes", false, usbTestParamsSetupReadWrite}, + {"Set/Clear halts", false, usbTestParamsSetupReadWrite}, + {"Control writes", false, usbTestParamsSetupControlWrites}, + {"Isochronous write", false, usbTestParamsSetupReadWrite}, + {"Isochronous read", false, usbTestParamsSetupReadWrite}, + {"Bulk write unaligned (DMA)", false, usbTestParamsSetupReadWrite}, + {"Bulk read unaligned (DMA)", false, usbTestParamsSetupReadWrite}, + {"Bulk write unaligned (no DMA)", false, usbTestParamsSetupReadWrite}, + {"Bulk read unaligned (no DMA)", false, usbTestParamsSetupReadWrite}, + {"Control writes unaligned", false, usbTestParamsSetupControlWrites}, + {"Isochronous write unaligned", false, usbTestParamsSetupReadWrite}, + {"Isochronous read unaligned", false, usbTestParamsSetupReadWrite}, + {"Unlink queued Bulk", false, usbTestParamsSetupReadWrite} +}; + +/** The test handle. */ +static RTTEST g_hTest; +/** The expected device speed. */ +static USBTESTSPEED g_enmSpeed = USBTESTSPEED_ANY; + +/** + * Setup callback for basic read/write (bulk, isochronous) tests. + * + * @copydoc FNUSBTESTPARAMSSETUP + */ +static DECLCALLBACK(int) usbTestParamsSetupReadWrite(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams) +{ + NOREF(idxTest); + NOREF(pszTest); + + pParams->cIterations = 1000; + pParams->cbData = 512; + pParams->cbVariation = 512; + pParams->cSgLength = 32; + + return VINF_SUCCESS; +} + +/** + * Setup callback for the control writes test. + * + * @copydoc FNUSBTESTPARAMSSETUP + */ +static DECLCALLBACK(int) usbTestParamsSetupControlWrites(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams) +{ + NOREF(idxTest); + NOREF(pszTest); + + pParams->cIterations = 1000; + pParams->cbData = 512; + /* + * Must be smaller than cbData or the parameter check in the usbtest module fails, + * no idea yet why it must be this. + */ + pParams->cbVariation = 256; + pParams->cSgLength = 32; + + return VINF_SUCCESS; +} + +/** + * Shows tool usage text. + */ +static void usbTestUsage(PRTSTREAM pStrm) +{ + char szExec[RTPATH_MAX]; + RTStrmPrintf(pStrm, "usage: %s [options]\n", + RTPathFilename(RTProcGetExecutablePath(szExec, sizeof(szExec)))); + RTStrmPrintf(pStrm, "\n"); + RTStrmPrintf(pStrm, "options: \n"); + + + for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) + { + const char *pszHelp; + switch (g_aCmdOptions[i].iShort) + { + case 'h': + pszHelp = "Displays this help and exit"; + break; + case 'd': + pszHelp = "Use the specified test device"; + break; + case 'e': + pszHelp = "Exclude the given test id from the list"; + break; + case 'a': + pszHelp = "Exclude all tests from the list (useful to enable single tests later with --include)"; + break; + case 'i': + pszHelp = "Include the given test id in the list"; + break; + case 's': + pszHelp = "The device speed to expect"; + break; + default: + pszHelp = "Option undocumented"; + break; + } + char szOpt[256]; + RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); + RTStrmPrintf(pStrm, " %-30s%s\n", szOpt, pszHelp); + } +} + +/** + * Searches for a USB test device and returns the bus and device ID and the device speed. + */ +static int usbTestDeviceQueryBusAndDevId(uint16_t *pu16BusId, uint16_t *pu16DevId, USBTESTSPEED *penmSpeed) +{ + bool fFound = false; + +#define USBTEST_USB_DEV_SYSFS "/sys/bus/usb/devices/" + + RTDIR hDirUsb = NULL; + int rc = RTDirOpen(&hDirUsb, USBTEST_USB_DEV_SYSFS); + if (RT_SUCCESS(rc)) + { + do + { + RTDIRENTRY DirUsbBus; + rc = RTDirRead(hDirUsb, &DirUsbBus, NULL); + if ( RT_SUCCESS(rc) + && RTStrNCmp(DirUsbBus.szName, "usb", 3) + && RTLinuxSysFsExists(USBTEST_USB_DEV_SYSFS "%s/idVendor", DirUsbBus.szName)) + { + int64_t idVendor = 0; + int64_t idProduct = 0; + int64_t iBusId = 0; + int64_t iDevId = 0; + char aszSpeed[20]; + + rc = RTLinuxSysFsReadIntFile(16, &idVendor, USBTEST_USB_DEV_SYSFS "%s/idVendor", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadIntFile(16, &idProduct, USBTEST_USB_DEV_SYSFS "%s/idProduct", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadIntFile(16, &iBusId, USBTEST_USB_DEV_SYSFS "%s/busnum", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadIntFile(16, &iDevId, USBTEST_USB_DEV_SYSFS "%s/devnum", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadStrFile(&aszSpeed[0], sizeof(aszSpeed), NULL, USBTEST_USB_DEV_SYSFS "%s/speed", DirUsbBus.szName); + + if ( RT_SUCCESS(rc) + && idVendor == 0x0525 + && idProduct == 0xa4a0) + { + if (penmSpeed) + { + /* Parse the speed. */ + if (!RTStrCmp(&aszSpeed[0], "1.5")) + *penmSpeed = USBTESTSPEED_LOW; + else if (!RTStrCmp(&aszSpeed[0], "12")) + *penmSpeed = USBTESTSPEED_FULL; + else if (!RTStrCmp(&aszSpeed[0], "480")) + *penmSpeed = USBTESTSPEED_HIGH; + else if ( !RTStrCmp(&aszSpeed[0], "5000") + || !RTStrCmp(&aszSpeed[0], "10000")) + *penmSpeed = USBTESTSPEED_SUPER; + else + *penmSpeed = USBTESTSPEED_UNKNOWN; + } + + if (pu16BusId) + *pu16BusId = (uint16_t)iBusId; + if (pu16DevId) + *pu16DevId = (uint16_t)iDevId; + fFound = true; + break; + } + } + else if (rc != VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + } while ( RT_SUCCESS(rc) + && !fFound); + + if (rc == VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + RTDirClose(hDirUsb); + } + + if (RT_SUCCESS(rc) && !fFound) + rc = VERR_NOT_FOUND; + + return rc; +} + +/** + * Search for a USB test device and return the device path. + * + * @returns Path to the USB test device or NULL if none was found. + */ +static char *usbTestFindDevice(void) +{ + /* + * Very crude and quick way to search for the correct test device. + * Assumption is that the path looks like /dev/bus/usb/%3d/%3d. + */ + char *pszDevPath = NULL; + + RTDIR hDirUsb = NULL; + int rc = RTDirOpen(&hDirUsb, "/dev/bus/usb"); + if (RT_SUCCESS(rc)) + { + do + { + RTDIRENTRY DirUsbBus; + rc = RTDirRead(hDirUsb, &DirUsbBus, NULL); + if (RT_SUCCESS(rc)) + { + char aszPath[RTPATH_MAX + 1]; + RTStrPrintf(&aszPath[0], RT_ELEMENTS(aszPath), "/dev/bus/usb/%s", DirUsbBus.szName); + + RTDIR hDirUsbBus = NULL; + rc = RTDirOpen(&hDirUsbBus, &aszPath[0]); + if (RT_SUCCESS(rc)) + { + do + { + RTDIRENTRY DirUsbDev; + rc = RTDirRead(hDirUsbBus, &DirUsbDev, NULL); + if (RT_SUCCESS(rc)) + { + char aszPathDev[RTPATH_MAX + 1]; + RTStrPrintf(&aszPathDev[0], RT_ELEMENTS(aszPathDev), "/dev/bus/usb/%s/%s", + DirUsbBus.szName, DirUsbDev.szName); + + RTFILE hFileDev; + rc = RTFileOpen(&hFileDev, aszPathDev, RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_NONE); + if (RT_SUCCESS(rc)) + { + USBDEVDESC DevDesc; + + rc = RTFileRead(hFileDev, &DevDesc, sizeof(DevDesc), NULL); + RTFileClose(hFileDev); + + if ( RT_SUCCESS(rc) + && DevDesc.idVendor == 0x0525 + && DevDesc.idProduct == 0xa4a0) + pszDevPath = RTStrDup(aszPathDev); + } + + rc = VINF_SUCCESS; + } + else if (rc != VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + } while ( RT_SUCCESS(rc) + && !pszDevPath); + + rc = VINF_SUCCESS; + RTDirClose(hDirUsbBus); + } + } + else if (rc != VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + } while ( RT_SUCCESS(rc) + && !pszDevPath); + + RTDirClose(hDirUsb); + } + + return pszDevPath; +} + +static int usbTestIoctl(int iDevFd, int iInterface, PUSBTESTPARAMS pParams) +{ + struct usbdevfs_ioctl IoCtlData; + + IoCtlData.ifno = iInterface; + IoCtlData.ioctl_code = (int)USBTEST_REQUEST; + IoCtlData.data = pParams; + return ioctl(iDevFd, USBDEVFS_IOCTL, &IoCtlData); +} + +/** + * Test execution worker. + * + * @param pszDevice The device to use for testing. + */ +static void usbTestExec(const char *pszDevice) +{ + int iDevFd; + + RTTestSub(g_hTest, "Opening device"); + iDevFd = open(pszDevice, O_RDWR); + if (iDevFd != -1) + { + USBTESTPARAMS Params; + + RTTestPassed(g_hTest, "Opening device successful\n"); + + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + { + RTTestSub(g_hTest, g_aTests[i].pszName); + + if (g_aTests[i].fExcluded) + { + RTTestSkipped(g_hTest, "Excluded from list"); + continue; + } + + int rc = g_aTests[i].pfnParamsSetup(i, g_aTests[i].pszName, &Params); + if (RT_SUCCESS(rc)) + { + Params.idxTest = i; + + /* Assume the test interface has the number 0 for now. */ + int rcPosix = usbTestIoctl(iDevFd, 0, &Params); + if (rcPosix < 0 && errno == EOPNOTSUPP) + { + RTTestSkipped(g_hTest, "Not supported"); + continue; + } + + if (rcPosix < 0) + { + /* + * The error status code of the unlink testcase is + * offset by 2000 for the sync and 1000 for the sync code path + * (see drivers/usb/misc/usbtest.c in the Linux kernel sources). + * + * Adjust to the actual status code so converting doesn't assert. + */ + int iTmpErrno = errno; + if (iTmpErrno >= 2000) + iTmpErrno -= 2000; + else if (iTmpErrno >= 1000) + iTmpErrno -= 1000; + RTTestFailed(g_hTest, "Test failed with %Rrc\n", RTErrConvertFromErrno(iTmpErrno)); + } + else + { + uint64_t u64Ns = Params.TimeTest.tv_sec * RT_NS_1SEC + Params.TimeTest.tv_usec * RT_NS_1US; + RTTestValue(g_hTest, "Runtime", u64Ns, RTTESTUNIT_NS); + } + } + else + RTTestFailed(g_hTest, "Setting up test parameters failed with %Rrc\n", rc); + RTTestSubDone(g_hTest); + } + + close(iDevFd); + } + else + RTTestFailed(g_hTest, "Opening device failed with %Rrc\n", RTErrConvertFromErrno(errno)); + +} + +int main(int argc, char *argv[]) +{ + /* + * Init IPRT and globals. + */ + int rc = RTTestInitAndCreate("UsbTest", &g_hTest); + if (rc) + return rc; + + /* + * Default values. + */ + const char *pszDevice = NULL; + + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); + while ((rc = RTGetOpt(&GetState, &ValueUnion))) + { + switch (rc) + { + case 'h': + usbTestUsage(g_pStdOut); + return RTEXITCODE_SUCCESS; + case 'd': + pszDevice = ValueUnion.psz; + break; + case 's': + if (!RTStrICmp(ValueUnion.psz, "Low")) + g_enmSpeed = USBTESTSPEED_LOW; + else if (!RTStrICmp(ValueUnion.psz, "Full")) + g_enmSpeed = USBTESTSPEED_FULL; + else if (!RTStrICmp(ValueUnion.psz, "High")) + g_enmSpeed = USBTESTSPEED_HIGH; + else if (!RTStrICmp(ValueUnion.psz, "Super")) + g_enmSpeed = USBTESTSPEED_SUPER; + else + { + RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid speed passed to --expected-speed\n"); + RTTestErrorInc(g_hTest); + return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); + } + break; + case 'e': + if (ValueUnion.u32 < RT_ELEMENTS(g_aTests)) + g_aTests[ValueUnion.u32].fExcluded = true; + else + { + RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid test number passed to --exclude\n"); + RTTestErrorInc(g_hTest); + return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); + } + break; + case 'a': + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + g_aTests[i].fExcluded = true; + break; + case 'i': + if (ValueUnion.u32 < RT_ELEMENTS(g_aTests)) + g_aTests[ValueUnion.u32].fExcluded = false; + else + { + RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid test number passed to --include\n"); + RTTestErrorInc(g_hTest); + return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); + } + break; + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + /* + * Start testing. + */ + RTTestBanner(g_hTest); + + /* Find the first test device if none was given. */ + if (!pszDevice) + { + RTTestSub(g_hTest, "Detecting device"); + pszDevice = usbTestFindDevice(); + if (!pszDevice) + RTTestFailed(g_hTest, "Failed to find suitable device\n"); + + RTTestSubDone(g_hTest); + } + + if (pszDevice) + { + /* First check that the requested speed matches. */ + if (g_enmSpeed != USBTESTSPEED_ANY) + { + RTTestSub(g_hTest, "Checking correct device speed"); + + USBTESTSPEED enmSpeed = USBTESTSPEED_UNKNOWN; + rc = usbTestDeviceQueryBusAndDevId(NULL, NULL, &enmSpeed); + if (RT_SUCCESS(rc)) + { + if (enmSpeed == g_enmSpeed) + RTTestPassed(g_hTest, "Reported device speed matches requested speed\n"); + else + RTTestFailed(g_hTest, "Reported device speed doesn'match requested speed (%u vs %u)\n", + enmSpeed, g_enmSpeed); + } + else + RTTestFailed(g_hTest, "Failed to query device speed with rc=%Rrc\n", rc); + + RTTestSubDone(g_hTest); + } + usbTestExec(pszDevice); + } + + RTEXITCODE rcExit = RTTestSummaryAndDestroy(g_hTest); + return rcExit; +} + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestService.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestService.cpp new file mode 100644 index 00000000..0912ff51 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestService.cpp @@ -0,0 +1,1658 @@ +/* $Id: UsbTestService.cpp $ */ +/** @file + * UsbTestService - Remote USB test configuration and execution server. + */ + +/* + * 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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_DEFAULT +#include <iprt/alloca.h> +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/critsect.h> +#include <iprt/crc.h> +#include <iprt/ctype.h> +#include <iprt/dir.h> +#include <iprt/env.h> +#include <iprt/err.h> +#include <iprt/getopt.h> +#include <iprt/handle.h> +#include <iprt/initterm.h> +#include <iprt/json.h> +#include <iprt/list.h> +#include <iprt/log.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/param.h> +#include <iprt/path.h> +#include <iprt/pipe.h> +#include <iprt/poll.h> +#include <iprt/process.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/thread.h> + +#include "UsbTestServiceInternal.h" +#include "UsbTestServiceGadget.h" +#include "UsbTestServicePlatform.h" + + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + +#define UTS_USBIP_PORT_FIRST 3240 +#define UTS_USBIP_PORT_LAST 3340 + +/** + * UTS client state. + */ +typedef enum UTSCLIENTSTATE +{ + /** Invalid client state. */ + UTSCLIENTSTATE_INVALID = 0, + /** Client is initialising, only the HOWDY and BYE packets are allowed. */ + UTSCLIENTSTATE_INITIALISING, + /** Client is in fully cuntional state and ready to process all requests. */ + UTSCLIENTSTATE_READY, + /** Client is destroying. */ + UTSCLIENTSTATE_DESTROYING, + /** 32bit hack. */ + UTSCLIENTSTATE_32BIT_HACK = 0x7fffffff +} UTSCLIENTSTATE; + +/** + * UTS client instance. + */ +typedef struct UTSCLIENT +{ + /** List node for new clients. */ + RTLISTNODE NdLst; + /** The current client state. */ + UTSCLIENTSTATE enmState; + /** Transport backend specific data. */ + PUTSTRANSPORTCLIENT pTransportClient; + /** Client hostname. */ + char *pszHostname; + /** Gadget host handle. */ + UTSGADGETHOST hGadgetHost; + /** Handle fo the current configured gadget. */ + UTSGADGET hGadget; +} UTSCLIENT; +/** Pointer to a UTS client instance. */ +typedef UTSCLIENT *PUTSCLIENT; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** + * Transport layers. + */ +static const PCUTSTRANSPORT g_apTransports[] = +{ + &g_TcpTransport, + //&g_SerialTransport, + //&g_FileSysTransport, + //&g_GuestPropTransport, + //&g_TestDevTransport, +}; + +/** The select transport layer. */ +static PCUTSTRANSPORT g_pTransport; +/** The config path. */ +static char g_szCfgPath[RTPATH_MAX]; +/** The scratch path. */ +static char g_szScratchPath[RTPATH_MAX]; +/** The default scratch path. */ +static char g_szDefScratchPath[RTPATH_MAX]; +/** The CD/DVD-ROM path. */ +static char g_szCdRomPath[RTPATH_MAX]; +/** The default CD/DVD-ROM path. */ +static char g_szDefCdRomPath[RTPATH_MAX]; +/** The operating system short name. */ +static char g_szOsShortName[16]; +/** The CPU architecture short name. */ +static char g_szArchShortName[16]; +/** The combined "OS.arch" name. */ +static char g_szOsDotArchShortName[32]; +/** The combined "OS/arch" name. */ +static char g_szOsSlashArchShortName[32]; +/** The executable suffix. */ +static char g_szExeSuff[8]; +/** The shell script suffix. */ +static char g_szScriptSuff[8]; +/** Whether to display the output of the child process or not. */ +static bool g_fDisplayOutput = true; +/** Whether to terminate or not. + * @todo implement signals and stuff. */ +static bool volatile g_fTerminate = false; +/** Configuration AST. */ +static RTJSONVAL g_hCfgJson = NIL_RTJSONVAL; +/** Pipe for communicating with the serving thread about new clients. - read end */ +static RTPIPE g_hPipeR; +/** Pipe for communicating with the serving thread about new clients. - write end */ +static RTPIPE g_hPipeW; +/** Thread serving connected clients. */ +static RTTHREAD g_hThreadServing; +/** Critical section protecting the list of new clients. */ +static RTCRITSECT g_CritSectClients; +/** List of new clients waiting to be picked up by the client worker thread. */ +static RTLISTANCHOR g_LstClientsNew; +/** First USB/IP port we can use. */ +static uint16_t g_uUsbIpPortFirst = UTS_USBIP_PORT_FIRST; +/** Last USB/IP port we can use. */ +static uint16_t g_uUsbIpPortLast = UTS_USBIP_PORT_LAST; +/** Next free port. */ +static uint16_t g_uUsbIpPortNext = UTS_USBIP_PORT_FIRST; + + + +/** + * Returns the string represenation of the given state. + */ +static const char *utsClientStateStringify(UTSCLIENTSTATE enmState) +{ + switch (enmState) + { + case UTSCLIENTSTATE_INVALID: + return "INVALID"; + case UTSCLIENTSTATE_INITIALISING: + return "INITIALISING"; + case UTSCLIENTSTATE_READY: + return "READY"; + case UTSCLIENTSTATE_DESTROYING: + return "DESTROYING"; + case UTSCLIENTSTATE_32BIT_HACK: + default: + break; + } + + AssertMsgFailed(("Unknown state %#x\n", enmState)); + return "UNKNOWN"; +} + +/** + * Calculates the checksum value, zero any padding space and send the packet. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param pPkt The packet to send. Must point to a correctly + * aligned buffer. + */ +static int utsSendPkt(PUTSCLIENT pClient, PUTSPKTHDR pPkt) +{ + Assert(pPkt->cb >= sizeof(*pPkt)); + pPkt->uCrc32 = RTCrc32(pPkt->achOpcode, pPkt->cb - RT_UOFFSETOF(UTSPKTHDR, achOpcode)); + if (pPkt->cb != RT_ALIGN_32(pPkt->cb, UTSPKT_ALIGNMENT)) + memset((uint8_t *)pPkt + pPkt->cb, '\0', RT_ALIGN_32(pPkt->cb, UTSPKT_ALIGNMENT) - pPkt->cb); + + Log(("utsSendPkt: cb=%#x opcode=%.8s\n", pPkt->cb, pPkt->achOpcode)); + Log2(("%.*Rhxd\n", RT_MIN(pPkt->cb, 256), pPkt)); + int rc = g_pTransport->pfnSendPkt(pClient->pTransportClient, pPkt); + while (RT_UNLIKELY(rc == VERR_INTERRUPTED) && !g_fTerminate) + rc = g_pTransport->pfnSendPkt(pClient->pTransportClient, pPkt); + if (RT_FAILURE(rc)) + Log(("utsSendPkt: rc=%Rrc\n", rc)); + + return rc; +} + +/** + * Sends a babble reply and disconnects the client (if applicable). + * + * @param pClient The UTS client structure. + * @param pszOpcode The BABBLE opcode. + */ +static void utsReplyBabble(PUTSCLIENT pClient, const char *pszOpcode) +{ + UTSPKTHDR Reply; + Reply.cb = sizeof(Reply); + Reply.uCrc32 = 0; + memcpy(Reply.achOpcode, pszOpcode, sizeof(Reply.achOpcode)); + + g_pTransport->pfnBabble(pClient->pTransportClient, &Reply, 20*1000); +} + +/** + * Receive and validate a packet. + * + * Will send bable responses to malformed packets that results in a error status + * code. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param ppPktHdr Where to return the packet on success. Free + * with RTMemFree. + * @param fAutoRetryOnFailure Whether to retry on error. + */ +static int utsRecvPkt(PUTSCLIENT pClient, PPUTSPKTHDR ppPktHdr, bool fAutoRetryOnFailure) +{ + for (;;) + { + PUTSPKTHDR pPktHdr; + int rc = g_pTransport->pfnRecvPkt(pClient->pTransportClient, &pPktHdr); + if (RT_SUCCESS(rc)) + { + /* validate the packet. */ + if ( pPktHdr->cb >= sizeof(UTSPKTHDR) + && pPktHdr->cb < UTSPKT_MAX_SIZE) + { + Log2(("utsRecvPkt: pPktHdr=%p cb=%#x crc32=%#x opcode=%.8s\n" + "%.*Rhxd\n", + pPktHdr, pPktHdr->cb, pPktHdr->uCrc32, pPktHdr->achOpcode, RT_MIN(pPktHdr->cb, 256), pPktHdr)); + uint32_t uCrc32Calc = pPktHdr->uCrc32 != 0 + ? RTCrc32(&pPktHdr->achOpcode[0], pPktHdr->cb - RT_UOFFSETOF(UTSPKTHDR, achOpcode)) + : 0; + if (pPktHdr->uCrc32 == uCrc32Calc) + { + AssertCompileMemberSize(UTSPKTHDR, achOpcode, 8); + if ( RT_C_IS_UPPER(pPktHdr->achOpcode[0]) + && RT_C_IS_UPPER(pPktHdr->achOpcode[1]) + && (RT_C_IS_UPPER(pPktHdr->achOpcode[2]) || pPktHdr->achOpcode[2] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[3]) || pPktHdr->achOpcode[3] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[4]) || pPktHdr->achOpcode[4] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[5]) || pPktHdr->achOpcode[5] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[6]) || pPktHdr->achOpcode[6] == ' ') + && (RT_C_IS_PRINT(pPktHdr->achOpcode[7]) || pPktHdr->achOpcode[7] == ' ') + ) + { + Log(("utsRecvPkt: cb=%#x opcode=%.8s\n", pPktHdr->cb, pPktHdr->achOpcode)); + *ppPktHdr = pPktHdr; + return rc; + } + + rc = VERR_IO_BAD_COMMAND; + } + else + { + Log(("utsRecvPkt: cb=%#x opcode=%.8s crc32=%#x actual=%#x\n", + pPktHdr->cb, pPktHdr->achOpcode, pPktHdr->uCrc32, uCrc32Calc)); + rc = VERR_IO_CRC; + } + } + else + rc = VERR_IO_BAD_LENGTH; + + /* Send babble reply and disconnect the client if the transport is + connection oriented. */ + if (rc == VERR_IO_BAD_LENGTH) + utsReplyBabble(pClient, "BABBLE L"); + else if (rc == VERR_IO_CRC) + utsReplyBabble(pClient, "BABBLE C"); + else if (rc == VERR_IO_BAD_COMMAND) + utsReplyBabble(pClient, "BABBLE O"); + else + utsReplyBabble(pClient, "BABBLE "); + RTMemFree(pPktHdr); + } + + /* Try again or return failure? */ + if ( g_fTerminate + || rc != VERR_INTERRUPTED + || !fAutoRetryOnFailure + ) + { + Log(("utsRecvPkt: rc=%Rrc\n", rc)); + return rc; + } + } +} + +/** + * Make a simple reply, only status opcode. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pReply The reply packet. + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param cbExtra Bytes in addition to the header. + */ +static int utsReplyInternal(PUTSCLIENT pClient, PUTSPKTSTS pReply, const char *pszOpcode, size_t cbExtra) +{ + /* copy the opcode, don't be too strict in case of a padding screw up. */ + size_t cchOpcode = strlen(pszOpcode); + if (RT_LIKELY(cchOpcode == sizeof(pReply->Hdr.achOpcode))) + memcpy(pReply->Hdr.achOpcode, pszOpcode, sizeof(pReply->Hdr.achOpcode)); + else + { + Assert(cchOpcode == sizeof(pReply->Hdr.achOpcode)); + while (cchOpcode > 0 && pszOpcode[cchOpcode - 1] == ' ') + cchOpcode--; + AssertMsgReturn(cchOpcode < sizeof(pReply->Hdr.achOpcode), ("%d/'%.8s'\n", cchOpcode, pszOpcode), VERR_INTERNAL_ERROR_4); + memcpy(pReply->Hdr.achOpcode, pszOpcode, cchOpcode); + memset(&pReply->Hdr.achOpcode[cchOpcode], ' ', sizeof(pReply->Hdr.achOpcode) - cchOpcode); + } + + pReply->Hdr.cb = (uint32_t)sizeof(UTSPKTSTS) + (uint32_t)cbExtra; + pReply->Hdr.uCrc32 = 0; + + return utsSendPkt(pClient, &pReply->Hdr); +} + +/** + * Make a simple reply, only status opcode. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + */ +static int utsReplySimple(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr, const char *pszOpcode) +{ + UTSPKTSTS Pkt; + + RT_ZERO(Pkt); + Pkt.rcReq = VINF_SUCCESS; + Pkt.cchStsMsg = 0; + NOREF(pPktHdr); + return utsReplyInternal(pClient, &Pkt, pszOpcode, 0); +} + +/** + * Acknowledges a packet with success. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The original packet (for future use). + */ +static int utsReplyAck(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + return utsReplySimple(pClient, pPktHdr, "ACK "); +} + +/** + * Replies with a failure. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The original packet (for future use). + * @param rcReq Status code. + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param rcReq The status code of the request. + * @param pszDetailFmt Longer description of the problem (format string). + * @param va Format arguments. + */ +static int utsReplyFailureV(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr, const char *pszOpcode, int rcReq, const char *pszDetailFmt, va_list va) +{ + NOREF(pPktHdr); + union + { + UTSPKTSTS Hdr; + char ach[256]; + } uPkt; + + RT_ZERO(uPkt); + size_t cchDetail = RTStrPrintfV(&uPkt.ach[sizeof(UTSPKTSTS)], + sizeof(uPkt) - sizeof(UTSPKTSTS), + pszDetailFmt, va); + uPkt.Hdr.rcReq = rcReq; + uPkt.Hdr.cchStsMsg = cchDetail; + return utsReplyInternal(pClient, &uPkt.Hdr, pszOpcode, cchDetail + 1); +} + +/** + * Replies with a failure. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The original packet (for future use). + * @param pszOpcode The status opcode. Exactly 8 chars long, padd + * with space. + * @param rcReq Status code. + * @param pszDetailFmt Longer description of the problem (format string). + * @param ... Format arguments. + */ +static int utsReplyFailure(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr, const char *pszOpcode, int rcReq, const char *pszDetailFmt, ...) +{ + va_list va; + va_start(va, pszDetailFmt); + int rc = utsReplyFailureV(pClient, pPktHdr, pszOpcode, rcReq, pszDetailFmt, va); + va_end(va); + return rc; +} + +/** + * Replies according to the return code. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The packet to reply to. + * @param rcOperation The status code to report. + * @param pszOperationFmt The operation that failed. Typically giving the + * function call with important arguments. + * @param ... Arguments to the format string. + */ +static int utsReplyRC(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr, int rcOperation, const char *pszOperationFmt, ...) +{ + if (RT_SUCCESS(rcOperation)) + return utsReplyAck(pClient, pPktHdr); + + char szOperation[128]; + va_list va; + va_start(va, pszOperationFmt); + RTStrPrintfV(szOperation, sizeof(szOperation), pszOperationFmt, va); + va_end(va); + + return utsReplyFailure(pClient, pPktHdr, "FAILED ", rcOperation, "%s failed with rc=%Rrc (opcode '%.8s')", + szOperation, rcOperation, pPktHdr->achOpcode); +} + +#if 0 /* unused */ +/** + * Signal a bad packet minum size. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The packet to reply to. + * @param cbMin The minimum size. + */ +static int utsReplyBadMinSize(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr, size_t cbMin) +{ + return utsReplyFailure(pClient, pPktHdr, "BAD SIZE", VERR_INVALID_PARAMETER, "Expected at least %zu bytes, got %u (opcode '%.8s')", + cbMin, pPktHdr->cb, pPktHdr->achOpcode); +} +#endif + +/** + * Signal a bad packet exact size. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The packet to reply to. + * @param cb The wanted size. + */ +static int utsReplyBadSize(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr, size_t cb) +{ + return utsReplyFailure(pClient, pPktHdr, "BAD SIZE", VERR_INVALID_PARAMETER, "Expected at %zu bytes, got %u (opcode '%.8s')", + cb, pPktHdr->cb, pPktHdr->achOpcode); +} + +#if 0 /* unused */ +/** + * Deals with a command that isn't implemented yet. + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The packet which opcode isn't implemented. + */ +static int utsReplyNotImplemented(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + return utsReplyFailure(pClient, pPktHdr, "NOT IMPL", VERR_NOT_IMPLEMENTED, "Opcode '%.8s' is not implemented", pPktHdr->achOpcode); +} +#endif + +/** + * Deals with a unknown command. + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The packet to reply to. + */ +static int utsReplyUnknown(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + return utsReplyFailure(pClient, pPktHdr, "UNKNOWN ", VERR_NOT_FOUND, "Opcode '%.8s' is not known", pPktHdr->achOpcode); +} + +/** + * Deals with a command which contains an unterminated string. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The packet containing the unterminated string. + */ +static int utsReplyBadStrTermination(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + return utsReplyFailure(pClient, pPktHdr, "BAD TERM", VERR_INVALID_PARAMETER, "Opcode '%.8s' contains an unterminated string", pPktHdr->achOpcode); +} + +/** + * Deals with a command sent in an invalid client state. + * + * @returns IPRT status code of the send. + * @param pClient The UTS client structure. + * @param pPktHdr The packet containing the unterminated string. + */ +static int utsReplyInvalidState(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + return utsReplyFailure(pClient, pPktHdr, "INVSTATE", VERR_INVALID_STATE, "Opcode '%.8s' is not supported at client state '%s", + pPktHdr->achOpcode, utsClientStateStringify(pClient->enmState)); +} + +/** + * Parses an unsigned integer from the given value string. + * + * @returns IPRT status code. + * @retval VERR_OUT_OF_RANGE if the parsed value exceeds the given maximum. + * @param pszVal The value string. + * @param uMax The maximum value. + * @param pu64 Where to store the parsed number on success. + */ +static int utsDoGadgetCreateCfgParseUInt(const char *pszVal, uint64_t uMax, uint64_t *pu64) +{ + int rc = RTStrToUInt64Ex(pszVal, NULL, 0, pu64); + if (RT_SUCCESS(rc)) + { + if (*pu64 > uMax) + rc = VERR_OUT_OF_RANGE; + } + + return rc; +} + +/** + * Parses a signed integer from the given value string. + * + * @returns IPRT status code. + * @retval VERR_OUT_OF_RANGE if the parsed value exceeds the given range. + * @param pszVal The value string. + * @param iMin The minimum value. + * @param iMax The maximum value. + * @param pi64 Where to store the parsed number on success. + */ +static int utsDoGadgetCreateCfgParseInt(const char *pszVal, int64_t iMin, int64_t iMax, int64_t *pi64) +{ + int rc = RTStrToInt64Ex(pszVal, NULL, 0, pi64); + if (RT_SUCCESS(rc)) + { + if ( *pi64 < iMin + || *pi64 > iMax) + rc = VERR_OUT_OF_RANGE; + } + + return rc; +} + +/** + * Parses the given config item and fills in the value according to the given type. + * + * @returns IPRT status code. + * @param pCfgItem The config item to parse. + * @param u32Type The config type. + * @param pszVal The value encoded as a string. + */ +static int utsDoGadgetCreateCfgParseItem(PUTSGADGETCFGITEM pCfgItem, uint32_t u32Type, + const char *pszVal) +{ + int rc = VINF_SUCCESS; + + switch (u32Type) + { + case UTSPKT_GDGT_CFG_ITEM_TYPE_BOOLEAN: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_BOOLEAN; + if ( RTStrICmp(pszVal, "enabled") + || RTStrICmp(pszVal, "1") + || RTStrICmp(pszVal, "true")) + pCfgItem->Val.u.f = true; + else if ( RTStrICmp(pszVal, "disabled") + || RTStrICmp(pszVal, "0") + || RTStrICmp(pszVal, "false")) + pCfgItem->Val.u.f = false; + else + rc = VERR_INVALID_PARAMETER; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_STRING: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_STRING; + pCfgItem->Val.u.psz = RTStrDup(pszVal); + if (!pCfgItem->Val.u.psz) + rc = VERR_NO_STR_MEMORY; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_UINT8: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_UINT8; + + uint64_t u64; + rc = utsDoGadgetCreateCfgParseUInt(pszVal, UINT8_MAX, &u64); + if (RT_SUCCESS(rc)) + pCfgItem->Val.u.u8 = (uint8_t)u64; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_UINT16: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_UINT16; + + uint64_t u64; + rc = utsDoGadgetCreateCfgParseUInt(pszVal, UINT16_MAX, &u64); + if (RT_SUCCESS(rc)) + pCfgItem->Val.u.u16 = (uint16_t)u64; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_UINT32: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_UINT32; + + uint64_t u64; + rc = utsDoGadgetCreateCfgParseUInt(pszVal, UINT32_MAX, &u64); + if (RT_SUCCESS(rc)) + pCfgItem->Val.u.u32 = (uint32_t)u64; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_UINT64: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_UINT64; + rc = utsDoGadgetCreateCfgParseUInt(pszVal, UINT64_MAX, &pCfgItem->Val.u.u64); + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_INT8: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_INT8; + + int64_t i64; + rc = utsDoGadgetCreateCfgParseInt(pszVal, INT8_MIN, INT8_MAX, &i64); + if (RT_SUCCESS(rc)) + pCfgItem->Val.u.i8 = (int8_t)i64; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_INT16: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_INT16; + + int64_t i64; + rc = utsDoGadgetCreateCfgParseInt(pszVal, INT16_MIN, INT16_MAX, &i64); + if (RT_SUCCESS(rc)) + pCfgItem->Val.u.i16 = (int16_t)i64; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_INT32: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_INT32; + + int64_t i64; + rc = utsDoGadgetCreateCfgParseInt(pszVal, INT32_MIN, INT32_MAX, &i64); + if (RT_SUCCESS(rc)) + pCfgItem->Val.u.i32 = (int32_t)i64; + break; + } + case UTSPKT_GDGT_CFG_ITEM_TYPE_INT64: + { + pCfgItem->Val.enmType = UTSGADGETCFGTYPE_INT64; + rc = utsDoGadgetCreateCfgParseInt(pszVal, INT64_MIN, INT64_MAX, &pCfgItem->Val.u.i64); + break; + } + default: + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + +/** + * Creates the configuration from the given GADGET CREATE packet. + * + * @returns IPRT status code. + * @param pCfgItem The first config item header in the request packet. + * @param cCfgItems Number of config items in the packet to parse. + * @param cbPkt Number of bytes left in the packet for the config data. + * @param paCfg The array of configuration items to fill. + */ +static int utsDoGadgetCreateFillCfg(PUTSPKTREQGDGTCTORCFGITEM pCfgItem, unsigned cCfgItems, + size_t cbPkt, PUTSGADGETCFGITEM paCfg) +{ + int rc = VINF_SUCCESS; + unsigned idxCfg = 0; + + while ( RT_SUCCESS(rc) + && cCfgItems + && cbPkt) + { + if (cbPkt >= sizeof(UTSPKTREQGDGTCTORCFGITEM)) + { + cbPkt -= sizeof(UTSPKTREQGDGTCTORCFGITEM); + if (pCfgItem->u32KeySize + pCfgItem->u32ValSize >= cbPkt) + { + const char *pszKey = (const char *)(pCfgItem + 1); + const char *pszVal = pszKey + pCfgItem->u32KeySize; + + /* Validate termination. */ + if ( *(pszKey + pCfgItem->u32KeySize - 1) != '\0' + || *(pszVal + pCfgItem->u32ValSize - 1) != '\0') + rc = VERR_INVALID_PARAMETER; + else + { + paCfg[idxCfg].pszKey = RTStrDup(pszKey); + + rc = utsDoGadgetCreateCfgParseItem(&paCfg[idxCfg], pCfgItem->u32Type, pszVal); + if (RT_SUCCESS(rc)) + { + cbPkt -= pCfgItem->u32KeySize + pCfgItem->u32ValSize; + cCfgItems--; + idxCfg++; + pCfgItem = (PUTSPKTREQGDGTCTORCFGITEM)(pszVal + pCfgItem->u32ValSize); + } + } + } + else + rc = VERR_INVALID_PARAMETER; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + +/** + * Verifies and acknowledges a "BYE" request. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param pPktHdr The howdy packet. + */ +static int utsDoBye(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + int rc; + if (pPktHdr->cb == sizeof(UTSPKTHDR)) + rc = utsReplyAck(pClient, pPktHdr); + else + rc = utsReplyBadSize(pClient, pPktHdr, sizeof(UTSPKTHDR)); + return rc; +} + +/** + * Verifies and acknowledges a "HOWDY" request. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param pPktHdr The howdy packet. + */ +static int utsDoHowdy(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + int rc = VINF_SUCCESS; + + if (pPktHdr->cb != sizeof(UTSPKTREQHOWDY)) + return utsReplyBadSize(pClient, pPktHdr, sizeof(UTSPKTREQHOWDY)); + + if (pClient->enmState != UTSCLIENTSTATE_INITIALISING) + return utsReplyInvalidState(pClient, pPktHdr); + + PUTSPKTREQHOWDY pReq = (PUTSPKTREQHOWDY)pPktHdr; + + if (pReq->uVersion != UTS_PROTOCOL_VS) + return utsReplyRC(pClient, pPktHdr, VERR_VERSION_MISMATCH, "The given version %#x is not supported", pReq->uVersion); + + /* Verify hostname string. */ + if (pReq->cchHostname >= sizeof(pReq->achHostname)) + return utsReplyBadSize(pClient, pPktHdr, sizeof(pReq->achHostname) - 1); + + if (pReq->achHostname[pReq->cchHostname] != '\0') + return utsReplyBadStrTermination(pClient, pPktHdr); + + /* Extract string. */ + pClient->pszHostname = RTStrDup(&pReq->achHostname[0]); + if (!pClient->pszHostname) + return utsReplyRC(pClient, pPktHdr, VERR_NO_MEMORY, "Failed to allocate memory for the hostname string"); + + if (pReq->fUsbConn & UTSPKT_HOWDY_CONN_F_PHYSICAL) + return utsReplyRC(pClient, pPktHdr, VERR_NOT_SUPPORTED, "Physical connections are not yet supported"); + + if (pReq->fUsbConn & UTSPKT_HOWDY_CONN_F_USBIP) + { + /* Set up the USB/IP server, find an unused port we can start the server on. */ + UTSGADGETCFGITEM aCfg[2]; + + uint16_t uPort = g_uUsbIpPortNext; + + if (g_uUsbIpPortNext == g_uUsbIpPortLast) + g_uUsbIpPortNext = g_uUsbIpPortFirst; + else + g_uUsbIpPortNext++; + + aCfg[0].pszKey = "UsbIp/Port"; + aCfg[0].Val.enmType = UTSGADGETCFGTYPE_UINT16; + aCfg[0].Val.u.u16 = uPort; + aCfg[1].pszKey = NULL; + + rc = utsGadgetHostCreate(UTSGADGETHOSTTYPE_USBIP, &aCfg[0], &pClient->hGadgetHost); + if (RT_SUCCESS(rc)) + { + /* Send the reply with the configured USB/IP port. */ + UTSPKTREPHOWDY Rep; + + RT_ZERO(Rep); + + Rep.uVersion = UTS_PROTOCOL_VS; + Rep.fUsbConn = UTSPKT_HOWDY_CONN_F_USBIP; + Rep.uUsbIpPort = uPort; + Rep.cUsbIpDevices = 1; + Rep.cPhysicalDevices = 0; + + rc = utsReplyInternal(pClient, &Rep.Sts, "ACK ", sizeof(Rep) - sizeof(UTSPKTSTS)); + if (RT_SUCCESS(rc)) + { + g_pTransport->pfnNotifyHowdy(pClient->pTransportClient); + pClient->enmState = UTSCLIENTSTATE_READY; + RTDirRemoveRecursive(g_szScratchPath, RTDIRRMREC_F_CONTENT_ONLY); + } + } + else + return utsReplyRC(pClient, pPktHdr, rc, "Creating the USB/IP gadget host failed"); + } + else + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_PARAMETER, "No access method requested"); + + return rc; +} + +/** + * Verifies and processes a "GADGET CREATE" request. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param pPktHdr The gadget create packet. + */ +static int utsDoGadgetCreate(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + int rc = VINF_SUCCESS; + + if (pPktHdr->cb < sizeof(UTSPKTREQGDGTCTOR)) + return utsReplyBadSize(pClient, pPktHdr, sizeof(UTSPKTREQGDGTCTOR)); + + if ( pClient->enmState != UTSCLIENTSTATE_READY + || pClient->hGadgetHost == NIL_UTSGADGETHOST) + return utsReplyInvalidState(pClient, pPktHdr); + + PUTSPKTREQGDGTCTOR pReq = (PUTSPKTREQGDGTCTOR)pPktHdr; + + if (pReq->u32GdgtType != UTSPKT_GDGT_CREATE_TYPE_TEST) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_PARAMETER, "The given gadget type is not supported"); + + if (pReq->u32GdgtAccess != UTSPKT_GDGT_CREATE_ACCESS_USBIP) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_PARAMETER, "The given gadget access method is not supported"); + + PUTSGADGETCFGITEM paCfg = NULL; + if (pReq->u32CfgItems > 0) + { + paCfg = (PUTSGADGETCFGITEM)RTMemAllocZ((pReq->u32CfgItems + 1) * sizeof(UTSGADGETCFGITEM)); + if (RT_UNLIKELY(!paCfg)) + return utsReplyRC(pClient, pPktHdr, VERR_NO_MEMORY, "Failed to allocate memory for configration items"); + + rc = utsDoGadgetCreateFillCfg((PUTSPKTREQGDGTCTORCFGITEM)(pReq + 1), pReq->u32CfgItems, + pPktHdr->cb - sizeof(UTSPKTREQGDGTCTOR), paCfg); + if (RT_FAILURE(rc)) + { + RTMemFree(paCfg); + return utsReplyRC(pClient, pPktHdr, rc, "Failed to parse configuration"); + } + } + + rc = utsGadgetCreate(pClient->hGadgetHost, UTSGADGETCLASS_TEST, paCfg, &pClient->hGadget); + if (RT_SUCCESS(rc)) + { + UTSPKTREPGDGTCTOR Rep; + RT_ZERO(Rep); + + Rep.idGadget = 0; + Rep.u32BusId = utsGadgetGetBusId(pClient->hGadget); + Rep.u32DevId = utsGadgetGetDevId(pClient->hGadget); + rc = utsReplyInternal(pClient, &Rep.Sts, "ACK ", sizeof(Rep) - sizeof(UTSPKTSTS)); + } + else + rc = utsReplyRC(pClient, pPktHdr, rc, "Failed to create gadget with %Rrc\n", rc); + + return rc; +} + +/** + * Verifies and processes a "GADGET DESTROY" request. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param pPktHdr The gadget destroy packet. + */ +static int utsDoGadgetDestroy(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(UTSPKTREQGDGTDTOR)) + return utsReplyBadSize(pClient, pPktHdr, sizeof(UTSPKTREQGDGTDTOR)); + + if ( pClient->enmState != UTSCLIENTSTATE_READY + || pClient->hGadgetHost == NIL_UTSGADGETHOST) + return utsReplyInvalidState(pClient, pPktHdr); + + PUTSPKTREQGDGTDTOR pReq = (PUTSPKTREQGDGTDTOR)pPktHdr; + + if (pReq->idGadget != 0) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_HANDLE, "The given gadget handle is invalid"); + if (pClient->hGadget == NIL_UTSGADGET) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_STATE, "The gadget is not set up"); + + utsGadgetRelease(pClient->hGadget); + pClient->hGadget = NIL_UTSGADGET; + + return utsReplyAck(pClient, pPktHdr); +} + +/** + * Verifies and processes a "GADGET CONNECT" request. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param pPktHdr The gadget connect packet. + */ +static int utsDoGadgetConnect(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(UTSPKTREQGDGTCNCT)) + return utsReplyBadSize(pClient, pPktHdr, sizeof(UTSPKTREQGDGTCNCT)); + + if ( pClient->enmState != UTSCLIENTSTATE_READY + || pClient->hGadgetHost == NIL_UTSGADGETHOST) + return utsReplyInvalidState(pClient, pPktHdr); + + PUTSPKTREQGDGTCNCT pReq = (PUTSPKTREQGDGTCNCT)pPktHdr; + + if (pReq->idGadget != 0) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_HANDLE, "The given gadget handle is invalid"); + if (pClient->hGadget == NIL_UTSGADGET) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_STATE, "The gadget is not set up"); + + int rc = utsGadgetConnect(pClient->hGadget); + if (RT_SUCCESS(rc)) + rc = utsReplyAck(pClient, pPktHdr); + else + rc = utsReplyRC(pClient, pPktHdr, rc, "Failed to connect the gadget"); + + return rc; +} + +/** + * Verifies and processes a "GADGET DISCONNECT" request. + * + * @returns IPRT status code. + * @param pClient The UTS client structure. + * @param pPktHdr The gadget disconnect packet. + */ +static int utsDoGadgetDisconnect(PUTSCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + if (pPktHdr->cb != sizeof(UTSPKTREQGDGTDCNT)) + return utsReplyBadSize(pClient, pPktHdr, sizeof(UTSPKTREQGDGTDCNT)); + + if ( pClient->enmState != UTSCLIENTSTATE_READY + || pClient->hGadgetHost == NIL_UTSGADGETHOST) + return utsReplyInvalidState(pClient, pPktHdr); + + PUTSPKTREQGDGTDCNT pReq = (PUTSPKTREQGDGTDCNT)pPktHdr; + + if (pReq->idGadget != 0) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_HANDLE, "The given gadget handle is invalid"); + if (pClient->hGadget == NIL_UTSGADGET) + return utsReplyRC(pClient, pPktHdr, VERR_INVALID_STATE, "The gadget is not set up"); + + int rc = utsGadgetDisconnect(pClient->hGadget); + if (RT_SUCCESS(rc)) + rc = utsReplyAck(pClient, pPktHdr); + else + rc = utsReplyRC(pClient, pPktHdr, rc, "Failed to disconnect the gadget"); + + return rc; +} + +/** + * Main request processing routine for each client. + * + * @returns IPRT status code. + * @param pClient The UTS client structure sending the request. + */ +static int utsClientReqProcess(PUTSCLIENT pClient) +{ + /* + * Read client command packet and process it. + */ + PUTSPKTHDR pPktHdr = NULL; + int rc = utsRecvPkt(pClient, &pPktHdr, true /*fAutoRetryOnFailure*/); + if (RT_FAILURE(rc)) + return rc; + + /* + * Do a string switch on the opcode bit. + */ + /* Connection: */ + if ( utsIsSameOpcode(pPktHdr, UTSPKT_OPCODE_HOWDY)) + rc = utsDoHowdy(pClient, pPktHdr); + else if (utsIsSameOpcode(pPktHdr, UTSPKT_OPCODE_BYE)) + rc = utsDoBye(pClient, pPktHdr); + /* Gadget API. */ + else if (utsIsSameOpcode(pPktHdr, UTSPKT_OPCODE_GADGET_CREATE)) + rc = utsDoGadgetCreate(pClient, pPktHdr); + else if (utsIsSameOpcode(pPktHdr, UTSPKT_OPCODE_GADGET_DESTROY)) + rc = utsDoGadgetDestroy(pClient, pPktHdr); + else if (utsIsSameOpcode(pPktHdr, UTSPKT_OPCODE_GADGET_CONNECT)) + rc = utsDoGadgetConnect(pClient, pPktHdr); + else if (utsIsSameOpcode(pPktHdr, UTSPKT_OPCODE_GADGET_DISCONNECT)) + rc = utsDoGadgetDisconnect(pClient, pPktHdr); + /* Misc: */ + else + rc = utsReplyUnknown(pClient, pPktHdr); + + RTMemFree(pPktHdr); + + return rc; +} + +/** + * Destroys a client instance. + * + * @param pClient The UTS client structure. + */ +static void utsClientDestroy(PUTSCLIENT pClient) +{ + if (pClient->pszHostname) + RTStrFree(pClient->pszHostname); + if (pClient->hGadget != NIL_UTSGADGET) + utsGadgetRelease(pClient->hGadget); + if (pClient->hGadgetHost != NIL_UTSGADGETHOST) + utsGadgetHostRelease(pClient->hGadgetHost); + RTMemFree(pClient); +} + +/** + * The main thread worker serving the clients. + */ +static DECLCALLBACK(int) utsClientWorker(RTTHREAD hThread, void *pvUser) +{ + RT_NOREF2(hThread, pvUser); + unsigned cClientsMax = 0; + unsigned cClientsCur = 0; + PUTSCLIENT *papClients = NULL; + RTPOLLSET hPollSet; + + int rc = RTPollSetCreate(&hPollSet); + if (RT_FAILURE(rc)) + return rc; + + /* Add the pipe to the poll set. */ + rc = RTPollSetAddPipe(hPollSet, g_hPipeR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, 0); + if (RT_SUCCESS(rc)) + { + while (!g_fTerminate) + { + uint32_t fEvts; + uint32_t uId; + rc = RTPoll(hPollSet, RT_INDEFINITE_WAIT, &fEvts, &uId); + if (RT_SUCCESS(rc)) + { + if (uId == 0) + { + if (fEvts & RTPOLL_EVT_ERROR) + break; + + /* We got woken up because of a new client. */ + Assert(fEvts & RTPOLL_EVT_READ); + + uint8_t bRead; + size_t cbRead = 0; + rc = RTPipeRead(g_hPipeR, &bRead, 1, &cbRead); + AssertRC(rc); + + RTCritSectEnter(&g_CritSectClients); + /* Walk the list and add all new clients. */ + PUTSCLIENT pIt, pItNext; + RTListForEachSafe(&g_LstClientsNew, pIt, pItNext, UTSCLIENT, NdLst) + { + RTListNodeRemove(&pIt->NdLst); + Assert(cClientsCur <= cClientsMax); + if (cClientsCur == cClientsMax) + { + /* Realloc to accommodate for the new clients. */ + PUTSCLIENT *papClientsNew = (PUTSCLIENT *)RTMemReallocZ(papClients, cClientsMax * sizeof(PUTSCLIENT), (cClientsMax + 10) * sizeof(PUTSCLIENT)); + if (RT_LIKELY(papClientsNew)) + { + cClientsMax += 10; + papClients = papClientsNew; + } + } + + if (cClientsCur < cClientsMax) + { + /* Find a free slot in the client array. */ + unsigned idxSlt = 0; + while ( idxSlt < cClientsMax + && papClients[idxSlt] != NULL) + idxSlt++; + + rc = g_pTransport->pfnPollSetAdd(hPollSet, pIt->pTransportClient, idxSlt + 1); + if (RT_SUCCESS(rc)) + { + cClientsCur++; + papClients[idxSlt] = pIt; + } + else + { + g_pTransport->pfnNotifyBye(pIt->pTransportClient); + utsClientDestroy(pIt); + } + } + else + { + g_pTransport->pfnNotifyBye(pIt->pTransportClient); + utsClientDestroy(pIt); + } + } + RTCritSectLeave(&g_CritSectClients); + } + else + { + /* Client sends a request, pick the right client and process it. */ + PUTSCLIENT pClient = papClients[uId - 1]; + AssertPtr(pClient); + if (fEvts & RTPOLL_EVT_READ) + rc = utsClientReqProcess(pClient); + + if ( (fEvts & RTPOLL_EVT_ERROR) + || RT_FAILURE(rc)) + { + /* Close connection and remove client from array. */ + rc = g_pTransport->pfnPollSetRemove(hPollSet, pClient->pTransportClient, uId); + AssertRC(rc); + + g_pTransport->pfnNotifyBye(pClient->pTransportClient); + papClients[uId - 1] = NULL; + cClientsCur--; + utsClientDestroy(pClient); + } + } + } + } + } + + RTPollSetDestroy(hPollSet); + + return rc; +} + +/** + * The main loop. + * + * @returns exit code. + */ +static RTEXITCODE utsMainLoop(void) +{ + RTEXITCODE enmExitCode = RTEXITCODE_SUCCESS; + while (!g_fTerminate) + { + /* + * Wait for new connection and spin off a new thread + * for every new client. + */ + PUTSTRANSPORTCLIENT pTransportClient; + int rc = g_pTransport->pfnWaitForConnect(&pTransportClient); + if (RT_FAILURE(rc)) + continue; + + /* + * New connection, create new client structure and spin of + * the request handling thread. + */ + PUTSCLIENT pClient = (PUTSCLIENT)RTMemAllocZ(sizeof(UTSCLIENT)); + if (RT_LIKELY(pClient)) + { + pClient->enmState = UTSCLIENTSTATE_INITIALISING; + pClient->pTransportClient = pTransportClient; + pClient->pszHostname = NULL; + pClient->hGadgetHost = NIL_UTSGADGETHOST; + pClient->hGadget = NIL_UTSGADGET; + + /* Add client to the new list and inform the worker thread. */ + RTCritSectEnter(&g_CritSectClients); + RTListAppend(&g_LstClientsNew, &pClient->NdLst); + RTCritSectLeave(&g_CritSectClients); + + size_t cbWritten = 0; + rc = RTPipeWrite(g_hPipeW, "", 1, &cbWritten); + if (RT_FAILURE(rc)) + RTMsgError("Failed to inform worker thread of a new client"); + } + else + { + RTMsgError("Creating new client structure failed with out of memory error\n"); + g_pTransport->pfnNotifyBye(pTransportClient); + } + + + } + + return enmExitCode; +} + +/** + * Initializes the global UTS state. + * + * @returns IPRT status code. + */ +static int utsInit(void) +{ + int rc = VINF_SUCCESS; + PRTERRINFO pErrInfo = NULL; + + RTListInit(&g_LstClientsNew); + + rc = RTJsonParseFromFile(&g_hCfgJson, g_szCfgPath, pErrInfo); + if (RT_SUCCESS(rc)) + { + rc = utsPlatformInit(); + if (RT_SUCCESS(rc)) + { + rc = RTCritSectInit(&g_CritSectClients); + if (RT_SUCCESS(rc)) + { + rc = RTPipeCreate(&g_hPipeR, &g_hPipeW, 0); + if (RT_SUCCESS(rc)) + { + /* Spin off the thread serving connections. */ + rc = RTThreadCreate(&g_hThreadServing, utsClientWorker, NULL, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE, + "USBTSTSRV"); + if (RT_SUCCESS(rc)) + return VINF_SUCCESS; + else + RTMsgError("Creating the client worker thread failed with %Rrc\n", rc); + + RTPipeClose(g_hPipeR); + RTPipeClose(g_hPipeW); + } + else + RTMsgError("Creating communications pipe failed with %Rrc\n", rc); + + RTCritSectDelete(&g_CritSectClients); + } + else + RTMsgError("Creating global critical section failed with %Rrc\n", rc); + + RTJsonValueRelease(g_hCfgJson); + } + else + RTMsgError("Initializing the platform failed with %Rrc\n", rc); + } + else + { + if (RTErrInfoIsSet(pErrInfo)) + { + RTMsgError("Failed to parse config with detailed error: %s (%Rrc)\n", + pErrInfo->pszMsg, pErrInfo->rc); + RTErrInfoFree(pErrInfo); + } + else + RTMsgError("Failed to parse config with unknown error (%Rrc)\n", rc); + } + + return rc; +} + +/** + * Determines the default configuration. + */ +static void utsSetDefaults(void) +{ + /* + * OS and ARCH. + */ + AssertCompile(sizeof(KBUILD_TARGET) <= sizeof(g_szOsShortName)); + strcpy(g_szOsShortName, KBUILD_TARGET); + + AssertCompile(sizeof(KBUILD_TARGET_ARCH) <= sizeof(g_szArchShortName)); + strcpy(g_szArchShortName, KBUILD_TARGET_ARCH); + + AssertCompile(sizeof(KBUILD_TARGET) + sizeof(KBUILD_TARGET_ARCH) <= sizeof(g_szOsDotArchShortName)); + strcpy(g_szOsDotArchShortName, KBUILD_TARGET); + g_szOsDotArchShortName[sizeof(KBUILD_TARGET) - 1] = '.'; + strcpy(&g_szOsDotArchShortName[sizeof(KBUILD_TARGET)], KBUILD_TARGET_ARCH); + + AssertCompile(sizeof(KBUILD_TARGET) + sizeof(KBUILD_TARGET_ARCH) <= sizeof(g_szOsSlashArchShortName)); + strcpy(g_szOsSlashArchShortName, KBUILD_TARGET); + g_szOsSlashArchShortName[sizeof(KBUILD_TARGET) - 1] = '/'; + strcpy(&g_szOsSlashArchShortName[sizeof(KBUILD_TARGET)], KBUILD_TARGET_ARCH); + +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + strcpy(g_szExeSuff, ".exe"); + strcpy(g_szScriptSuff, ".cmd"); +#else + strcpy(g_szExeSuff, ""); + strcpy(g_szScriptSuff, ".sh"); +#endif + + /* + * The CD/DVD-ROM location. + */ + /** @todo do a better job here :-) */ +#ifdef RT_OS_WINDOWS + strcpy(g_szDefCdRomPath, "D:/"); +#elif defined(RT_OS_OS2) + strcpy(g_szDefCdRomPath, "D:/"); +#else + if (RTDirExists("/media")) + strcpy(g_szDefCdRomPath, "/media/cdrom"); + else + strcpy(g_szDefCdRomPath, "/mnt/cdrom"); +#endif + strcpy(g_szCdRomPath, g_szDefCdRomPath); + + /* + * Temporary directory. + */ + int rc = RTPathTemp(g_szDefScratchPath, sizeof(g_szDefScratchPath)); + if (RT_SUCCESS(rc)) +#if defined(RT_OS_OS2) || defined(RT_OS_WINDOWS) || defined(RT_OS_DOS) + rc = RTPathAppend(g_szDefScratchPath, sizeof(g_szDefScratchPath), "uts-XXXX.tmp"); +#else + rc = RTPathAppend(g_szDefScratchPath, sizeof(g_szDefScratchPath), "uts-XXXXXXXXX.tmp"); +#endif + if (RT_FAILURE(rc)) + { + RTMsgError("RTPathTemp/Append failed when constructing scratch path: %Rrc\n", rc); + strcpy(g_szDefScratchPath, "/tmp/uts-XXXX.tmp"); + } + strcpy(g_szScratchPath, g_szDefScratchPath); + + /* + * Config file location. + */ + /** @todo Improve */ +#if !defined(RT_OS_WINDOWS) + strcpy(g_szCfgPath, "/etc/uts.conf"); +#else + strcpy(g_szCfgPath, ""); +#endif + + /* + * The default transporter is the first one. + */ + g_pTransport = g_apTransports[0]; +} + +/** + * Prints the usage. + * + * @param pStrm Where to print it. + * @param pszArgv0 The program name (argv[0]). + */ +static void utsUsage(PRTSTREAM pStrm, const char *pszArgv0) +{ + RTStrmPrintf(pStrm, + "Usage: %Rbn [options]\n" + "\n" + "Options:\n" + " --config <path>\n" + " Where to load the config from\n" + " --cdrom <path>\n" + " Where the CD/DVD-ROM will be mounted.\n" + " Default: %s\n" + " --scratch <path>\n" + " Where to put scratch files.\n" + " Default: %s \n" + , + pszArgv0, + g_szDefCdRomPath, + g_szDefScratchPath); + RTStrmPrintf(pStrm, + " --transport <name>\n" + " Use the specified transport layer, one of the following:\n"); + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + RTStrmPrintf(pStrm, " %s - %s\n", g_apTransports[i]->szName, g_apTransports[i]->pszDesc); + RTStrmPrintf(pStrm, " Default: %s\n", g_pTransport->szName); + RTStrmPrintf(pStrm, + " --display-output, --no-display-output\n" + " Display the output and the result of all child processes.\n"); + RTStrmPrintf(pStrm, + " --foreground\n" + " Don't daemonize, run in the foreground.\n"); + RTStrmPrintf(pStrm, + " --help, -h, -?\n" + " Display this message and exit.\n" + " --version, -V\n" + " Display the version and exit.\n"); + + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + if (g_apTransports[i]->cOpts) + { + RTStrmPrintf(pStrm, + "\n" + "Options for %s:\n", g_apTransports[i]->szName); + g_apTransports[i]->pfnUsage(g_pStdOut); + } +} + +/** + * Parses the arguments. + * + * @returns Exit code. Exit if this is non-zero or @a *pfExit is set. + * @param argc The number of arguments. + * @param argv The argument vector. + * @param pfExit For indicating exit when the exit code is zero. + */ +static RTEXITCODE utsParseArgv(int argc, char **argv, bool *pfExit) +{ + *pfExit = false; + + /* + * Storage for locally handled options. + */ + bool fDaemonize = true; + bool fDaemonized = false; + + /* + * Combine the base and transport layer option arrays. + */ + static const RTGETOPTDEF s_aBaseOptions[] = + { + { "--config", 'C', RTGETOPT_REQ_STRING }, + { "--transport", 't', RTGETOPT_REQ_STRING }, + { "--cdrom", 'c', RTGETOPT_REQ_STRING }, + { "--scratch", 's', RTGETOPT_REQ_STRING }, + { "--display-output", 'd', RTGETOPT_REQ_NOTHING }, + { "--no-display-output",'D', RTGETOPT_REQ_NOTHING }, + { "--foreground", 'f', RTGETOPT_REQ_NOTHING }, + { "--daemonized", 'Z', RTGETOPT_REQ_NOTHING }, + }; + + size_t cOptions = RT_ELEMENTS(s_aBaseOptions); + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + cOptions += g_apTransports[i]->cOpts; + + PRTGETOPTDEF paOptions = (PRTGETOPTDEF)alloca(cOptions * sizeof(RTGETOPTDEF)); + if (!paOptions) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "alloca failed\n"); + + memcpy(paOptions, s_aBaseOptions, sizeof(s_aBaseOptions)); + cOptions = RT_ELEMENTS(s_aBaseOptions); + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + { + memcpy(&paOptions[cOptions], g_apTransports[i]->paOpts, g_apTransports[i]->cOpts * sizeof(RTGETOPTDEF)); + cOptions += g_apTransports[i]->cOpts; + } + + /* + * Parse the arguments. + */ + RTGETOPTSTATE GetState; + int rc = RTGetOptInit(&GetState, argc, argv, paOptions, cOptions, 1, 0 /* fFlags */); + AssertRC(rc); + + int ch; + RTGETOPTUNION Val; + while ((ch = RTGetOpt(&GetState, &Val))) + { + switch (ch) + { + case 'C': + rc = RTStrCopy(g_szCfgPath, sizeof(g_szCfgPath), Val.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Config file path is path too long (%Rrc)\n", rc); + break; + + case 'c': + rc = RTStrCopy(g_szCdRomPath, sizeof(g_szCdRomPath), Val.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "CD/DVD-ROM is path too long (%Rrc)\n", rc); + break; + + case 'd': + g_fDisplayOutput = true; + break; + + case 'D': + g_fDisplayOutput = false; + break; + + case 'f': + fDaemonize = false; + break; + + case 'h': + utsUsage(g_pStdOut, argv[0]); + *pfExit = true; + return RTEXITCODE_SUCCESS; + + case 's': + rc = RTStrCopy(g_szScratchPath, sizeof(g_szScratchPath), Val.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "scratch path is too long (%Rrc)\n", rc); + break; + + case 't': + { + PCUTSTRANSPORT pTransport = NULL; + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + if (!strcmp(g_apTransports[i]->szName, Val.psz)) + { + pTransport = g_apTransports[i]; + break; + } + if (!pTransport) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unknown transport layer name '%s'\n", Val.psz); + g_pTransport = pTransport; + break; + } + + case 'V': + RTPrintf("$Revision: 157380 $\n"); + *pfExit = true; + return RTEXITCODE_SUCCESS; + + case 'Z': + fDaemonized = true; + fDaemonize = false; + break; + + default: + { + rc = VERR_TRY_AGAIN; + for (size_t i = 0; i < RT_ELEMENTS(g_apTransports); i++) + if (g_apTransports[i]->cOpts) + { + rc = g_apTransports[i]->pfnOption(ch, &Val); + if (RT_SUCCESS(rc)) + break; + if (rc != VERR_TRY_AGAIN) + { + *pfExit = true; + return RTEXITCODE_SYNTAX; + } + } + if (rc == VERR_TRY_AGAIN) + { + *pfExit = true; + return RTGetOptPrintError(ch, &Val); + } + break; + } + } + } + + /* + * Daemonize ourselves if asked to. + */ + if (fDaemonize && !*pfExit) + { + rc = RTProcDaemonize(argv, "--daemonized"); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcDaemonize: %Rrc\n", rc); + *pfExit = true; + } + + return RTEXITCODE_SUCCESS; +} + + +int main(int argc, char **argv) +{ + /* + * Initialize the runtime. + */ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Determine defaults and parse the arguments. + */ + utsSetDefaults(); + bool fExit; + RTEXITCODE rcExit = utsParseArgv(argc, argv, &fExit); + if (rcExit != RTEXITCODE_SUCCESS || fExit) + return rcExit; + + /* + * Initialize global state. + */ + rc = utsInit(); + if (RT_FAILURE(rc)) + return RTEXITCODE_FAILURE; + + /* + * Initialize the transport layer. + */ + rc = g_pTransport->pfnInit(); + if (RT_FAILURE(rc)) + return RTEXITCODE_FAILURE; + + /* + * Ok, start working + */ + rcExit = utsMainLoop(); + + /* + * Cleanup. + */ + g_pTransport->pfnTerm(); + + utsPlatformTerm(); + + return rcExit; +} + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadget.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadget.cpp new file mode 100644 index 00000000..05b4c569 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadget.cpp @@ -0,0 +1,211 @@ +/* $Id: UsbTestServiceGadget.cpp $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, USB gadget host API. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/asm.h> +#include <iprt/ctype.h> +#include <iprt/errcore.h> +#include <iprt/mem.h> +#include <iprt/string.h> + +#include "UsbTestServiceGadgetInternal.h" + + +/********************************************************************************************************************************* +* Constants And Macros, Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * Internal UTS gadget host instance data. + */ +typedef struct UTSGADGETINT +{ + /** Reference counter. */ + volatile uint32_t cRefs; + /** Pointer to the gadget class callback table. */ + PCUTSGADGETCLASSIF pClassIf; + /** The gadget host handle. */ + UTSGADGETHOST hGadgetHost; + /** Class specific instance data - variable in size. */ + uint8_t abClassInst[1]; +} UTSGADGETINT; +/** Pointer to the internal gadget host instance data. */ +typedef UTSGADGETINT *PUTSGADGETINT; + + +/********************************************************************************************************************************* +* Global variables * +*********************************************************************************************************************************/ + +/** Known gadget host interfaces. */ +static const PCUTSGADGETCLASSIF g_apUtsGadgetClass[] = +{ + &g_UtsGadgetClassTest +}; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ + + +/** + * Destroys a gadget instance. + * + * @param pThis The gadget instance. + */ +static void utsGadgetDestroy(PUTSGADGETINT pThis) +{ + pThis->pClassIf->pfnTerm((PUTSGADGETCLASSINT)&pThis->abClassInst[0]); + RTMemFree(pThis); +} + + +DECLHIDDEN(int) utsGadgetCreate(UTSGADGETHOST hGadgetHost, UTSGADGETCLASS enmClass, + PCUTSGADGETCFGITEM paCfg, PUTSGADET phGadget) +{ + int rc = VINF_SUCCESS; + PCUTSGADGETCLASSIF pClassIf = NULL; + + /* Get the interface. */ + for (unsigned i = 0; i < RT_ELEMENTS(g_apUtsGadgetClass); i++) + { + if (g_apUtsGadgetClass[i]->enmClass == enmClass) + { + pClassIf = g_apUtsGadgetClass[i]; + break; + } + } + + if (RT_LIKELY(pClassIf)) + { + PUTSGADGETINT pThis = (PUTSGADGETINT)RTMemAllocZ(RT_UOFFSETOF_DYN(UTSGADGETINT, abClassInst[pClassIf->cbClass])); + if (RT_LIKELY(pThis)) + { + pThis->cRefs = 1; + pThis->hGadgetHost = hGadgetHost; + pThis->pClassIf = pClassIf; + rc = pClassIf->pfnInit((PUTSGADGETCLASSINT)&pThis->abClassInst[0], paCfg); + if (RT_SUCCESS(rc)) + { + /* Connect the gadget to the host. */ + rc = utsGadgetHostGadgetConnect(pThis->hGadgetHost, pThis); + if (RT_SUCCESS(rc)) + *phGadget = pThis; + } + else + RTMemFree(pThis); + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VERR_INVALID_PARAMETER; + + return rc; +} + + +DECLHIDDEN(uint32_t) utsGadgetRetain(UTSGADGET hGadget) +{ + PUTSGADGETINT pThis = hGadget; + + AssertPtrReturn(pThis, 0); + + return ASMAtomicIncU32(&pThis->cRefs); +} + + +DECLHIDDEN(uint32_t) utsGadgetRelease(UTSGADGET hGadget) +{ + PUTSGADGETINT pThis = hGadget; + + AssertPtrReturn(pThis, 0); + + uint32_t cRefs = ASMAtomicDecU32(&pThis->cRefs); + if (!cRefs) + utsGadgetDestroy(pThis); + + return cRefs; +} + + +DECLHIDDEN(uint32_t) utsGadgetGetBusId(UTSGADGET hGadget) +{ + PUTSGADGETINT pThis = hGadget; + + AssertPtrReturn(pThis, VERR_INVALID_HANDLE); + return pThis->pClassIf->pfnGetBusId((PUTSGADGETCLASSINT)&pThis->abClassInst[0]); +} + + +DECLHIDDEN(uint32_t) utsGadgetGetDevId(UTSGADGET hGadget) +{ + PUTSGADGETINT pThis = hGadget; + + AssertPtrReturn(pThis, VERR_INVALID_HANDLE); + return 1; /** @todo Current assumption which is true on Linux with dummy_hcd. */ +} + + +DECLHIDDEN(int) utsGadgetConnect(UTSGADGET hGadget) +{ + PUTSGADGETINT pThis = hGadget; + + AssertPtrReturn(pThis, VERR_INVALID_HANDLE); + int rc = pThis->pClassIf->pfnConnect((PUTSGADGETCLASSINT)&pThis->abClassInst[0]); + if (RT_SUCCESS(rc)) + rc = utsGadgetHostGadgetConnect(pThis->hGadgetHost, hGadget); + + return rc; +} + + +DECLHIDDEN(int) utsGadgetDisconnect(UTSGADGET hGadget) +{ + PUTSGADGETINT pThis = hGadget; + + AssertPtrReturn(pThis, VERR_INVALID_HANDLE); + int rc = utsGadgetHostGadgetDisconnect(pThis->hGadgetHost, hGadget); + if (RT_SUCCESS(rc)) + rc = pThis->pClassIf->pfnDisconnect((PUTSGADGETCLASSINT)&pThis->abClassInst[0]); + + return rc; +} + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadget.h b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadget.h new file mode 100644 index 00000000..19d2603a --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadget.h @@ -0,0 +1,546 @@ +/* $Id: UsbTestServiceGadget.h $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, Gadget API. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_usb_UsbTestServiceGadget_h +#define VBOX_INCLUDED_SRC_usb_UsbTestServiceGadget_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <iprt/types.h> + +RT_C_DECLS_BEGIN + +/** Opaque gadget host handle. */ +typedef struct UTSGADGETHOSTINT *UTSGADGETHOST; +/** Pointer to a gadget host handle. */ +typedef UTSGADGETHOST *PUTSGADGETHOST; + +/** NIL gadget host handle. */ +#define NIL_UTSGADGETHOST ((UTSGADGETHOST)0) + +/** Opaque USB gadget handle. */ +typedef struct UTSGADGETINT *UTSGADGET; +/** Pointer to a USB gadget handle. */ +typedef UTSGADGET *PUTSGADET; + +/** NIL gadget handle. */ +#define NIL_UTSGADGET ((UTSGADGET)0) + +/** + * Gadget/Gadget host configuration item type. + */ +typedef enum UTSGADGETCFGTYPE +{ + /** Don't use! */ + UTSGADGETCFGTYPE_INVALID = 0, + /** Boolean type. */ + UTSGADGETCFGTYPE_BOOLEAN, + /** UTF-8 string. */ + UTSGADGETCFGTYPE_STRING, + /** Unsigned 8bit integer. */ + UTSGADGETCFGTYPE_UINT8, + /** Unsigned 16bit integer. */ + UTSGADGETCFGTYPE_UINT16, + /** Unsigned 32bit integer. */ + UTSGADGETCFGTYPE_UINT32, + /** Unsigned 64bit integer. */ + UTSGADGETCFGTYPE_UINT64, + /** Signed 8bit integer. */ + UTSGADGETCFGTYPE_INT8, + /** Signed 16bit integer. */ + UTSGADGETCFGTYPE_INT16, + /** Signed 32bit integer. */ + UTSGADGETCFGTYPE_INT32, + /** Signed 64bit integer. */ + UTSGADGETCFGTYPE_INT64, + /** 32bit hack. */ + UTSGADGETCFGTYPE_32BIT_HACK = 0x7fffffff +} UTSGADGETCFGTYPE; + +/** + * Gadget configuration value. + */ +typedef struct UTSGADGETCFGVAL +{ + /** Value type */ + UTSGADGETCFGTYPE enmType; + /** Value based on the type. */ + union + { + bool f; + const char *psz; + uint8_t u8; + uint16_t u16; + uint32_t u32; + uint64_t u64; + int8_t i8; + int16_t i16; + int32_t i32; + int64_t i64; + } u; +} UTSGADGETCFGVAL; +/** Pointer to a gadget configuration value. */ +typedef UTSGADGETCFGVAL *PUTSGADGETCFGVAL; +/** Pointer to a const gadget configuration value. */ +typedef const UTSGADGETCFGVAL *PCUTSGADGETCFGVAL; + +/** + * Gadget configuration item. + */ +typedef struct UTSGADGETCFGITEM +{ + /** Item key. */ + const char *pszKey; + /** Item value. */ + UTSGADGETCFGVAL Val; +} UTSGADGETCFGITEM; +/** Pointer to a gadget configuration item. */ +typedef UTSGADGETCFGITEM *PUTSGADGETCFGITEM; +/** Pointer to a const gadget configuration item. */ +typedef const UTSGADGETCFGITEM *PCUTSGADGETCFGITEM; + +/** + * Type for the gadget host. + */ +typedef enum UTSGADGETHOSTTYPE +{ + /** Invalid type, don't use. */ + UTSGADGETHOSTTYPE_INVALID = 0, + /** USB/IP host, gadgets are exported using a USB/IP server. */ + UTSGADGETHOSTTYPE_USBIP, + /** Physical connection using a device or OTG port. */ + UTSGADGETHOSTTYPE_PHYSICAL, + /** 32bit hack. */ + UTSGADGETHOSTTYPE_32BIT_HACK = 0x7fffffff +} UTSGADGETHOSTTYPE; + +/** + * USB gadget class. + */ +typedef enum UTSGADGETCLASS +{ + /** Invalid class, don't use. */ + UTSGADGETCLASS_INVALID = 0, + /** Special test device class. */ + UTSGADGETCLASS_TEST, + /** MSD device. */ + UTSGADGETCLASS_MSD, + /** 32bit hack. */ + UTSGADGETCLASS_32BIT_HACK = 0x7fffffff +} UTSGADGETCLASS; + +/** + * Queries the value of a given boolean key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pf Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryBool(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + bool *pf); + +/** + * Queries the value of a given boolean key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pf Where to store the value on success. + * @param fDef The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryBoolDef(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + bool *pf, bool fDef); + +/** + * Queries the string value of a given key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param ppszVal Where to store the pointer to the string on success, + * must be freed with RTStrFree(). + */ +DECLHIDDEN(int) utsGadgetCfgQueryString(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + char **ppszVal); + +/** + * Queries the string value of a given key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param ppszVal Where to store the pointer to the string on success, + * must be freed with RTStrFree(). + * @param pszDef The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryStringDef(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + char **ppszVal, const char *pszDef); + +/** + * Queries the value of a given uint8_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu8 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU8(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint8_t *pu8); + +/** + * Queries the value of a given uint8_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu8 Where to store the value on success. + * @param u8Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU8Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint8_t *pu8, uint8_t u8Def); + +/** + * Queries the value of a given uint16_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu16 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU16(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pu16); + +/** + * Queries the value of a given uint16_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu16 Where to store the value on success. + * @param u16Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU16Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pu16, uint16_t u16Def); + +/** + * Queries the value of a given uint32_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu32 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU32(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pu32); + +/** + * Queries the value of a given uint32_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu32 Where to store the value on success. + * @param u32Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU32Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pu32, uint32_t u32Def); + +/** + * Queries the value of a given uint64_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu64 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU64(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pu64); + +/** + * Queries the value of a given uint64_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pu64 Where to store the value on success. + * @param u64Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryU64Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pu64, uint64_t u64Def); + +/** + * Queries the value of a given int8_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi8 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS8(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + int8_t *pi8); + +/** + * Queries the value of a given int8_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi8 Where to store the value on success. + * @param i8Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS8Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + int8_t *pi8, uint8_t i8Def); + +/** + * Queries the value of a given int16_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi16 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS16(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pi16); + +/** + * Queries the value of a given int16_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi16 Where to store the value on success. + * @param i16Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS16Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pi16, uint16_t i16Def); + +/** + * Queries the value of a given int32_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi32 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS32(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pi32); + +/** + * Queries the value of a given int32_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi32 Where to store the value on success. + * @param i32Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS32Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pi32, uint32_t i32Def); + +/** + * Queries the value of a given int64_t key from the given configuration array. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi64 Where to store the value on success. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS64(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pi64); + +/** + * Queries the value of a given int64_t key from the given configuration array, + * setting a default if not found. + * + * @returns IPRT status code. + * @param paCfg The configuration items. + * @param pszKey The key query the value for. + * @param pi64 Where to store the value on success. + * @param i64Def The default value to assign if the key is not found. + */ +DECLHIDDEN(int) utsGadgetCfgQueryS64Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pi64, uint64_t i64Def); + +/** + * Creates a new USB gadget host. + * + * @returns IPRT status code. + * @param enmType The host type. + * @param paCfg Additional configuration parameters - optional. + * The array must be terminated with a NULL entry. + * @param phGadgetHost Where to store the handle to the gadget host on success. + */ +DECLHIDDEN(int) utsGadgetHostCreate(UTSGADGETHOSTTYPE enmType, PCUTSGADGETCFGITEM paCfg, + PUTSGADGETHOST phGadgetHost); + +/** + * Retains the given gadget host handle. + * + * @returns New reference count. + * @param hGadgetHost The gadget host handle to retain. + */ +DECLHIDDEN(uint32_t) utsGadgetHostRetain(UTSGADGETHOST hGadgetHost); + +/** + * Releases the given gadget host handle, destroying it if the reference + * count reaches 0. + * + * @returns New reference count. + * @param hGadgetHost The gadget host handle to release. + */ +DECLHIDDEN(uint32_t) utsGadgetHostRelease(UTSGADGETHOST hGadgetHost); + +/** + * Returns the current config of the given gadget host. + * + * @returns Pointer to a constant array of configuration items for the given gadget host. + * @param hGadgetHost The gadget host handle. + */ +DECLHIDDEN(PCUTSGADGETCFGITEM) utsGadgetHostGetCfg(UTSGADGETHOST hGadgetHost); + +/** + * Connects the given gadget to the host. + * + * @returns IPRT status code. + * @param hGadgetHost The gadget host handle. + * @param hGadget The gadget handle. + */ +DECLHIDDEN(int) utsGadgetHostGadgetConnect(UTSGADGETHOST hGadgetHost, UTSGADGET hGadget); + +/** + * Disconnects the given gadget from the host. + * + * @returns IPRT status code. + * @param hGadgetHost The gadget host handle. + * @param hGadget The gadget handle. + */ +DECLHIDDEN(int) utsGadgetHostGadgetDisconnect(UTSGADGETHOST hGadgetHost, UTSGADGET hGadget); + +/** + * Creates a new USB gadget based the class. + * + * @returns IPRT status code. + * @param hGadgetHost The gadget host the gadget is part of. + * @param enmClass The gadget class. + * @param paCfg Array of optional configuration items for the gadget. + * @param phGadget Where to store the gadget handle on success. + */ +DECLHIDDEN(int) utsGadgetCreate(UTSGADGETHOST hGadgetHost, UTSGADGETCLASS enmClass, + PCUTSGADGETCFGITEM paCfg, PUTSGADET phGadget); + +/** + * Retains the given gadget handle. + * + * @returns New reference count. + * @param hGadget The gadget handle to retain. + */ +DECLHIDDEN(uint32_t) utsGadgetRetain(UTSGADGET hGadget); + +/** + * Releases the given gadget handle, destroying it if the reference + * count reaches 0. + * + * @returns New reference count. + * @param hGadget The gadget handle to destroy. + */ +DECLHIDDEN(uint32_t) utsGadgetRelease(UTSGADGET hGadget); + +/** + * Returns the current config of the given gadget. + * + * @returns Pointer to a constant array of configuration items for the given gadget. + * @param hGadget The gadget handle. + */ +DECLHIDDEN(PCUTSGADGETCFGITEM) utsGadgetGetCfg(UTSGADGET hGadget); + +/** + * Returns the path of the given gadget from which it can be accessed. + * + * @returns Access path. + * @param hGadget The gadget handle. + */ +DECLHIDDEN(const char *) utsGadgetGetAccessPath(UTSGADGET hGadget); + +/** + * Returns the bus ID the gadget is on. + * + * @returns Bus ID of the gadget. + * @param hGadget The gadget handle. + */ +DECLHIDDEN(uint32_t) utsGadgetGetBusId(UTSGADGET hGadget); + +/** + * Returns the device ID of the gagdet. + * + * @returns Device ID of the gadget. + * @param hGadget The gadget handle. + */ +DECLHIDDEN(uint32_t) utsGadgetGetDevId(UTSGADGET hGadget); + +/** + * Mark the gadget as connected to the host. Depending + * on the host type it will be appear as physically attached + * or will appear in the exported USB device list. + * + * @returns IPRT status code. + * @param hGadget The gadget handle to connect. + */ +DECLHIDDEN(int) utsGadgetConnect(UTSGADGET hGadget); + +/** + * Mark the gadget as disconnected from the host. + * + * @returns IPRT status code. + * @param hGadget The gadget handle to disconnect. + */ +DECLHIDDEN(int) utsGadgetDisconnect(UTSGADGET hGadget); + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_usb_UsbTestServiceGadget_h */ + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetCfg.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetCfg.cpp new file mode 100644 index 00000000..7ae631a5 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetCfg.cpp @@ -0,0 +1,462 @@ +/* $Id: UsbTestServiceGadgetCfg.cpp $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, USB gadget Cfg API. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/cdefs.h> +#include <iprt/ctype.h> +#include <iprt/err.h> +#include <iprt/mem.h> +#include <iprt/string.h> + +#include "UsbTestServiceGadget.h" + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ + +/** + * Returns the gadget configuration item matching the given key. + * + * @returns Pointer to the configuration item on success or NULL if not found. + * @param paCfg The configuration item array. + * @param pszKey The key to look for. + */ +static PCUTSGADGETCFGITEM utsGadgetCfgGetItemFromKey(PCUTSGADGETCFGITEM paCfg, const char *pszKey) +{ + while ( paCfg + && paCfg->pszKey) + { + if (!RTStrCmp(paCfg->pszKey, pszKey)) + return paCfg; + + paCfg++; + } + return NULL; +} + + + +DECLHIDDEN(int) utsGadgetCfgQueryBool(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + bool *pf) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_BOOLEAN) + { + *pf = pCfgItem->Val.u.f; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryBoolDef(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + bool *pf, bool fDef) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_BOOLEAN) + { + *pf = pCfgItem ? pCfgItem->Val.u.f : fDef; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryString(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + char **ppszVal) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_STRING) + { + *ppszVal = RTStrDup(pCfgItem->Val.u.psz); + if (*ppszVal) + rc = VINF_SUCCESS; + else + rc = VERR_NO_STR_MEMORY; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryStringDef(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + char **ppszVal, const char *pszDef) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_STRING) + { + *ppszVal = RTStrDup(pCfgItem ? pCfgItem->Val.u.psz : pszDef); + if (*ppszVal) + rc = VINF_SUCCESS; + else + rc = VERR_NO_STR_MEMORY; + } + else + rc = VERR_INVALID_PARAMETER; + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU8(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint8_t *pu8) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT8) + { + *pu8 = pCfgItem->Val.u.u8; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU8Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint8_t *pu8, uint8_t u8Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT8) + { + *pu8 = pCfgItem ? pCfgItem->Val.u.u8 : u8Def; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU16(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pu16) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT16) + { + *pu16 = pCfgItem->Val.u.u16; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU16Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pu16, uint16_t u16Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT16) + { + *pu16 = pCfgItem ? pCfgItem->Val.u.u16 : u16Def; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU32(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pu32) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT32) + { + *pu32 = pCfgItem->Val.u.u32; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU32Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pu32, uint32_t u32Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT32) + { + *pu32 = pCfgItem ? pCfgItem->Val.u.u32 : u32Def; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU64(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pu64) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT64) + { + *pu64 = pCfgItem->Val.u.u64; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryU64Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pu64, uint64_t u64Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_UINT64) + { + *pu64 = pCfgItem ? pCfgItem->Val.u.u64 : u64Def; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS8(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + int8_t *pi8) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT8) + { + *pi8 = pCfgItem->Val.u.i8; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS8Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + int8_t *pi8, uint8_t i8Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT8) + { + *pi8 = pCfgItem ? pCfgItem->Val.u.i8 : i8Def; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS16(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pi16) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT16) + { + *pi16 = pCfgItem->Val.u.i16; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS16Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint16_t *pi16, uint16_t i16Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT16) + { + *pi16 = pCfgItem ? pCfgItem->Val.u.i16 : i16Def; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS32(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pi32) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT32) + { + *pi32 = pCfgItem->Val.u.i32; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS32Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint32_t *pi32, uint32_t i32Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT32) + { + *pi32 = pCfgItem ? pCfgItem->Val.u.i32 : i32Def; + rc = VINF_SUCCESS; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS64(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pi64) +{ + int rc = VERR_NOT_FOUND; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if (pCfgItem) + { + if (pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT64) + { + *pi64 = pCfgItem->Val.u.i64; + rc = VINF_SUCCESS; + } + else + rc = VERR_INVALID_PARAMETER; + } + + return rc; +} + + +DECLHIDDEN(int) utsGadgetCfgQueryS64Def(PCUTSGADGETCFGITEM paCfg, const char *pszKey, + uint64_t *pi64, uint64_t i64Def) +{ + int rc = VERR_INVALID_PARAMETER; + PCUTSGADGETCFGITEM pCfgItem = utsGadgetCfgGetItemFromKey(paCfg, pszKey); + + if ( !pCfgItem + || pCfgItem->Val.enmType == UTSGADGETCFGTYPE_INT64) + { + *pi64 = pCfgItem ? pCfgItem->Val.u.i64 : i64Def; + rc = VINF_SUCCESS; + } + + return rc; +} + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetClassTest.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetClassTest.cpp new file mode 100644 index 00000000..6a2f3626 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetClassTest.cpp @@ -0,0 +1,470 @@ +/* $Id: UsbTestServiceGadgetClassTest.cpp $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, USB gadget class + * for the test device. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/asm.h> +#include <iprt/ctype.h> +#include <iprt/dir.h> +#include <iprt/err.h> +#include <iprt/env.h> +#include <iprt/mem.h> +#include <iprt/path.h> +#include <iprt/process.h> +#include <iprt/string.h> +#include <iprt/symlink.h> +#include <iprt/thread.h> + +#include <iprt/linux/sysfs.h> + +#include "UsbTestServiceGadgetInternal.h" +#include "UsbTestServicePlatform.h" + + +/********************************************************************************************************************************* +* Constants And Macros, Structures and Typedefs * +*********************************************************************************************************************************/ + +/** Default configfs mount point. */ +#define UTS_GADGET_CLASS_CONFIGFS_MNT_DEF "/sys/kernel/config/usb_gadget" +/** Gadget template name */ +#define UTS_GADGET_TEMPLATE_NAME "gadget_test" + +/** Default vendor ID which is recognized by the usbtest driver. */ +#define UTS_GADGET_TEST_VENDOR_ID_DEF UINT16_C(0x0525) +/** Default product ID which is recognized by the usbtest driver. */ +#define UTS_GADGET_TEST_PRODUCT_ID_DEF UINT16_C(0xa4a0) +/** Default device class. */ +#define UTS_GADGET_TEST_DEVICE_CLASS_DEF UINT8_C(0xff) +/** Default serial number string. */ +#define UTS_GADGET_TEST_SERIALNUMBER_DEF "0123456789" +/** Default manufacturer string. */ +#define UTS_GADGET_TEST_MANUFACTURER_DEF "Oracle Inc." +/** Default product string. */ +#define UTS_GADGET_TEST_PRODUCT_DEF "USB test device" + +/** + * Internal UTS gadget host instance data. + */ +typedef struct UTSGADGETCLASSINT +{ + /** Gadget template path. */ + char *pszGadgetPath; + /** The UDC this gadget is connected to. */ + char *pszUdc; + /** Bus identifier for the used UDC. */ + uint32_t uBusId; + /** Device identifier. */ + uint32_t uDevId; +} UTSGADGETCLASSINT; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +/** Number of already created gadgets, used for the template name. */ +static volatile uint32_t g_cGadgets = 0; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ + +/** + * Creates a new directory pointed to by the given format string. + * + * @returns IPRT status code. + * @param pszFormat The format string. + * @param va The arguments. + */ +static int utsGadgetClassTestDirCreateV(const char *pszFormat, va_list va) +{ + int rc = VINF_SUCCESS; + char aszPath[RTPATH_MAX + 1]; + + size_t cbStr = RTStrPrintfV(&aszPath[0], sizeof(aszPath), pszFormat, va); + if (cbStr <= sizeof(aszPath) - 1) + rc = RTDirCreateFullPath(aszPath, 0700); + else + rc = VERR_BUFFER_OVERFLOW; + + return rc; +} + + +/** + * Creates a new directory pointed to by the given format string. + * + * @returns IPRT status code. + * @param pszFormat The format string. + * @param ... The arguments. + */ +static int utsGadgetClassTestDirCreate(const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + int rc = utsGadgetClassTestDirCreateV(pszFormat, va); + va_end(va); + return rc; +} + + +/** + * Removes a directory pointed to by the given format string. + * + * @returns IPRT status code. + * @param pszFormat The format string. + * @param va The arguments. + */ +static int utsGadgetClassTestDirRemoveV(const char *pszFormat, va_list va) +{ + int rc = VINF_SUCCESS; + char aszPath[RTPATH_MAX + 1]; + + size_t cbStr = RTStrPrintfV(&aszPath[0], sizeof(aszPath), pszFormat, va); + if (cbStr <= sizeof(aszPath) - 1) + rc = RTDirRemove(aszPath); + else + rc = VERR_BUFFER_OVERFLOW; + + return rc; +} + + +/** + * Removes a directory pointed to by the given format string. + * + * @returns IPRT status code. + * @param pszFormat The format string. + * @param ... The arguments. + */ +static int utsGadgetClassTestDirRemove(const char *pszFormat, ...) +{ + va_list va; + va_start(va, pszFormat); + int rc = utsGadgetClassTestDirRemoveV(pszFormat, va); + va_end(va); + return rc; +} + + +/** + * Links the given function to the given config. + * + * @returns IPRT status code. + * @param pClass The gadget class instance data. + * @param pszFunc The function to link. + * @param pszCfg The configuration which the function will be part of. + */ +static int utsGadgetClassTestLinkFuncToCfg(PUTSGADGETCLASSINT pClass, const char *pszFunc, const char *pszCfg) +{ + int rc = VINF_SUCCESS; + char aszPathFunc[RTPATH_MAX + 1]; + char aszPathCfg[RTPATH_MAX + 1]; + + size_t cbStr = RTStrPrintf(&aszPathFunc[0], sizeof(aszPathFunc), "%s/functions/%s", + pClass->pszGadgetPath, pszFunc); + if (cbStr <= sizeof(aszPathFunc) - 1) + { + cbStr = RTStrPrintf(&aszPathCfg[0], sizeof(aszPathCfg), "%s/configs/%s/%s", + pClass->pszGadgetPath, pszCfg, pszFunc); + if (cbStr <= sizeof(aszPathCfg) - 1) + rc = RTSymlinkCreate(&aszPathCfg[0], &aszPathFunc[0], RTSYMLINKTYPE_DIR, 0); + else + rc = VERR_BUFFER_OVERFLOW; + } + else + rc = VERR_BUFFER_OVERFLOW; + + return rc; +} + + +/** + * Unlinks the given function from the given configuration. + * + * @returns IPRT status code. + * @param pClass The gadget class instance data. + * @param pszFunc The function to unlink. + * @param pszCfg The configuration which the function is currently part of. + */ +static int utsGadgetClassTestUnlinkFuncFromCfg(PUTSGADGETCLASSINT pClass, const char *pszFunc, const char *pszCfg) +{ + int rc = VINF_SUCCESS; + char aszPath[RTPATH_MAX + 1]; + size_t cbStr = RTStrPrintf(&aszPath[0], sizeof(aszPath), "%s/configs/%s/%s", + pClass->pszGadgetPath, pszCfg, pszFunc); + if (cbStr <= sizeof(aszPath) - 1) + rc = RTSymlinkDelete(&aszPath[0], 0); + else + rc = VERR_BUFFER_OVERFLOW; + + return rc; +} + + +/** + * Cleans up any leftover configurations from the gadget class. + * + * @param pClass The gadget class instance data. + */ +static void utsGadgetClassTestCleanup(PUTSGADGETCLASSINT pClass) +{ + /* Unbind the gadget from the currently assigned UDC first. */ + int rc = RTLinuxSysFsWriteStrFile("", 0, NULL, "%s/UDC", pClass->pszGadgetPath); + AssertRC(rc); + + /* Delete the symlinks, ignore any errors. */ + utsGadgetClassTestUnlinkFuncFromCfg(pClass, "Loopback.0", "c.2"); + utsGadgetClassTestUnlinkFuncFromCfg(pClass, "SourceSink.0", "c.1"); + + /* Delete configuration strings and then the configuration directories. */ + utsGadgetClassTestDirRemove("%s/configs/c.2/strings/0x409", pClass->pszGadgetPath); + utsGadgetClassTestDirRemove("%s/configs/c.1/strings/0x409", pClass->pszGadgetPath); + + utsGadgetClassTestDirRemove("%s/configs/c.2", pClass->pszGadgetPath); + utsGadgetClassTestDirRemove("%s/configs/c.1", pClass->pszGadgetPath); + + /* Delete the functions. */ + utsGadgetClassTestDirRemove("%s/functions/Loopback.0", pClass->pszGadgetPath); + utsGadgetClassTestDirRemove("%s/functions/SourceSink.0", pClass->pszGadgetPath); + + /* Delete the english strings. */ + utsGadgetClassTestDirRemove("%s/strings/0x409", pClass->pszGadgetPath); + + /* Finally delete the gadget template. */ + utsGadgetClassTestDirRemove(pClass->pszGadgetPath); + + /* Release the UDC. */ + if (pClass->pszUdc) + { + rc = utsPlatformLnxReleaseUDC(pClass->pszUdc); + AssertRC(rc); + RTStrFree(pClass->pszUdc); + } +} + +/** + * @interface_method_impl{UTSGADGETCLASSIF,pfnInit} + */ +static DECLCALLBACK(int) utsGadgetClassTestInit(PUTSGADGETCLASSINT pClass, PCUTSGADGETCFGITEM paCfg) +{ + int rc = VINF_SUCCESS; + + if (RTLinuxSysFsExists(UTS_GADGET_CLASS_CONFIGFS_MNT_DEF)) + { + /* Create the gadget template */ + unsigned idx = ASMAtomicIncU32(&g_cGadgets); + + int rcStr = RTStrAPrintf(&pClass->pszGadgetPath, "%s/%s%u", UTS_GADGET_CLASS_CONFIGFS_MNT_DEF, + UTS_GADGET_TEMPLATE_NAME, idx); + if (rcStr == -1) + return VERR_NO_STR_MEMORY; + + rc = utsGadgetClassTestDirCreate(pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + { + uint16_t idVendor = 0; + uint16_t idProduct = 0; + uint8_t bDeviceClass = 0; + char *pszSerial = NULL; + char *pszManufacturer = NULL; + char *pszProduct = NULL; + bool fSuperSpeed = false; + + /* Get basic device config. */ + rc = utsGadgetCfgQueryU16Def(paCfg, "Gadget/idVendor", &idVendor, UTS_GADGET_TEST_VENDOR_ID_DEF); + if (RT_SUCCESS(rc)) + rc = utsGadgetCfgQueryU16Def(paCfg, "Gadget/idProduct", &idProduct, UTS_GADGET_TEST_PRODUCT_ID_DEF); + if (RT_SUCCESS(rc)) + rc = utsGadgetCfgQueryU8Def(paCfg, "Gadget/bDeviceClass", &bDeviceClass, UTS_GADGET_TEST_DEVICE_CLASS_DEF); + if (RT_SUCCESS(rc)) + rc = utsGadgetCfgQueryStringDef(paCfg, "Gadget/SerialNumber", &pszSerial, UTS_GADGET_TEST_SERIALNUMBER_DEF); + if (RT_SUCCESS(rc)) + rc = utsGadgetCfgQueryStringDef(paCfg, "Gadget/Manufacturer", &pszManufacturer, UTS_GADGET_TEST_MANUFACTURER_DEF); + if (RT_SUCCESS(rc)) + rc = utsGadgetCfgQueryStringDef(paCfg, "Gadget/Product", &pszProduct, UTS_GADGET_TEST_PRODUCT_DEF); + if (RT_SUCCESS(rc)) + rc = utsGadgetCfgQueryBoolDef(paCfg, "Gadget/SuperSpeed", &fSuperSpeed, false); + + if (RT_SUCCESS(rc)) + { + /* Write basic attributes. */ + rc = RTLinuxSysFsWriteU16File(16, idVendor, "%s/idVendor", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteU16File(16, idProduct, "%s/idProduct", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteU16File(16, bDeviceClass, "%s/bDeviceClass", pClass->pszGadgetPath); + + /* Create english language strings. */ + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestDirCreate("%s/strings/0x409", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteStrFile(pszSerial, 0, NULL, "%s/strings/0x409/serialnumber", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteStrFile(pszManufacturer, 0, NULL, "%s/strings/0x409/manufacturer", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteStrFile(pszProduct, 0, NULL, "%s/strings/0x409/product", pClass->pszGadgetPath); + + /* Create the gadget functions. */ + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestDirCreate("%s/functions/SourceSink.0", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestDirCreate("%s/functions/Loopback.0", pClass->pszGadgetPath); + + /* Create the device configs. */ + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestDirCreate("%s/configs/c.1", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestDirCreate("%s/configs/c.2", pClass->pszGadgetPath); + + /* Write configuration strings. */ + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestDirCreate("%s/configs/c.1/strings/0x409", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestDirCreate("%s/configs/c.2/strings/0x409", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteStrFile("source and sink data", 0, NULL, "%s/configs/c.1/strings/0x409/configuration", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteStrFile("loop input to output", 0, NULL, "%s/configs/c.2/strings/0x409/configuration", pClass->pszGadgetPath); + + /* Link the functions into the configurations. */ + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestLinkFuncToCfg(pClass, "SourceSink.0", "c.1"); + if (RT_SUCCESS(rc)) + rc = utsGadgetClassTestLinkFuncToCfg(pClass, "Loopback.0", "c.2"); + + /* Finally enable the gadget by attaching it to a UDC. */ + if (RT_SUCCESS(rc)) + { + pClass->pszUdc = NULL; + + rc = utsPlatformLnxAcquireUDC(fSuperSpeed, &pClass->pszUdc, &pClass->uBusId); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsWriteStrFile(pClass->pszUdc, 0, NULL, "%s/UDC", pClass->pszGadgetPath); + if (RT_SUCCESS(rc)) + RTThreadSleep(500); /* Fudge: Sleep a bit to give the device a chance to appear on the host so binding succeeds. */ + } + } + + if (pszSerial) + RTStrFree(pszSerial); + if (pszManufacturer) + RTStrFree(pszManufacturer); + if (pszProduct) + RTStrFree(pszProduct); + } + } + else + rc = VERR_NOT_FOUND; + + if (RT_FAILURE(rc)) + utsGadgetClassTestCleanup(pClass); + + return rc; +} + + +/** + * @interface_method_impl{UTSGADGETCLASSIF,pfnTerm} + */ +static DECLCALLBACK(void) utsGadgetClassTestTerm(PUTSGADGETCLASSINT pClass) +{ + utsGadgetClassTestCleanup(pClass); + + if (pClass->pszGadgetPath) + RTStrFree(pClass->pszGadgetPath); +} + + +/** + * @interface_method_impl{UTSGADGETCLASSIF,pfnGetBusId} + */ +static DECLCALLBACK(uint32_t) utsGadgetClassTestGetBusId(PUTSGADGETCLASSINT pClass) +{ + return pClass->uBusId; +} + + +/** + * @interface_method_impl{UTSGADGETCLASSIF,pfnConnect} + */ +static DECLCALLBACK(int) utsGadgetClassTestConnect(PUTSGADGETCLASSINT pClass) +{ + int rc = RTLinuxSysFsWriteStrFile("connect", 0, NULL, "/sys/class/udc/%s/soft_connect", pClass->pszUdc); + if (RT_SUCCESS(rc)) + RTThreadSleep(500); /* Fudge: Sleep a bit to give the device a chance to appear on the host so binding succeeds. */ + + return rc; +} + + +/** + * @interface_method_impl{UTSGADGETCLASSIF,pfnDisconnect} + */ +static DECLCALLBACK(int) utsGadgetClassTestDisconnect(PUTSGADGETCLASSINT pClass) +{ + return RTLinuxSysFsWriteStrFile("disconnect", 0, NULL, "/sys/class/udc/%s/soft_connect", pClass->pszUdc);} + + + +/** + * The gadget host interface callback table. + */ +const UTSGADGETCLASSIF g_UtsGadgetClassTest = +{ + /** enmType */ + UTSGADGETCLASS_TEST, + /** pszDesc */ + "UTS test device gadget class", + /** cbIf */ + sizeof(UTSGADGETCLASSINT), + /** pfnInit */ + utsGadgetClassTestInit, + /** pfnTerm */ + utsGadgetClassTestTerm, + /** pfnGetBusId */ + utsGadgetClassTestGetBusId, + /** pfnConnect */ + utsGadgetClassTestConnect, + /** pfnDisconnect. */ + utsGadgetClassTestDisconnect +}; + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHost.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHost.cpp new file mode 100644 index 00000000..1f8cc53b --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHost.cpp @@ -0,0 +1,179 @@ +/* $Id: UsbTestServiceGadgetHost.cpp $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, USB gadget host API. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/asm.h> +#include <iprt/ctype.h> +#include <iprt/errcore.h> +#include <iprt/mem.h> +#include <iprt/string.h> + +#include "UsbTestServiceGadget.h" +#include "UsbTestServiceGadgetHostInternal.h" + + +/********************************************************************************************************************************* +* Constants And Macros, Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * Internal UTS gadget host instance data. + */ +typedef struct UTSGADGETHOSTINT +{ + /** Reference counter. */ + volatile uint32_t cRefs; + /** Pointer to the gadget host callback table. */ + PCUTSGADGETHOSTIF pHstIf; + /** Interface specific instance data - variable in size. */ + uint8_t abIfInst[1]; +} UTSGADGETHOSTINT; +/** Pointer to the internal gadget host instance data. */ +typedef UTSGADGETHOSTINT *PUTSGADGETHOSTINT; + + +/********************************************************************************************************************************* +* Global variables * +*********************************************************************************************************************************/ + +/** Known gadget host interfaces. */ +static const PCUTSGADGETHOSTIF g_apUtsGadgetHostIf[] = +{ + &g_UtsGadgetHostIfUsbIp, +}; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ + + +/** + * Destroys a gadget host instance. + * + * @param pThis The gadget host instance. + */ +static void utsGadgetHostDestroy(PUTSGADGETHOSTINT pThis) +{ + /** @todo Remove all gadgets. */ + pThis->pHstIf->pfnTerm((PUTSGADGETHOSTTYPEINT)&pThis->abIfInst[0]); + RTMemFree(pThis); +} + + +DECLHIDDEN(int) utsGadgetHostCreate(UTSGADGETHOSTTYPE enmType, PCUTSGADGETCFGITEM paCfg, + PUTSGADGETHOST phGadgetHost) +{ + int rc = VINF_SUCCESS; + PCUTSGADGETHOSTIF pIf = NULL; + + /* Get the interface. */ + for (unsigned i = 0; i < RT_ELEMENTS(g_apUtsGadgetHostIf); i++) + { + if (g_apUtsGadgetHostIf[i]->enmType == enmType) + { + pIf = g_apUtsGadgetHostIf[i]; + break; + } + } + + if (RT_LIKELY(pIf)) + { + PUTSGADGETHOSTINT pThis = (PUTSGADGETHOSTINT)RTMemAllocZ(RT_UOFFSETOF_DYN(UTSGADGETHOSTINT, abIfInst[pIf->cbIf])); + if (RT_LIKELY(pThis)) + { + pThis->cRefs = 1; + pThis->pHstIf = pIf; + rc = pIf->pfnInit((PUTSGADGETHOSTTYPEINT)&pThis->abIfInst[0], paCfg); + if (RT_SUCCESS(rc)) + *phGadgetHost = pThis; + else + RTMemFree(pThis); + } + else + rc = VERR_NO_MEMORY; + } + else + rc = VERR_INVALID_PARAMETER; + + return rc; +} + + +DECLHIDDEN(uint32_t) utsGadgetHostRetain(UTSGADGETHOST hGadgetHost) +{ + PUTSGADGETHOSTINT pThis = hGadgetHost; + + AssertPtrReturn(pThis, 0); + + return ASMAtomicIncU32(&pThis->cRefs); +} + + +DECLHIDDEN(uint32_t) utsGadgetHostRelease(UTSGADGETHOST hGadgetHost) +{ + PUTSGADGETHOSTINT pThis = hGadgetHost; + + AssertPtrReturn(pThis, 0); + + uint32_t cRefs = ASMAtomicDecU32(&pThis->cRefs); + if (!cRefs) + utsGadgetHostDestroy(pThis); + + return cRefs; +} + + +DECLHIDDEN(int) utsGadgetHostGadgetConnect(UTSGADGETHOST hGadgetHost, UTSGADGET hGadget) +{ + PUTSGADGETHOSTINT pThis = hGadgetHost; + + AssertPtrReturn(pThis, VERR_INVALID_HANDLE); + return pThis->pHstIf->pfnGadgetConnect((PUTSGADGETHOSTTYPEINT)&pThis->abIfInst[0], hGadget); +} + + +DECLHIDDEN(int) utsGadgetHostGadgetDisconnect(UTSGADGETHOST hGadgetHost, UTSGADGET hGadget) +{ + PUTSGADGETHOSTINT pThis = hGadgetHost; + + AssertPtrReturn(pThis, VERR_INVALID_HANDLE); + return pThis->pHstIf->pfnGadgetDisconnect((PUTSGADGETHOSTTYPEINT)&pThis->abIfInst[0], hGadget); +} + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHostInternal.h b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHostInternal.h new file mode 100644 index 00000000..6ae11fd0 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHostInternal.h @@ -0,0 +1,132 @@ +/* $Id: UsbTestServiceGadgetHostInternal.h $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, Gadget API. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_usb_UsbTestServiceGadgetHostInternal_h +#define VBOX_INCLUDED_SRC_usb_UsbTestServiceGadgetHostInternal_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <iprt/types.h> + +#include "UsbTestServiceGadget.h" + +RT_C_DECLS_BEGIN + +/** Pointer to an opaque type dependent gadget host instance data. */ +typedef struct UTSGADGETHOSTTYPEINT *PUTSGADGETHOSTTYPEINT; +/** Pointer to a gadget host instance pointer. */ +typedef PUTSGADGETHOSTTYPEINT *PPUTSGADETHOSTTYPEINT; + +/** + * Gadget host interface. + */ +typedef struct UTSGADGETHOSTIF +{ + /** The gadget host type implemented. */ + UTSGADGETHOSTTYPE enmType; + /** Description. */ + const char *pszDesc; + /** Size of the interface specific instance data. */ + size_t cbIf; + + /** + * Initializes the gadget host interface. + * + * @returns IPRT status code. + * @param pIf The interface specific instance data. + * @param paCfg The configuration of the interface. + */ + DECLR3CALLBACKMEMBER(int, pfnInit, (PUTSGADGETHOSTTYPEINT pIf, PCUTSGADGETCFGITEM paCfg)); + + /** + * Terminates the gadget host interface. + * + * @param pIf The interface specific instance data. + */ + DECLR3CALLBACKMEMBER(void, pfnTerm, (PUTSGADGETHOSTTYPEINT pIf)); + + /** + * Adds the given gadget to the host interface. + * + * @returns IPRT status code. + * @param pIf The interface specific instance data. + * @param hGadget The gadget handle to add. + */ + DECLR3CALLBACKMEMBER(int, pfnGadgetAdd, (PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget)); + + /** + * Removes the given gadget from the host interface. + * + * @returns IPRT status code. + * @param pIf The interface specific instance data. + * @param hGadget The gadget handle to remove. + */ + DECLR3CALLBACKMEMBER(int, pfnGadgetRemove, (PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget)); + + /** + * Connects the given gadget to the host interface so it appears as connected to the client + * of the gadget host. + * + * @returns IPRT status code. + * @param pIf The interface specific instance data. + * @param hGadget The gadget handle to add. + */ + DECLR3CALLBACKMEMBER(int, pfnGadgetConnect, (PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget)); + + /** + * Disconnects the given gadget from the host interface so it appears as disconnected to the client + * of the gadget host. + * + * @returns IPRT status code. + * @param pIf The interface specific instance data. + * @param hGadget The gadget handle to add. + */ + DECLR3CALLBACKMEMBER(int, pfnGadgetDisconnect, (PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget)); + +} UTSGADGETHOSTIF; +/** Pointer to a gadget host callback table. */ +typedef UTSGADGETHOSTIF *PUTSGADGETHOSTIF; +/** Pointer to a const gadget host callback table. */ +typedef const struct UTSGADGETHOSTIF *PCUTSGADGETHOSTIF; + +extern UTSGADGETHOSTIF const g_UtsGadgetHostIfUsbIp; + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_usb_UsbTestServiceGadgetHostInternal_h */ + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHostUsbIp.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHostUsbIp.cpp new file mode 100644 index 00000000..a0f183ca --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetHostUsbIp.cpp @@ -0,0 +1,263 @@ +/* $Id: UsbTestServiceGadgetHostUsbIp.cpp $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, USB gadget host interface + * for USB/IP. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/asm.h> +#include <iprt/ctype.h> +#include <iprt/err.h> +#include <iprt/env.h> +#include <iprt/mem.h> +#include <iprt/process.h> +#include <iprt/string.h> +#include <iprt/thread.h> +#include <iprt/time.h> + +#include "UsbTestServiceGadgetHostInternal.h" +#include "UsbTestServicePlatform.h" + + +/********************************************************************************************************************************* +* Constants And Macros, Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * Internal UTS gadget host instance data. + */ +typedef struct UTSGADGETHOSTTYPEINT +{ + /** Handle to the USB/IP daemon process. */ + RTPROCESS hProcUsbIp; +} UTSGADGETHOSTTYPEINT; + +/** Default port of the USB/IP server. */ +#define UTS_GADGET_HOST_USBIP_PORT_DEF 3240 + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ + + +/** + * Worker for binding/unbinding the given gadget from the USB/IP server. + * + * @returns IPRT status code. + * @param pThis The gadget host instance. + * @param hGadget The gadget handle. + * @param fBind Flag whether to do a bind or unbind. + */ +static int usbGadgetHostUsbIpBindUnbind(PUTSGADGETHOSTTYPEINT pThis, UTSGADGET hGadget, bool fBind) +{ + RT_NOREF1(pThis); + uint32_t uBusId, uDevId; + char aszBus[32]; + + uBusId = utsGadgetGetBusId(hGadget); + uDevId = utsGadgetGetDevId(hGadget); + + /* Create the busid argument string. */ + size_t cbRet = RTStrPrintf(&aszBus[0], RT_ELEMENTS(aszBus), "%u-%u", uBusId, uDevId); + if (cbRet == RT_ELEMENTS(aszBus)) + return VERR_BUFFER_OVERFLOW; + + /* Bind to the USB/IP server. */ + RTPROCESS hProcUsbIp = NIL_RTPROCESS; + const char *apszArgv[5]; + + apszArgv[0] = "usbip"; + apszArgv[1] = fBind ? "bind" : "unbind"; + apszArgv[2] = "-b"; + apszArgv[3] = &aszBus[0]; + apszArgv[4] = NULL; + + int rc = RTProcCreate("usbip", apszArgv, RTENV_DEFAULT, RTPROC_FLAGS_SEARCH_PATH, &hProcUsbIp); + if (RT_SUCCESS(rc)) + { + RTPROCSTATUS ProcSts; + rc = RTProcWait(hProcUsbIp, RTPROCWAIT_FLAGS_BLOCK, &ProcSts); + if (RT_SUCCESS(rc)) + { + /* Evaluate the process status. */ + if ( ProcSts.enmReason != RTPROCEXITREASON_NORMAL + || ProcSts.iStatus != 0) + rc = VERR_UNRESOLVED_ERROR; /** @todo Log and give finer grained status code. */ + } + } + + return rc; +} + +/** + * @interface_method_impl{UTSGADGETHOSTIF,pfnInit} + */ +static DECLCALLBACK(int) utsGadgetHostUsbIpInit(PUTSGADGETHOSTTYPEINT pIf, PCUTSGADGETCFGITEM paCfg) +{ + int rc = VINF_SUCCESS; + uint16_t uPort = 0; + + pIf->hProcUsbIp = NIL_RTPROCESS; + + rc = utsGadgetCfgQueryU16Def(paCfg, "UsbIp/Port", &uPort, UTS_GADGET_HOST_USBIP_PORT_DEF); + if (RT_SUCCESS(rc)) + { + /* Make sure the kernel drivers are loaded. */ + rc = utsPlatformModuleLoad("usbip-core", NULL, 0); + if (RT_SUCCESS(rc)) + { + rc = utsPlatformModuleLoad("usbip-host", NULL, 0); + if (RT_SUCCESS(rc)) + { + char aszPort[10]; + char aszPidFile[64]; + const char *apszArgv[6]; + + RTStrPrintf(aszPort, RT_ELEMENTS(aszPort), "%u", uPort); + RTStrPrintf(aszPidFile, RT_ELEMENTS(aszPidFile), "/var/run/usbipd-%u.pid", uPort); + /* Start the USB/IP server process. */ + apszArgv[0] = "usbipd"; + apszArgv[1] = "--tcp-port"; + apszArgv[2] = aszPort; + apszArgv[3] = "--pid"; + apszArgv[4] = aszPidFile; + apszArgv[5] = NULL; + rc = RTProcCreate("usbipd", apszArgv, RTENV_DEFAULT, RTPROC_FLAGS_SEARCH_PATH, &pIf->hProcUsbIp); + if (RT_SUCCESS(rc)) + { + /* Wait for a bit to make sure the server started up successfully. */ + uint64_t tsStart = RTTimeMilliTS(); + do + { + RTPROCSTATUS ProcSts; + rc = RTProcWait(pIf->hProcUsbIp, RTPROCWAIT_FLAGS_NOBLOCK, &ProcSts); + if (rc != VERR_PROCESS_RUNNING) + { + rc = VERR_INVALID_HANDLE; + break; + } + RTThreadSleep(1); + rc = VINF_SUCCESS; + } while (RTTimeMilliTS() - tsStart < 2 * 1000); /* 2 seconds. */ + } + } + } + } + + return rc; +} + + +/** + * @interface_method_impl{UTSGADGETHOSTIF,pfnTerm} + */ +static DECLCALLBACK(void) utsGadgetHostUsbIpTerm(PUTSGADGETHOSTTYPEINT pIf) +{ + /* Kill the process and wait for it to terminate. */ + RTProcTerminate(pIf->hProcUsbIp); + + RTPROCSTATUS ProcSts; + RTProcWait(pIf->hProcUsbIp, RTPROCWAIT_FLAGS_BLOCK, &ProcSts); +} + + +/** + * @interface_method_impl{UTSGADGETHOSTIF,pfnGadgetAdd} + */ +static DECLCALLBACK(int) utsGadgetHostUsbIpGadgetAdd(PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget) +{ + /* Nothing to do so far. */ + RT_NOREF2(pIf, hGadget); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{UTSGADGETHOSTIF,pfnGadgetRemove} + */ +static DECLCALLBACK(int) utsGadgetHostUsbIpGadgetRemove(PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget) +{ + /* Nothing to do so far. */ + RT_NOREF2(pIf, hGadget); + return VINF_SUCCESS; +} + + +/** + * @interface_method_impl{UTSGADGETHOSTIF,pfnGadgetConnect} + */ +static DECLCALLBACK(int) utsGadgetHostUsbIpGadgetConnect(PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget) +{ + return usbGadgetHostUsbIpBindUnbind(pIf, hGadget, true /* fBind */); +} + + +/** + * @interface_method_impl{UTSGADGETHOSTIF,pfnGadgetDisconnect} + */ +static DECLCALLBACK(int) utsGadgetHostUsbIpGadgetDisconnect(PUTSGADGETHOSTTYPEINT pIf, UTSGADGET hGadget) +{ + return usbGadgetHostUsbIpBindUnbind(pIf, hGadget, false /* fBind */); +} + + + +/** + * The gadget host interface callback table. + */ +const UTSGADGETHOSTIF g_UtsGadgetHostIfUsbIp = +{ + /** enmType */ + UTSGADGETHOSTTYPE_USBIP, + /** pszDesc */ + "UTS USB/IP gadget host", + /** cbIf */ + sizeof(UTSGADGETHOSTTYPEINT), + /** pfnInit */ + utsGadgetHostUsbIpInit, + /** pfnTerm */ + utsGadgetHostUsbIpTerm, + /** pfnGadgetAdd */ + utsGadgetHostUsbIpGadgetAdd, + /** pfnGadgetRemove */ + utsGadgetHostUsbIpGadgetRemove, + /** pfnGadgetConnect */ + utsGadgetHostUsbIpGadgetConnect, + /** pfnGadgetDisconnect */ + utsGadgetHostUsbIpGadgetDisconnect +}; diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetInternal.h b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetInternal.h new file mode 100644 index 00000000..dce2719f --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceGadgetInternal.h @@ -0,0 +1,118 @@ +/* $Id: UsbTestServiceGadgetInternal.h $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, Interal gadget interfaces. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_usb_UsbTestServiceGadgetInternal_h +#define VBOX_INCLUDED_SRC_usb_UsbTestServiceGadgetInternal_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <iprt/types.h> + +#include "UsbTestServiceGadget.h" + +RT_C_DECLS_BEGIN + +/** Pointer to an opaque type dependent gadget host instance data. */ +typedef struct UTSGADGETCLASSINT *PUTSGADGETCLASSINT; +/** Pointer to a gadget host instance pointer. */ +typedef PUTSGADGETCLASSINT *PPUTSGADGETCLASSINT; + +/** + * Gadget class interface. + */ +typedef struct UTSGADGETCLASSIF +{ + /** The gadget class type implemented. */ + UTSGADGETCLASS enmClass; + /** Description. */ + const char *pszDesc; + /** Size of the class specific instance data. */ + size_t cbClass; + + /** + * Initializes the gadget class instance. + * + * @returns IPRT status code. + * @param pClass The interface specific instance data. + * @param paCfg The configuration of the interface. + */ + DECLR3CALLBACKMEMBER(int, pfnInit, (PUTSGADGETCLASSINT pClass, PCUTSGADGETCFGITEM paCfg)); + + /** + * Terminates the gadget class instance. + * + * @param pClass The interface specific instance data. + */ + DECLR3CALLBACKMEMBER(void, pfnTerm, (PUTSGADGETCLASSINT pClass)); + + /** + * Returns the bus ID of the class instance. + * + * @returns Bus ID. + * @param pClass The interface specific instance data. + */ + DECLR3CALLBACKMEMBER(uint32_t, pfnGetBusId, (PUTSGADGETCLASSINT pClass)); + + /** + * Connects the gadget. + * + * @returns IPRT status code. + * @param pClass The interface specific instance data. + */ + DECLR3CALLBACKMEMBER(int, pfnConnect, (PUTSGADGETCLASSINT pClass)); + + /** + * Disconnect the gadget. + * + * @returns IPRT status code. + * @param pClass The interface specific instance data. + */ + DECLR3CALLBACKMEMBER(int, pfnDisconnect, (PUTSGADGETCLASSINT pClass)); + +} UTSGADGETCLASSIF; +/** Pointer to a gadget class callback table. */ +typedef UTSGADGETCLASSIF *PUTSGADGETCLASSIF; +/** Pointer to a const gadget host callback table. */ +typedef const struct UTSGADGETCLASSIF *PCUTSGADGETCLASSIF; + +extern UTSGADGETCLASSIF const g_UtsGadgetClassTest; + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_usb_UsbTestServiceGadgetInternal_h */ + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceInternal.h b/src/VBox/ValidationKit/utils/usb/UsbTestServiceInternal.h new file mode 100644 index 00000000..71e2d961 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceInternal.h @@ -0,0 +1,226 @@ +/* $Id: UsbTestServiceInternal.h $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, Internal Header. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_usb_UsbTestServiceInternal_h +#define VBOX_INCLUDED_SRC_usb_UsbTestServiceInternal_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/getopt.h> +#include <iprt/stream.h> + +#include "UsbTestServiceProtocol.h" + +RT_C_DECLS_BEGIN + +/** Opaque UTS transport layer specific client data. */ +typedef struct UTSTRANSPORTCLIENT *PUTSTRANSPORTCLIENT; +typedef PUTSTRANSPORTCLIENT *PPUTSTRANSPORTCLIENT; + +/** + * Transport layer descriptor. + */ +typedef struct UTSTRANSPORT +{ + /** The name. */ + char szName[16]; + /** The description. */ + const char *pszDesc; + /** Pointer to an array of options. */ + PCRTGETOPTDEF paOpts; + /** The number of options in the array. */ + size_t cOpts; + + /** + * Print the usage information for this transport layer. + * + * @param pStream The stream to print the usage info to. + * + * @remarks This is only required if TXSTRANSPORT::cOpts is greater than 0. + */ + DECLR3CALLBACKMEMBER(void, pfnUsage, (PRTSTREAM pStream)); + + /** + * Handle an option. + * + * When encountering an options that is not part of the base options, we'll call + * this method for each transport layer until one handles it. + * + * @retval VINF_SUCCESS if handled. + * @retval VERR_TRY_AGAIN if not handled. + * @retval VERR_INVALID_PARAMETER if we should exit with a non-zero status. + * + * @param ch The short option value. + * @param pVal Pointer to the value union. + * + * @remarks This is only required if TXSTRANSPORT::cOpts is greater than 0. + */ + DECLR3CALLBACKMEMBER(int, pfnOption, (int ch, PCRTGETOPTUNION pVal)); + + /** + * Initializes the transport layer. + * + * @returns IPRT status code. On errors, the transport layer shall call + * RTMsgError to display the error details to the user. + */ + DECLR3CALLBACKMEMBER(int, pfnInit, (void)); + + /** + * Terminate the transport layer, closing and freeing resources. + * + * On errors, the transport layer shall call RTMsgError to display the error + * details to the user. + */ + DECLR3CALLBACKMEMBER(void, pfnTerm, (void)); + + /** + * Waits for a new client to connect and returns the client specific data on + * success. + */ + DECLR3CALLBACKMEMBER(int, pfnWaitForConnect, (PPUTSTRANSPORTCLIENT ppClientNew)); + + /** + * Polls for incoming packets. + * + * @returns true if there are pending packets, false if there isn't. + * @param pClient The client to poll for data. + */ + DECLR3CALLBACKMEMBER(bool, pfnPollIn, (PUTSTRANSPORTCLIENT pClient)); + + /** + * Adds any pollable handles to the poll set. + * + * @returns IPRT status code. + * @param hPollSet The poll set to add them to. + * @param pClient The transport client structure. + * @param idStart The handle ID to start at. + */ + DECLR3CALLBACKMEMBER(int, pfnPollSetAdd, (RTPOLLSET hPollSet, PUTSTRANSPORTCLIENT pClient, uint32_t idStart)); + + /** + * Removes the given client frmo the given pollset. + * + * @returns IPRT status code. + * @param hPollSet The poll set to remove from. + * @param pClient The transport client structure. + * @param idStart The handle ID to remove. + */ + DECLR3CALLBACKMEMBER(int, pfnPollSetRemove, (RTPOLLSET hPollSet, PUTSTRANSPORTCLIENT pClient, uint32_t idStart)); + + /** + * Receives an incoming packet. + * + * This will block until the data becomes available or we're interrupted by a + * signal or something. + * + * @returns IPRT status code. On error conditions other than VERR_INTERRUPTED, + * the current operation will be aborted when applicable. When + * interrupted, the transport layer will store the data until the next + * receive call. + * + * @param pClient The transport client structure. + * @param ppPktHdr Where to return the pointer to the packet we've + * read. This is allocated from the heap using + * RTMemAlloc (w/ UTSPKT_ALIGNMENT) and must be + * free by calling RTMemFree. + */ + DECLR3CALLBACKMEMBER(int, pfnRecvPkt, (PUTSTRANSPORTCLIENT pClient, PPUTSPKTHDR ppPktHdr)); + + /** + * Sends an outgoing packet. + * + * This will block until the data has been written. + * + * @returns IPRT status code. + * @retval VERR_INTERRUPTED if interrupted before anything was sent. + * + * @param pClient The transport client structure. + * @param pPktHdr The packet to send. The size is given by + * aligning the size in the header by + * UTSPKT_ALIGNMENT. + */ + DECLR3CALLBACKMEMBER(int, pfnSendPkt, (PUTSTRANSPORTCLIENT pClient, PCUTSPKTHDR pPktHdr)); + + /** + * Sends a babble packet and disconnects the client (if applicable). + * + * @param pClient The transport client structure. + * @param pPktHdr The packet to send. The size is given by + * aligning the size in the header by + * UTSPKT_ALIGNMENT. + * @param cMsSendTimeout The send timeout measured in milliseconds. + */ + DECLR3CALLBACKMEMBER(void, pfnBabble, (PUTSTRANSPORTCLIENT pClient, PCUTSPKTHDR pPktHdr, RTMSINTERVAL cMsSendTimeout)); + + /** + * Notification about a client HOWDY. + * + * @param pClient The transport client structure. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyHowdy, (PUTSTRANSPORTCLIENT pClient)); + + /** + * Notification about a client BYE. + * + * For connection oriented transport layers, it would be good to disconnect the + * client at this point. + * + * @param pClient The transport client structure. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyBye, (PUTSTRANSPORTCLIENT pClient)); + + /** + * Notification about a REBOOT or SHUTDOWN. + * + * For connection oriented transport layers, stop listening for and + * accepting at this point. + */ + DECLR3CALLBACKMEMBER(void, pfnNotifyReboot, (void)); + + /** Non-zero end marker. */ + uint32_t u32EndMarker; +} UTSTRANSPORT; +/** Pointer to a const transport layer descriptor. */ +typedef const struct UTSTRANSPORT *PCUTSTRANSPORT; + + +extern UTSTRANSPORT const g_TcpTransport; + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_usb_UsbTestServiceInternal_h */ + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServicePlatform-linux.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServicePlatform-linux.cpp new file mode 100644 index 00000000..0f422479 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServicePlatform-linux.cpp @@ -0,0 +1,448 @@ +/* $Id: UsbTestServicePlatform-linux.cpp $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, Platform + * specific helpers - Linux version. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#include <iprt/asm.h> +#include <iprt/ctype.h> +#include <iprt/err.h> +#include <iprt/dir.h> +#include <iprt/env.h> +#include <iprt/mem.h> +#include <iprt/path.h> +#include <iprt/process.h> +#include <iprt/string.h> + +#include <iprt/linux/sysfs.h> + +#include "UsbTestServicePlatform.h" + + +/********************************************************************************************************************************* +* Constants And Macros, Structures and Typedefs * +*********************************************************************************************************************************/ + +/** Where the dummy_hcd.* and dummy_udc.* entries are stored. */ +#define UTS_PLATFORM_LNX_DUMMY_HCD_PATH "/sys/devices/platform" + +/** + * A USB bus provided by the dummy HCD. + */ +typedef struct UTSPLATFORMLNXDUMMYHCDBUS +{ + /** The bus ID on the host the dummy HCD is serving. */ + uint32_t uBusId; + /** Flag whether this is a super speed bus. */ + bool fSuperSpeed; +} UTSPLATFORMLNXDUMMYHCDBUS; +/** Pointer to a Dummy HCD bus. */ +typedef UTSPLATFORMLNXDUMMYHCDBUS *PUTSPLATFORMLNXDUMMYHCDBUS; + +/** + * A dummy UDC descriptor. + */ +typedef struct UTSPLATFORMLNXDUMMYHCD +{ + /** Index of the dummy hcd entry. */ + uint32_t idxDummyHcd; + /** Name for the dummy HCD. */ + const char *pszHcdName; + /** Name for the accompanying dummy HCD. */ + const char *pszUdcName; + /** Flag whether this HCD is free for use. */ + bool fAvailable; + /** Flag whether this HCD contains a super speed capable bus. */ + bool fSuperSpeed; + /** Number of busses this HCD instance serves. */ + unsigned cBusses; + /** Bus structures the HCD serves.*/ + PUTSPLATFORMLNXDUMMYHCDBUS paBusses; +} UTSPLATFORMLNXDUMMYHCD; +/** Pointer to a dummy HCD entry. */ +typedef UTSPLATFORMLNXDUMMYHCD *PUTSPLATFORMLNXDUMMYHCD; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +/** Array of dummy HCD entries. */ +static PUTSPLATFORMLNXDUMMYHCD g_paDummyHcd = NULL; +/** Number of Dummy hCD entries in the array. */ +static unsigned g_cDummyHcd = 0; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ + + +/** + * Queries the assigned busses for the given dummy HCD instance. + * + * @returns IPRT status code. + * @param pHcd The dummy HCD bus instance. + * @param pszHcdName The base HCD name. + */ +static int utsPlatformLnxDummyHcdQueryBusses(PUTSPLATFORMLNXDUMMYHCD pHcd, const char *pszHcdName) +{ + int rc = VINF_SUCCESS; + char aszPath[RTPATH_MAX + 1]; + unsigned idxBusCur = 0; + unsigned idxBusMax = 0; + + size_t cchPath = RTStrPrintf(&aszPath[0], RT_ELEMENTS(aszPath), UTS_PLATFORM_LNX_DUMMY_HCD_PATH "/%s.%u/usb*", + pszHcdName, pHcd->idxDummyHcd); + if (cchPath == RT_ELEMENTS(aszPath)) + return VERR_BUFFER_OVERFLOW; + + RTDIR hDir = NULL; + rc = RTDirOpenFiltered(&hDir, aszPath, RTDIRFILTER_WINNT, 0 /*fFlags*/); + if (RT_SUCCESS(rc)) + { + do + { + RTDIRENTRY DirFolderContent; + rc = RTDirRead(hDir, &DirFolderContent, NULL); + if (RT_SUCCESS(rc)) + { + uint32_t uBusId = 0; + + /* Extract the bus number - it is after "usb", i.e. "usb9" indicates a bus ID of 9. */ + rc = RTStrToUInt32Ex(&DirFolderContent.szName[3], NULL, 10, &uBusId); + if (RT_SUCCESS(rc)) + { + /* Check whether this is a super speed bus. */ + int64_t iSpeed = 0; + bool fSuperSpeed = false; + rc = RTLinuxSysFsReadIntFile(10, &iSpeed, UTS_PLATFORM_LNX_DUMMY_HCD_PATH "/%s.%u/%s/speed", + pszHcdName, pHcd->idxDummyHcd, DirFolderContent.szName); + if ( RT_SUCCESS(rc) + && (iSpeed == 5000 || iSpeed == 10000)) + { + fSuperSpeed = true; + pHcd->fSuperSpeed = true; + } + + /* Add to array of available busses for this HCD. */ + if (idxBusCur == idxBusMax) + { + size_t cbNew = (idxBusMax + 10) * sizeof(UTSPLATFORMLNXDUMMYHCDBUS); + PUTSPLATFORMLNXDUMMYHCDBUS pNew = (PUTSPLATFORMLNXDUMMYHCDBUS)RTMemRealloc(pHcd->paBusses, cbNew); + if (pNew) + { + idxBusMax += 10; + pHcd->paBusses = pNew; + } + } + + if (idxBusCur < idxBusMax) + { + pHcd->paBusses[idxBusCur].uBusId = uBusId; + pHcd->paBusses[idxBusCur].fSuperSpeed = fSuperSpeed; + idxBusCur++; + } + else + rc = VERR_NO_MEMORY; + } + } + } while (RT_SUCCESS(rc)); + + pHcd->cBusses = idxBusCur; + + if (rc == VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + RTDirClose(hDir); + } + + return rc; +} + + +/** + * Scans all available HCDs with the given name. + * + * @returns IPRT status code. + * @param pszHcdName The base HCD name. + * @param pszUdcName The base UDC name. + */ +static int utsPlatformLnxHcdScanByName(const char *pszHcdName, const char *pszUdcName) +{ + char aszPath[RTPATH_MAX + 1]; + size_t cchPath = RTStrPrintf(&aszPath[0], RT_ELEMENTS(aszPath), + UTS_PLATFORM_LNX_DUMMY_HCD_PATH "/%s.*", pszHcdName); + if (cchPath == RT_ELEMENTS(aszPath)) + return VERR_BUFFER_OVERFLOW; + + /* Enumerate the available HCD and their bus numbers. */ + RTDIR hDir = NULL; + int rc = RTDirOpenFiltered(&hDir, aszPath, RTDIRFILTER_WINNT, 0 /*fFlags*/); + if (RT_SUCCESS(rc)) + { + unsigned idxHcdCur = g_cDummyHcd; + unsigned idxHcdMax = g_cDummyHcd; + + do + { + RTDIRENTRY DirFolderContent; + rc = RTDirRead(hDir, &DirFolderContent, NULL); + if (RT_SUCCESS(rc)) + { + /* + * Get the HCD index and assigned bus number form the sysfs entries, + * Any error here is silently ignored and results in the HCD not being + * added to the list of available controllers. + */ + const char *pszIdx = RTStrStr(DirFolderContent.szName, "."); + if (pszIdx) + { + /* Skip the separator and convert number to index. */ + pszIdx++; + + uint32_t idxHcd = 0; + rc = RTStrToUInt32Ex(pszIdx, NULL, 10, &idxHcd); + if (RT_SUCCESS(rc)) + { + /* Add to array of available HCDs. */ + if (idxHcdCur == idxHcdMax) + { + size_t cbNew = (idxHcdMax + 10) * sizeof(UTSPLATFORMLNXDUMMYHCD); + PUTSPLATFORMLNXDUMMYHCD pNew = (PUTSPLATFORMLNXDUMMYHCD)RTMemRealloc(g_paDummyHcd, cbNew); + if (pNew) + { + idxHcdMax += 10; + g_paDummyHcd = pNew; + } + } + + if (idxHcdCur < idxHcdMax) + { + g_paDummyHcd[idxHcdCur].idxDummyHcd = idxHcd; + g_paDummyHcd[idxHcdCur].pszHcdName = pszHcdName; + g_paDummyHcd[idxHcdCur].pszUdcName = pszUdcName; + g_paDummyHcd[idxHcdCur].fAvailable = true; + g_paDummyHcd[idxHcdCur].fSuperSpeed = false; + g_paDummyHcd[idxHcdCur].cBusses = 0; + g_paDummyHcd[idxHcdCur].paBusses = NULL; + rc = utsPlatformLnxDummyHcdQueryBusses(&g_paDummyHcd[idxHcdCur], pszHcdName); + if (RT_SUCCESS(rc)) + idxHcdCur++; + } + else + rc = VERR_NO_MEMORY; + } + } + } + } while (RT_SUCCESS(rc)); + + g_cDummyHcd = idxHcdCur; + + if (rc == VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + RTDirClose(hDir); + } + + return rc; +} + +DECLHIDDEN(int) utsPlatformInit(void) +{ + /* Load the modules required for setting up USB/IP testing. */ + int rc = utsPlatformModuleLoad("libcomposite", NULL, 0); + if (RT_SUCCESS(rc)) + { + const char *apszArg[] = { "num=20" }; /** @todo Make configurable from config. */ + rc = utsPlatformModuleLoad("dummy_hcd", &apszArg[0], RT_ELEMENTS(apszArg)); + if (RT_SUCCESS(rc)) + rc = utsPlatformModuleLoad("dummy_hcd_ss", &apszArg[0], RT_ELEMENTS(apszArg)); + if (RT_SUCCESS(rc)) + rc = utsPlatformLnxHcdScanByName("dummy_hcd", "dummy_udc"); + if (RT_SUCCESS(rc)) + rc = utsPlatformLnxHcdScanByName("dummy_hcd_ss", "dummy_udc_ss"); + } + + return rc; +} + + +DECLHIDDEN(void) utsPlatformTerm(void) +{ + /* Unload dummy HCD. */ + utsPlatformModuleUnload("dummy_hcd"); + utsPlatformModuleUnload("dummy_hcd_ss"); + + RTMemFree(g_paDummyHcd); +} + + +DECLHIDDEN(int) utsPlatformModuleLoad(const char *pszModule, const char **papszArgv, + unsigned cArgv) +{ + RTPROCESS hProcModprobe = NIL_RTPROCESS; + const char **papszArgs = (const char **)RTMemAllocZ((3 + cArgv) * sizeof(const char *)); + if (RT_UNLIKELY(!papszArgs)) + return VERR_NO_MEMORY; + + papszArgs[0] = "modprobe"; + papszArgs[1] = pszModule; + + unsigned idx; + for (idx = 0; idx < cArgv; idx++) + papszArgs[2+idx] = papszArgv[idx]; + papszArgs[2+idx] = NULL; + + int rc = RTProcCreate("modprobe", papszArgs, RTENV_DEFAULT, RTPROC_FLAGS_SEARCH_PATH, &hProcModprobe); + if (RT_SUCCESS(rc)) + { + RTPROCSTATUS ProcSts; + rc = RTProcWait(hProcModprobe, RTPROCWAIT_FLAGS_BLOCK, &ProcSts); + if (RT_SUCCESS(rc)) + { + /* Evaluate the process status. */ + if ( ProcSts.enmReason != RTPROCEXITREASON_NORMAL + || ProcSts.iStatus != 0) + rc = VERR_UNRESOLVED_ERROR; /** @todo Log and give finer grained status code. */ + } + } + + RTMemFree(papszArgs); + return rc; +} + + +DECLHIDDEN(int) utsPlatformModuleUnload(const char *pszModule) +{ + RTPROCESS hProcModprobe = NIL_RTPROCESS; + const char *apszArgv[3]; + + apszArgv[0] = "rmmod"; + apszArgv[1] = pszModule; + apszArgv[2] = NULL; + + int rc = RTProcCreate("rmmod", apszArgv, RTENV_DEFAULT, RTPROC_FLAGS_SEARCH_PATH, &hProcModprobe); + if (RT_SUCCESS(rc)) + { + RTPROCSTATUS ProcSts; + rc = RTProcWait(hProcModprobe, RTPROCWAIT_FLAGS_BLOCK, &ProcSts); + if (RT_SUCCESS(rc)) + { + /* Evaluate the process status. */ + if ( ProcSts.enmReason != RTPROCEXITREASON_NORMAL + || ProcSts.iStatus != 0) + rc = VERR_UNRESOLVED_ERROR; /** @todo Log and give finer grained status code. */ + } + } + + return rc; +} + + +DECLHIDDEN(int) utsPlatformLnxAcquireUDC(bool fSuperSpeed, char **ppszUdc, uint32_t *puBusId) +{ + int rc = VERR_NOT_FOUND; + + for (unsigned i = 0; i < g_cDummyHcd; i++) + { + PUTSPLATFORMLNXDUMMYHCD pHcd = &g_paDummyHcd[i]; + + /* + * We can't use a super speed capable UDC for gadgets with lower speeds + * because they hardcode the maximum speed to SuperSpeed most of the time + * which will make it unusable for lower speeds. + */ + if ( pHcd->fAvailable + && pHcd->fSuperSpeed == fSuperSpeed) + { + /* Check all assigned busses for a speed match. */ + for (unsigned idxBus = 0; idxBus < pHcd->cBusses; idxBus++) + { + if (pHcd->paBusses[idxBus].fSuperSpeed == fSuperSpeed) + { + rc = VINF_SUCCESS; + int cbRet = RTStrAPrintf(ppszUdc, "%s.%u", pHcd->pszUdcName, pHcd->idxDummyHcd); + if (cbRet == -1) + rc = VERR_NO_STR_MEMORY; + *puBusId = pHcd->paBusses[idxBus].uBusId; + pHcd->fAvailable = false; + break; + } + } + + if (rc != VERR_NOT_FOUND) + break; + } + } + + return rc; +} + + +DECLHIDDEN(int) utsPlatformLnxReleaseUDC(const char *pszUdc) +{ + int rc = VERR_INVALID_PARAMETER; + const char *pszIdx = RTStrStr(pszUdc, "."); + if (pszIdx) + { + size_t cchUdcName = pszIdx - pszUdc; + pszIdx++; + uint32_t idxHcd = 0; + rc = RTStrToUInt32Ex(pszIdx, NULL, 10, &idxHcd); + if (RT_SUCCESS(rc)) + { + rc = VERR_NOT_FOUND; + + for (unsigned i = 0; i < g_cDummyHcd; i++) + { + if ( g_paDummyHcd[i].idxDummyHcd == idxHcd + && !RTStrNCmp(g_paDummyHcd[i].pszUdcName, pszUdc, cchUdcName)) + { + AssertReturn(!g_paDummyHcd[i].fAvailable, VERR_INVALID_PARAMETER); + g_paDummyHcd[i].fAvailable = true; + rc = VINF_SUCCESS; + break; + } + } + } + } + + return rc; +} + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServicePlatform.h b/src/VBox/ValidationKit/utils/usb/UsbTestServicePlatform.h new file mode 100644 index 00000000..cf5eed26 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServicePlatform.h @@ -0,0 +1,105 @@ +/* $Id: UsbTestServicePlatform.h $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, Platform specific helpers. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_usb_UsbTestServicePlatform_h +#define VBOX_INCLUDED_SRC_usb_UsbTestServicePlatform_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> +#include <iprt/types.h> + +RT_C_DECLS_BEGIN + +/** + * Initializes the platform specific structures for UTS. + * + * @returns IPRT status code. + */ +DECLHIDDEN(int) utsPlatformInit(void); + +/** + * Frees all platform specific structures for UTS. + */ +DECLHIDDEN(void) utsPlatformTerm(void); + +/** + * Loads the specified kernel module on the platform. + * + * @returns IPRT status code. + * @param pszModule The module to load. + * @param papszArgv Array of arguments to pass to the module. + * @param cArgv Number of argument array entries. + */ +DECLHIDDEN(int) utsPlatformModuleLoad(const char *pszModule, const char **papszArgv, + unsigned cArgv); + +/** + * Unloads the specified kernel module on the platform. + * + * @returns IPRT status code. + * @param pszModule The module to unload. + */ +DECLHIDDEN(int) utsPlatformModuleUnload(const char *pszModule); + +#ifdef RT_OS_LINUX + +/** + * Acquires a free UDC to attach a gadget to. + * + * @returns IPRT status code. + * @param fSuperSpeed Flag whether a super speed bus is required. + * @param ppszUdc Where to store the pointer to the name of the UDC on success. + * Free with RTStrFree(). + * @param puBusId Where to store the bus ID the UDC is attached to on the host side. + */ +DECLHIDDEN(int) utsPlatformLnxAcquireUDC(bool fSuperSpeed, char **ppszUdc, uint32_t *puBusId); + +/** + * Releases the given UDC for other use. + * + * @returns IPRT status code. + * @param pszUdc The UDC to release. + */ +DECLHIDDEN(int) utsPlatformLnxReleaseUDC(const char *pszUdc); + +#endif + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_usb_UsbTestServicePlatform_h */ + diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceProtocol.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServiceProtocol.cpp new file mode 100644 index 00000000..a32e8a3e --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceProtocol.cpp @@ -0,0 +1,120 @@ +/* $Id: UsbTestServiceProtocol.cpp $ */ +/** @file + * UsbTestService - Remote USB test configuration and execution server, Protocol helpers. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_DEFAULT +#include <iprt/asm.h> +#include <iprt/cdefs.h> + +#include "UsbTestServiceProtocol.h" + + + +/** + * Converts a UTS packet header from host to network byte order. + * + * @param pPktHdr The packet header to convert. + */ +DECLINLINE(void) utsProtocolPktHdrH2N(PUTSPKTHDR pPktHdr) +{ + pPktHdr->cb = RT_H2N_U32(pPktHdr->cb); + pPktHdr->uCrc32 = RT_H2N_U32(pPktHdr->uCrc32); +} + + +/** + * Converts a UTS packet header from network to host byte order. + * + * @param pPktHdr The packet header to convert. + */ +DECLINLINE(void) utsProtocolPktHdrN2H(PUTSPKTHDR pPktHdr) +{ + pPktHdr->cb = RT_N2H_U32(pPktHdr->cb); + pPktHdr->uCrc32 = RT_N2H_U32(pPktHdr->uCrc32); +} + + +/** + * Converts a UTS status header from host to network byte order. + * + * @param pPktHdr The packet header to convert. + */ +DECLINLINE(void) utsProtocolStsHdrH2N(PUTSPKTSTS pPktHdr) +{ + utsProtocolPktHdrH2N(&pPktHdr->Hdr); + pPktHdr->rcReq = RT_H2N_U32(pPktHdr->rcReq); + pPktHdr->cchStsMsg = RT_H2N_U32(pPktHdr->cchStsMsg); +} + + +/** + * Converts a UTS status header from network to host byte order. + * + * @param pPktHdr The packet header to convert. + */ +DECLINLINE(void) utsProtocolStsHdrN2H(PUTSPKTSTS pPktHdr) +{ + utsProtocolPktHdrN2H(&pPktHdr->Hdr); + pPktHdr->rcReq = RT_N2H_U32(pPktHdr->rcReq); + pPktHdr->cchStsMsg = RT_N2H_U32(pPktHdr->cchStsMsg); +} + + +DECLHIDDEN(void) utsProtocolReqH2N(PUTSPKTHDR pPktHdr) +{ + utsProtocolPktHdrH2N(pPktHdr); +} + + +DECLHIDDEN(void) utsProtocolReqN2H(PUTSPKTHDR pPktHdr) +{ + RT_NOREF1(pPktHdr); +} + + +DECLHIDDEN(void) utsProtocolRepH2N(PUTSPKTSTS pPktHdr) +{ + RT_NOREF1(pPktHdr); +} + + +DECLHIDDEN(void) utsProtocolRepN2H(PUTSPKTSTS pPktHdr) +{ + RT_NOREF1(pPktHdr); +} diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceProtocol.h b/src/VBox/ValidationKit/utils/usb/UsbTestServiceProtocol.h new file mode 100644 index 00000000..5378493b --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceProtocol.h @@ -0,0 +1,373 @@ +/* $Id: UsbTestServiceProtocol.h $ */ +/** @file + * UsbTestServ - Remote USB test configuration and execution server, Protocol Header. + */ + +/* + * Copyright (C) 2016-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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + +#ifndef VBOX_INCLUDED_SRC_usb_UsbTestServiceProtocol_h +#define VBOX_INCLUDED_SRC_usb_UsbTestServiceProtocol_h +#ifndef RT_WITHOUT_PRAGMA_ONCE +# pragma once +#endif + +#include <iprt/cdefs.h> + +RT_C_DECLS_BEGIN + +/** + * Common Packet header (for requests and replies). + */ +typedef struct UTSPKTHDR +{ + /** The unpadded packet length. This include this header. */ + uint32_t cb; + /** The CRC-32 for the packet starting from the opcode field. 0 if the packet + * hasn't been CRCed. */ + uint32_t uCrc32; + /** Packet opcode, an unterminated ASCII string. */ + uint8_t achOpcode[8]; +} UTSPKTHDR; +AssertCompileSize(UTSPKTHDR, 16); +/** Pointer to a packet header. */ +typedef UTSPKTHDR *PUTSPKTHDR; +/** Pointer to a packet header. */ +typedef UTSPKTHDR const *PCUTSPKTHDR; +/** Pointer to a packet header pointer. */ +typedef PUTSPKTHDR *PPUTSPKTHDR; + +/** Packet alignment. */ +#define UTSPKT_ALIGNMENT 16 +/** Max packet size. */ +#define UTSPKT_MAX_SIZE _256K + +/** + * Status packet. + */ +typedef struct UTSPKTSTS +{ + /** Embedded common packet header. */ + UTSPKTHDR Hdr; + /** The IPRT status code of the request. */ + int32_t rcReq; + /** Size of the optional status message following this structure - + * only for errors. */ + uint32_t cchStsMsg; + /** Padding - reserved. */ + uint8_t au8Padding[8]; +} UTSPKTSTS; +AssertCompileSizeAlignment(UTSPKTSTS, UTSPKT_ALIGNMENT); +/** Pointer to a status packet header. */ +typedef UTSPKTSTS *PUTSPKTSTS; + +#define UTSPKT_OPCODE_HOWDY "HOWDY " + +/** 32bit protocol version consisting of a 16bit major and 16bit minor part. */ +#define UTS_PROTOCOL_VS (UTS_PROTOCOL_VS_MAJOR | UTS_PROTOCOL_VS_MINOR) +/** The major version part of the protocol version. */ +#define UTS_PROTOCOL_VS_MAJOR (1 << 16) +/** The minor version part of the protocol version. */ +#define UTS_PROTOCOL_VS_MINOR (0) + +/** + * The HOWDY request structure. + */ +typedef struct UTSPKTREQHOWDY +{ + /** Embedded packet header. */ + UTSPKTHDR Hdr; + /** Version of the protocol the client wants to use. */ + uint32_t uVersion; + /** Mask of USB device connections the client wants to use. */ + uint32_t fUsbConn; + /** The number of characters for the hostname. */ + uint32_t cchHostname; + /** The client host name as terminated ASCII string. */ + char achHostname[68]; +} UTSPKTREQHOWDY; +AssertCompileSizeAlignment(UTSPKTREQHOWDY, UTSPKT_ALIGNMENT); +/** Pointer to a HOWDY request structure. */ +typedef UTSPKTREQHOWDY *PUTSPKTREQHOWDY; + +/** + * The HOWDY reply structure. + */ +typedef struct UTSPKTREPHOWDY +{ + /** Status packet. */ + UTSPKTSTS Sts; + /** Version to use for the established connection. */ + uint32_t uVersion; + /** Mask of supported USB device connections for this connection. */ + uint32_t fUsbConn; + /** Port number the USB/IP server is listening on if + * the client requested USB/IP support and the server can + * deliver it. */ + uint32_t uUsbIpPort; + /** Maximum number of devices supported over USB/IP + * at the same time. */ + uint32_t cUsbIpDevices; + /** Maximum number of physical devices supported for this client + * if a physical connection is present. */ + uint32_t cPhysicalDevices; + /** Padding - reserved. */ + uint8_t au8Padding[12]; +} UTSPKTREPHOWDY; +AssertCompileSizeAlignment(UTSPKTREPHOWDY, UTSPKT_ALIGNMENT); +/** Pointer to a HOWDY reply structure. */ +typedef UTSPKTREPHOWDY *PUTSPKTREPHOWDY; + +/** Connections over USB/IP are supported. */ +#define UTSPKT_HOWDY_CONN_F_USBIP RT_BIT_32(0) +/** The server has a physical connection available to the client + * which can be used for testing. */ +#define UTSPKT_HOWDY_CONN_F_PHYSICAL RT_BIT_32(1) + + +#define UTSPKT_OPCODE_BYE "BYE " + +/* No additional structures for BYE. */ + +#define UTSPKT_OPCODE_GADGET_CREATE "GDGTCRT " + +/** + * The GADGET CREATE request structure. + */ +typedef struct UTSPKTREQGDGTCTOR +{ + /** Embedded packet header. */ + UTSPKTHDR Hdr; + /** Gadget type. */ + uint32_t u32GdgtType; + /** Access methods. */ + uint32_t u32GdgtAccess; + /** Number of config items - following this structure. */ + uint32_t u32CfgItems; + /** Reserved. */ + uint32_t u32Rsvd0; +} UTSPKTREQGDGTCTOR; +AssertCompileSizeAlignment(UTSPKTREQGDGTCTOR, UTSPKT_ALIGNMENT); +/** Pointer to a GADGET CREATE structure. */ +typedef UTSPKTREQGDGTCTOR *PUTSPKTREQGDGTCTOR; + +/** Gadget type - Test device. */ +#define UTSPKT_GDGT_CREATE_TYPE_TEST UINT32_C(0x1) + +/** Gadget acess method - USB/IP. */ +#define UTSPKT_GDGT_CREATE_ACCESS_USBIP UINT32_C(0x1) + +/** + * Configuration item. + */ +typedef struct UTSPKTREQGDGTCTORCFGITEM +{ + /** Size of the key incuding termination in bytes. */ + uint32_t u32KeySize; + /** Item type. */ + uint32_t u32Type; + /** Size of the value string including termination in bytes. */ + uint32_t u32ValSize; + /** Reserved. */ + uint32_t u32Rsvd0; +} UTSPKTREQGDGTCTORCFGITEM; +AssertCompileSizeAlignment(UTSPKTREQGDGTCTORCFGITEM, UTSPKT_ALIGNMENT); +/** Pointer to a configuration item. */ +typedef UTSPKTREQGDGTCTORCFGITEM *PUTSPKTREQGDGTCTORCFGITEM; + +/** Boolean configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_BOOLEAN UINT32_C(1) +/** String configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_STRING UINT32_C(2) +/** Unsigned 8-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_UINT8 UINT32_C(3) +/** Unsigned 16-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_UINT16 UINT32_C(4) +/** Unsigned 32-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_UINT32 UINT32_C(5) +/** Unsigned 64-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_UINT64 UINT32_C(6) +/** Signed 8-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_INT8 UINT32_C(7) +/** Signed 16-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_INT16 UINT32_C(8) +/** Signed 32-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_INT32 UINT32_C(9) +/** Signed 64-bit integer configuration item type. */ +#define UTSPKT_GDGT_CFG_ITEM_TYPE_INT64 UINT32_C(10) + +/** + * The GADGET CREATE reply structure. + */ +typedef struct UTSPKTREPGDGTCTOR +{ + /** Status packet. */ + UTSPKTSTS Sts; + /** The gadget ID on success. */ + uint32_t idGadget; + /** Bus ID the gadget is attached to */ + uint32_t u32BusId; + /** Device ID of the gadget on the bus. */ + uint32_t u32DevId; + /** Padding - reserved. */ + uint8_t au8Padding[4]; +} UTSPKTREPGDGTCTOR; +AssertCompileSizeAlignment(UTSPKTREPGDGTCTOR, UTSPKT_ALIGNMENT); +/** Pointer to a GADGET CREATE structure. */ +typedef UTSPKTREPGDGTCTOR *PUTSPKTREPGDGTCTOR; + + +#define UTSPKT_OPCODE_GADGET_DESTROY "GDGTDTOR" + +/** + * The GADGET DESTROY request structure. + */ +typedef struct UTSPKTREQGDGTDTOR +{ + /** Embedded packet header. */ + UTSPKTHDR Hdr; + /** Gadget ID as returned from the GADGET CREATE request on success. */ + uint32_t idGadget; + /** Padding - reserved. */ + uint8_t au8Padding[12]; +} UTSPKTREQGDGTDTOR; +AssertCompileSizeAlignment(UTSPKTREQGDGTDTOR, UTSPKT_ALIGNMENT); +/** Pointer to a GADGET DESTROY structure. */ +typedef UTSPKTREQGDGTDTOR *PUTSPKTREQGDGTDTOR; + +/* No additional structure for the reply (just standard STATUS packet). */ + +#define UTSPKT_OPCODE_GADGET_CONNECT "GDGTCNCT" + +/** + * The GADGET CONNECT request structure. + */ +typedef struct UTSPKTREQGDGTCNCT +{ + /** Embedded packet header. */ + UTSPKTHDR Hdr; + /** Gadget ID as returned from the GADGET CREATE request on success. */ + uint32_t idGadget; + /** Padding - reserved. */ + uint8_t au8Padding[12]; +} UTSPKTREQGDGTCNCT; +AssertCompileSizeAlignment(UTSPKTREQGDGTCNCT, UTSPKT_ALIGNMENT); +/** Pointer to a GADGET CONNECT request structure. */ +typedef UTSPKTREQGDGTCNCT *PUTSPKTREQGDGTCNCT; + +/* No additional structure for the reply (just standard STATUS packet). */ + +#define UTSPKT_OPCODE_GADGET_DISCONNECT "GDGTDCNT" + +/** + * The GADGET DISCONNECT request structure. + */ +typedef struct UTSPKTREQGDGTDCNT +{ + /** Embedded packet header. */ + UTSPKTHDR Hdr; + /** Gadget ID as returned from the GADGET CREATE request on success. */ + uint32_t idGadget; + /** Padding - reserved. */ + uint8_t au8Padding[12]; +} UTSPKTREQGDGTDCNT; +AssertCompileSizeAlignment(UTSPKTREQGDGTDCNT, UTSPKT_ALIGNMENT); +/** Pointer to a GADGET CONNECT request structure. */ +typedef UTSPKTREQGDGTDCNT *PUTSPKTREQGDGTDCNT; + +/* No additional structure for the reply (just standard STATUS packet). */ + +/** + * Checks if the two opcodes match. + * + * @returns true on match, false on mismatch. + * @param pPktHdr The packet header. + * @param pszOpcode2 The opcode we're comparing with. Does not have + * to be the whole 8 chars long. + */ +DECLINLINE(bool) utsIsSameOpcode(PCUTSPKTHDR pPktHdr, const char *pszOpcode2) +{ + if (pPktHdr->achOpcode[0] != pszOpcode2[0]) + return false; + if (pPktHdr->achOpcode[1] != pszOpcode2[1]) + return false; + + unsigned i = 2; + while ( i < RT_SIZEOFMEMB(UTSPKTHDR, achOpcode) + && pszOpcode2[i] != '\0') + { + if (pPktHdr->achOpcode[i] != pszOpcode2[i]) + break; + i++; + } + + if ( i < RT_SIZEOFMEMB(UTSPKTHDR, achOpcode) + && pszOpcode2[i] == '\0') + { + while ( i < RT_SIZEOFMEMB(UTSPKTHDR, achOpcode) + && pPktHdr->achOpcode[i] == ' ') + i++; + } + + return i == RT_SIZEOFMEMB(UTSPKTHDR, achOpcode); +} + +/** + * Converts a UTS request packet from host to network byte ordering. + * + * @param pPktHdr The packet to convert. + */ +DECLHIDDEN(void) utsProtocolReqH2N(PUTSPKTHDR pPktHdr); + +/** + * Converts a UTS request packet from network to host byte ordering. + * + * @param pPktHdr The packet to convert. + */ +DECLHIDDEN(void) utsProtocolReqN2H(PUTSPKTHDR pPktHdr); + +/** + * Converts a UTS reply packet from host to network byte ordering. + * + * @param pPktHdr The packet to convert. + */ +DECLHIDDEN(void) utsProtocolRepH2N(PUTSPKTHDR pPktHdr); + +/** + * Converts a UTS reply packet from network to host byte ordering. + * + * @param pPktHdr The packet to convert. + */ +DECLHIDDEN(void) utsProtocolRepN2H(PUTSPKTHDR pPktHdr); + +RT_C_DECLS_END + +#endif /* !VBOX_INCLUDED_SRC_usb_UsbTestServiceProtocol_h */ diff --git a/src/VBox/ValidationKit/utils/usb/UsbTestServiceTcp.cpp b/src/VBox/ValidationKit/utils/usb/UsbTestServiceTcp.cpp new file mode 100644 index 00000000..4d9013f7 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTestServiceTcp.cpp @@ -0,0 +1,512 @@ +/* $Id: UsbTestServiceTcp.cpp $ */ +/** @file + * UsbTestService - Remote USB test configuration and execution server, TCP/IP Transport Layer. + */ + +/* + * 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 <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_DEFAULT +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/critsect.h> +#include <iprt/err.h> +#include <iprt/log.h> +#include <iprt/mem.h> +#include <iprt/message.h> +#include <iprt/poll.h> +#include <iprt/string.h> +#include <iprt/tcp.h> +#include <iprt/thread.h> +#include <iprt/time.h> + +#include "UsbTestServiceInternal.h" + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ +/** The default server port. */ +#define UTS_TCP_DEF_BIND_PORT 6042 +/** The default server bind address. */ +#define UTS_TCP_DEF_BIND_ADDRESS "" + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * TCP specific client data. + */ +typedef struct UTSTRANSPORTCLIENT +{ + /** Socket of the current client. */ + RTSOCKET hTcpClient; + /** The size of the stashed data. */ + size_t cbTcpStashed; + /** The size of the stashed data allocation. */ + size_t cbTcpStashedAlloced; + /** The stashed data. */ + uint8_t *pbTcpStashed; +} UTSTRANSPORTCLIENT; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** @name TCP Parameters + * @{ */ +/** The addresses to bind to. Empty string means any. */ +static char g_szTcpBindAddr[256] = UTS_TCP_DEF_BIND_ADDRESS; +/** The TCP port to listen to. */ +static uint32_t g_uTcpBindPort = UTS_TCP_DEF_BIND_PORT; +/** @} */ + +/** Pointer to the TCP server instance. */ +static PRTTCPSERVER g_pTcpServer = NULL; +#if 0 /* unused */ +/** Stop connecting attempts when set. */ +static bool g_fTcpStopConnecting = false; +#endif + + + +/** + * Disconnects the current client and frees all stashed data. + */ +static void utsTcpDisconnectClient(PUTSTRANSPORTCLIENT pClient) +{ + if (pClient->hTcpClient != NIL_RTSOCKET) + { + int rc = RTTcpServerDisconnectClient2(pClient->hTcpClient); + pClient->hTcpClient = NIL_RTSOCKET; + AssertRCSuccess(rc); + } + + if (pClient->pbTcpStashed) + { + RTMemFree(pClient->pbTcpStashed); + pClient->pbTcpStashed = NULL; + } +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnWaitForConnect} + */ +static DECLCALLBACK(int) utsTcpWaitForConnect(PPUTSTRANSPORTCLIENT ppClientNew) +{ + int rc; + RTSOCKET hClientNew; + + rc = RTTcpServerListen2(g_pTcpServer, &hClientNew); + Log(("utsTcpWaitForConnect: RTTcpServerListen2 -> %Rrc\n", rc)); + + if (RT_SUCCESS(rc)) + { + PUTSTRANSPORTCLIENT pClient = (PUTSTRANSPORTCLIENT)RTMemAllocZ(sizeof(UTSTRANSPORTCLIENT)); + if (RT_LIKELY(pClient)) + { + pClient->hTcpClient = hClientNew; + pClient->cbTcpStashed = 0; + pClient->cbTcpStashedAlloced = 0; + pClient->pbTcpStashed = NULL; + *ppClientNew = pClient; + } + else + { + RTTcpServerDisconnectClient2(hClientNew); + rc = VERR_NO_MEMORY; + } + } + + return rc; +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnNotifyReboot} + */ +static DECLCALLBACK(void) utsTcpNotifyReboot(void) +{ + Log(("utsTcpNotifyReboot: RTTcpServerDestroy(%p)\n", g_pTcpServer)); + if (g_pTcpServer) + { + int rc = RTTcpServerDestroy(g_pTcpServer); + if (RT_FAILURE(rc)) + RTMsgInfo("RTTcpServerDestroy failed in utsTcpNotifyReboot: %Rrc", rc); + g_pTcpServer = NULL; + } +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnNotifyBye} + */ +static DECLCALLBACK(void) utsTcpNotifyBye(PUTSTRANSPORTCLIENT pClient) +{ + Log(("utsTcpNotifyBye: utsTcpDisconnectClient %RTsock\n", pClient->hTcpClient)); + utsTcpDisconnectClient(pClient); + RTMemFree(pClient); +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnNotifyHowdy} + */ +static DECLCALLBACK(void) utsTcpNotifyHowdy(PUTSTRANSPORTCLIENT pClient) +{ + /* nothing to do here */ + RT_NOREF1(pClient); +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnBabble} + */ +static DECLCALLBACK(void) utsTcpBabble(PUTSTRANSPORTCLIENT pClient, PCUTSPKTHDR pPktHdr, RTMSINTERVAL cMsSendTimeout) +{ + /* + * Try send the babble reply. + */ + NOREF(cMsSendTimeout); /** @todo implement the timeout here; non-blocking write + select-on-write. */ + int rc; + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, UTSPKT_ALIGNMENT); + do rc = RTTcpWrite(pClient->hTcpClient, pPktHdr, cbToSend); + while (rc == VERR_INTERRUPTED); + + /* + * Disconnect the client. + */ + Log(("utsTcpBabble: utsTcpDisconnectClient(%RTsock) (RTTcpWrite rc=%Rrc)\n", pClient->hTcpClient, rc)); + utsTcpDisconnectClient(pClient); +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnSendPkt} + */ +static DECLCALLBACK(int) utsTcpSendPkt(PUTSTRANSPORTCLIENT pClient, PCUTSPKTHDR pPktHdr) +{ + Assert(pPktHdr->cb >= sizeof(UTSPKTHDR)); + + /* + * Write it. + */ + size_t cbToSend = RT_ALIGN_Z(pPktHdr->cb, UTSPKT_ALIGNMENT); + int rc = RTTcpWrite(pClient->hTcpClient, pPktHdr, cbToSend); + if ( RT_FAILURE(rc) + && rc != VERR_INTERRUPTED) + { + /* assume fatal connection error. */ + Log(("RTTcpWrite -> %Rrc -> utsTcpDisconnectClient(%RTsock)\n", rc, pClient->hTcpClient)); + utsTcpDisconnectClient(pClient); + } + + return rc; +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnRecvPkt} + */ +static DECLCALLBACK(int) utsTcpRecvPkt(PUTSTRANSPORTCLIENT pClient, PPUTSPKTHDR ppPktHdr) +{ + int rc = VINF_SUCCESS; + *ppPktHdr = NULL; + + /* + * Read state. + */ + size_t offData = 0; + size_t cbData = 0; + size_t cbDataAlloced; + uint8_t *pbData = NULL; + + /* + * Any stashed data? + */ + if (pClient->cbTcpStashedAlloced) + { + offData = pClient->cbTcpStashed; + cbDataAlloced = pClient->cbTcpStashedAlloced; + pbData = pClient->pbTcpStashed; + + pClient->cbTcpStashed = 0; + pClient->cbTcpStashedAlloced = 0; + pClient->pbTcpStashed = NULL; + } + else + { + cbDataAlloced = RT_ALIGN_Z(64, UTSPKT_ALIGNMENT); + pbData = (uint8_t *)RTMemAlloc(cbDataAlloced); + if (!pbData) + return VERR_NO_MEMORY; + } + + /* + * Read and valid the length. + */ + while (offData < sizeof(uint32_t)) + { + size_t cbRead; + rc = RTTcpRead(pClient->hTcpClient, pbData + offData, sizeof(uint32_t) - offData, &cbRead); + if (RT_FAILURE(rc)) + break; + if (cbRead == 0) + { + Log(("utsTcpRecvPkt: RTTcpRead -> %Rrc / cbRead=0 -> VERR_NET_NOT_CONNECTED (#1)\n", rc)); + rc = VERR_NET_NOT_CONNECTED; + break; + } + offData += cbRead; + } + if (RT_SUCCESS(rc)) + { + ASMCompilerBarrier(); /* paranoia^3 */ + cbData = *(uint32_t volatile *)pbData; + if (cbData >= sizeof(UTSPKTHDR) && cbData <= UTSPKT_MAX_SIZE) + { + /* + * Align the length and reallocate the return packet it necessary. + */ + cbData = RT_ALIGN_Z(cbData, UTSPKT_ALIGNMENT); + if (cbData > cbDataAlloced) + { + void *pvNew = RTMemRealloc(pbData, cbData); + if (pvNew) + { + pbData = (uint8_t *)pvNew; + cbDataAlloced = cbData; + } + else + rc = VERR_NO_MEMORY; + } + if (RT_SUCCESS(rc)) + { + /* + * Read the remainder of the data. + */ + while (offData < cbData) + { + size_t cbRead; + rc = RTTcpRead(pClient->hTcpClient, pbData + offData, cbData - offData, &cbRead); + if (RT_FAILURE(rc)) + break; + if (cbRead == 0) + { + Log(("utsTcpRecvPkt: RTTcpRead -> %Rrc / cbRead=0 -> VERR_NET_NOT_CONNECTED (#2)\n", rc)); + rc = VERR_NET_NOT_CONNECTED; + break; + } + offData += cbRead; + } + } + } + else + rc = VERR_NET_PROTOCOL_ERROR; + } + if (RT_SUCCESS(rc)) + *ppPktHdr = (PUTSPKTHDR)pbData; + else + { + /* + * Deal with errors. + */ + if (rc == VERR_INTERRUPTED) + { + /* stash it away for the next call. */ + pClient->cbTcpStashed = cbData; + pClient->cbTcpStashedAlloced = cbDataAlloced; + pClient->pbTcpStashed = pbData; + } + else + { + RTMemFree(pbData); + + /* assume fatal connection error. */ + Log(("utsTcpRecvPkt: RTTcpRead -> %Rrc -> utsTcpDisconnectClient(%RTsock)\n", rc, pClient->hTcpClient)); + utsTcpDisconnectClient(pClient); + } + } + + return rc; +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnPollSetAdd} + */ +static DECLCALLBACK(int) utsTcpPollSetAdd(RTPOLLSET hPollSet, PUTSTRANSPORTCLIENT pClient, uint32_t idStart) +{ + return RTPollSetAddSocket(hPollSet, pClient->hTcpClient, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, idStart); +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnPollSetRemove} + */ +static DECLCALLBACK(int) utsTcpPollSetRemove(RTPOLLSET hPollSet, PUTSTRANSPORTCLIENT pClient, uint32_t idStart) +{ + RT_NOREF1(pClient); + return RTPollSetRemove(hPollSet, idStart); +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnPollIn} + */ +static DECLCALLBACK(bool) utsTcpPollIn(PUTSTRANSPORTCLIENT pClient) +{ + int rc = RTTcpSelectOne(pClient->hTcpClient, 0/*cMillies*/); + return RT_SUCCESS(rc); +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnTerm} + */ +static DECLCALLBACK(void) utsTcpTerm(void) +{ + /* Shut down the server (will wake up thread). */ + if (g_pTcpServer) + { + Log(("utsTcpTerm: Destroying server...\n")); + int rc = RTTcpServerDestroy(g_pTcpServer); + if (RT_FAILURE(rc)) + RTMsgInfo("RTTcpServerDestroy failed in utsTcpTerm: %Rrc", rc); + g_pTcpServer = NULL; + } + + Log(("utsTcpTerm: done\n")); +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnInit} + */ +static DECLCALLBACK(int) utsTcpInit(void) +{ + int rc = RTTcpServerCreateEx(g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, &g_pTcpServer); + if (RT_FAILURE(rc)) + { + if (rc == VERR_NET_DOWN) + { + RTMsgInfo("RTTcpServerCreateEx(%s, %u,) failed: %Rrc, retrying for 20 seconds...\n", + g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, rc); + uint64_t StartMs = RTTimeMilliTS(); + do + { + RTThreadSleep(1000); + rc = RTTcpServerCreateEx(g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, &g_pTcpServer); + } while ( rc == VERR_NET_DOWN + && RTTimeMilliTS() - StartMs < 20000); + if (RT_SUCCESS(rc)) + RTMsgInfo("RTTcpServerCreateEx succceeded.\n"); + } + if (RT_FAILURE(rc)) + { + g_pTcpServer = NULL; + RTMsgError("RTTcpServerCreateEx(%s, %u,) failed: %Rrc\n", + g_szTcpBindAddr[0] ? g_szTcpBindAddr : NULL, g_uTcpBindPort, rc); + } + } + + return rc; +} + +/** Options */ +enum UTSTCPOPT +{ + UTSTCPOPT_BIND_ADDRESS = 1000, + UTSTCPOPT_BIND_PORT +}; + +/** + * @interface_method_impl{UTSTRANSPORT,pfnOption} + */ +static DECLCALLBACK(int) utsTcpOption(int ch, PCRTGETOPTUNION pVal) +{ + int rc; + + switch (ch) + { + case UTSTCPOPT_BIND_ADDRESS: + rc = RTStrCopy(g_szTcpBindAddr, sizeof(g_szTcpBindAddr), pVal->psz); + if (RT_FAILURE(rc)) + return RTMsgErrorRc(VERR_INVALID_PARAMETER, "TCP bind address is too long (%Rrc)", rc); + return VINF_SUCCESS; + + case UTSTCPOPT_BIND_PORT: + g_uTcpBindPort = pVal->u16 == 0 ? UTS_TCP_DEF_BIND_PORT : pVal->u16; + return VINF_SUCCESS; + } + return VERR_TRY_AGAIN; +} + +/** + * @interface_method_impl{UTSTRANSPORT,pfnUsage} + */ +DECLCALLBACK(void) utsTcpUsage(PRTSTREAM pStream) +{ + RTStrmPrintf(pStream, + " --tcp-bind-address <address>\n" + " The address(es) to listen to TCP connection on. Empty string\n" + " means any address, this is the default.\n" + " --tcp-bind-port <port>\n" + " The port to listen to TCP connections on.\n" + " Default: %u\n" + , UTS_TCP_DEF_BIND_PORT); +} + +/** Command line options for the TCP/IP transport layer. */ +static const RTGETOPTDEF g_TcpOpts[] = +{ + { "--tcp-bind-address", UTSTCPOPT_BIND_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-bind-port", UTSTCPOPT_BIND_PORT, RTGETOPT_REQ_UINT16 } +}; + +/** TCP/IP transport layer. */ +const UTSTRANSPORT g_TcpTransport = +{ + /* .szName = */ "tcp", + /* .pszDesc = */ "TCP/IP", + /* .cOpts = */ &g_TcpOpts[0], + /* .paOpts = */ RT_ELEMENTS(g_TcpOpts), + /* .pfnUsage = */ utsTcpUsage, + /* .pfnOption = */ utsTcpOption, + /* .pfnInit = */ utsTcpInit, + /* .pfnTerm = */ utsTcpTerm, + /* .pfnWaitForConnect = */ utsTcpWaitForConnect, + /* .pfnPollIn = */ utsTcpPollIn, + /* .pfnPollSetAdd = */ utsTcpPollSetAdd, + /* .pfnPollSetRemove = */ utsTcpPollSetRemove, + /* .pfnRecvPkt = */ utsTcpRecvPkt, + /* .pfnSendPkt = */ utsTcpSendPkt, + /* .pfnBabble = */ utsTcpBabble, + /* .pfnNotifyHowdy = */ utsTcpNotifyHowdy, + /* .pfnNotifyBye = */ utsTcpNotifyBye, + /* .pfnNotifyReboot = */ utsTcpNotifyReboot, + /* .u32EndMarker = */ UINT32_C(0x12345678) +}; |