summaryrefslogtreecommitdiffstats
path: root/src/VBox/Devices/Audio/AudioTestServiceClient.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/Devices/Audio/AudioTestServiceClient.cpp')
-rw-r--r--src/VBox/Devices/Audio/AudioTestServiceClient.cpp608
1 files changed, 608 insertions, 0 deletions
diff --git a/src/VBox/Devices/Audio/AudioTestServiceClient.cpp b/src/VBox/Devices/Audio/AudioTestServiceClient.cpp
new file mode 100644
index 00000000..e826a9c5
--- /dev/null
+++ b/src/VBox/Devices/Audio/AudioTestServiceClient.cpp
@@ -0,0 +1,608 @@
+/* $Id: AudioTestServiceClient.cpp $ */
+/** @file
+ * AudioTestServiceClient - Audio Test Service (ATS), Client helpers.
+ *
+ * Note: Only does TCP/IP as transport layer for now.
+ */
+
+/*
+ * 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>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+
+/*********************************************************************************************************************************
+* Header Files *
+*********************************************************************************************************************************/
+#define LOG_GROUP LOG_GROUP_AUDIO_TEST
+
+#include <iprt/crc.h>
+#include <iprt/err.h>
+#include <iprt/file.h>
+#include <iprt/mem.h>
+#include <iprt/string.h>
+#include <iprt/tcp.h>
+
+#include <VBox/log.h>
+
+#include "AudioTestService.h"
+#include "AudioTestServiceInternal.h"
+#include "AudioTestServiceClient.h"
+
+/** @todo Use common defines between server protocol and this client. */
+
+/**
+ * A generic ATS reply, used by the client
+ * to process the incoming packets.
+ */
+typedef struct ATSSRVREPLY
+{
+ char szOp[ATSPKT_OPCODE_MAX_LEN];
+ /** Pointer to payload data.
+ * This does *not* include the header! */
+ void *pvPayload;
+ /** Size (in bytes) of the payload data.
+ * This does *not* include the header! */
+ size_t cbPayload;
+} ATSSRVREPLY;
+/** Pointer to a generic ATS reply. */
+typedef struct ATSSRVREPLY *PATSSRVREPLY;
+
+
+/*********************************************************************************************************************************
+* Prototypes *
+*********************************************************************************************************************************/
+static int audioTestSvcClientDisconnectInternal(PATSCLIENT pClient);
+
+/**
+ * Initializes an ATS client, internal version.
+ *
+ * @param pClient Client to initialize.
+ */
+static void audioTestSvcClientInit(PATSCLIENT pClient)
+{
+ RT_BZERO(pClient, sizeof(ATSCLIENT));
+}
+
+/**
+ * Destroys an ATS server reply.
+ *
+ * @param pReply Reply to destroy.
+ */
+static void audioTestSvcClientReplyDestroy(PATSSRVREPLY pReply)
+{
+ if (!pReply)
+ return;
+
+ if (pReply->pvPayload)
+ {
+ Assert(pReply->cbPayload);
+ RTMemFree(pReply->pvPayload);
+ pReply->pvPayload = NULL;
+ }
+
+ pReply->cbPayload = 0;
+}
+
+/**
+ * Receives a reply from an ATS server.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to receive reply for.
+ * @param pReply Where to store the reply.
+ * The reply must be destroyed with audioTestSvcClientReplyDestroy() then.
+ * @param fNoDataOk If it's okay that the reply is not expected to have any payload.
+ */
+static int audioTestSvcClientRecvReply(PATSCLIENT pClient, PATSSRVREPLY pReply, bool fNoDataOk)
+{
+ LogFlowFuncEnter();
+
+ PATSPKTHDR pPktHdr;
+ int rc = pClient->pTransport->pfnRecvPkt(pClient->pTransportInst, pClient->pTransportClient, &pPktHdr);
+ if (RT_SUCCESS(rc))
+ {
+ AssertReleaseMsgReturn(pPktHdr->cb >= sizeof(ATSPKTHDR),
+ ("audioTestSvcClientRecvReply: Received invalid packet size (%RU32)\n", pPktHdr->cb),
+ VERR_NET_PROTOCOL_ERROR);
+ pReply->cbPayload = pPktHdr->cb - sizeof(ATSPKTHDR);
+ Log3Func(("szOp=%.8s, cb=%RU32\n", pPktHdr->achOpcode, pPktHdr->cb));
+ if (pReply->cbPayload)
+ {
+ pReply->pvPayload = RTMemDup((uint8_t *)pPktHdr + sizeof(ATSPKTHDR), pReply->cbPayload);
+ }
+ else
+ pReply->pvPayload = NULL;
+
+ if ( !pReply->cbPayload
+ && !fNoDataOk)
+ {
+ LogRelFunc(("Payload is empty (%zu), but caller expected data\n", pReply->cbPayload));
+ rc = VERR_NET_PROTOCOL_ERROR;
+ }
+ else
+ {
+ memcpy(&pReply->szOp, &pPktHdr->achOpcode, sizeof(pReply->szOp));
+ }
+
+ RTMemFree(pPktHdr);
+ pPktHdr = NULL;
+ }
+
+ if (RT_FAILURE(rc))
+ LogRelFunc(("Receiving reply from server failed with %Rrc\n", rc));
+
+ LogFlowFuncLeaveRC(rc);
+ return rc;
+}
+
+/**
+ * Receives a reply for an ATS server and checks if it is an acknowledge (success) one.
+ *
+ * @returns VBox status code.
+ * @retval VERR_NET_PROTOCOL_ERROR if the reply indicates a failure.
+ * @param pClient Client to receive reply for.
+ */
+static int audioTestSvcClientRecvAck(PATSCLIENT pClient)
+{
+ ATSSRVREPLY Reply;
+ RT_ZERO(Reply);
+
+ int rc = audioTestSvcClientRecvReply(pClient, &Reply, true /* fNoDataOk */);
+ if (RT_SUCCESS(rc))
+ {
+ /* Most likely cases first. */
+ if ( RTStrNCmp(Reply.szOp, "ACK ", ATSPKT_OPCODE_MAX_LEN) == 0)
+ {
+ /* Nothing to do here. */
+ }
+ else if (RTStrNCmp(Reply.szOp, "FAILED ", ATSPKT_OPCODE_MAX_LEN) == 0)
+ {
+ LogRelFunc(("Received error from server (cbPayload=%zu)\n", Reply.cbPayload));
+
+ if (Reply.cbPayload)
+ {
+ if ( Reply.cbPayload >= sizeof(int) /* At least the rc must be present. */
+ && Reply.cbPayload <= sizeof(ATSPKTREPFAIL) - sizeof(ATSPKTHDR))
+ {
+ rc = *(int *)Reply.pvPayload; /* Reach error code back to caller. */
+
+ const char *pcszMsg = (char *)Reply.pvPayload + sizeof(int);
+ /** @todo Check NULL termination of pcszMsg? */
+
+ LogRelFunc(("Error message: %s (%Rrc)\n", pcszMsg, rc));
+ }
+ else
+ {
+ LogRelFunc(("Received invalid failure payload (cb=%zu)\n", Reply.cbPayload));
+ rc = VERR_NET_PROTOCOL_ERROR;
+ }
+ }
+ }
+ else
+ {
+ LogRelFunc(("Received invalid opcode ('%.8s')\n", Reply.szOp));
+ rc = VERR_NET_PROTOCOL_ERROR;
+ }
+
+ audioTestSvcClientReplyDestroy(&Reply);
+ }
+
+ LogRelFlowFuncLeaveRC(rc);
+ return rc;
+}
+
+/**
+ * Sends a message plus optional payload to an ATS server.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to send message for.
+ * @param pvHdr Pointer to header data to send.
+ * @param cbHdr Size (in bytes) of \a pvHdr to send.
+ */
+static int audioTestSvcClientSendMsg(PATSCLIENT pClient, void *pvHdr, size_t cbHdr)
+{
+ RT_NOREF(cbHdr);
+ AssertPtrReturn(pClient->pTransport, VERR_INVALID_POINTER);
+ AssertPtrReturn(pClient->pTransportInst, VERR_INVALID_POINTER);
+ AssertPtrReturn(pClient->pTransportClient, VERR_INVALID_POINTER);
+ return pClient->pTransport->pfnSendPkt(pClient->pTransportInst, pClient->pTransportClient, (PCATSPKTHDR)pvHdr);
+}
+
+/**
+ * Initializes a client request header.
+ *
+ * @param pReqHdr Request header to initialize.
+ * @param cbReq Size (in bytes) the request will have (does *not* include payload).
+ * @param pszOp Operation to perform with the request.
+ * @param cbPayload Size (in bytes) of payload that will follow the header. Optional and can be 0.
+ */
+DECLINLINE(void) audioTestSvcClientReqHdrInit(PATSPKTHDR pReqHdr, size_t cbReq, const char *pszOp, size_t cbPayload)
+{
+ AssertReturnVoid(strlen(pszOp) >= 2);
+ AssertReturnVoid(strlen(pszOp) <= ATSPKT_OPCODE_MAX_LEN);
+
+ /** @todo Validate opcode. */
+
+ RT_BZERO(pReqHdr, sizeof(ATSPKTHDR));
+
+ memcpy(pReqHdr->achOpcode, pszOp, strlen(pszOp));
+ pReqHdr->uCrc32 = 0; /** @todo Do CRC-32 calculation. */
+ pReqHdr->cb = (uint32_t)cbReq + (uint32_t)cbPayload;
+
+ Assert(pReqHdr->cb <= ATSPKT_MAX_SIZE);
+}
+
+/**
+ * Sends an acknowledege response back to the server.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to send command for.
+ */
+static int audioTestSvcClientSendAck(PATSCLIENT pClient)
+{
+ ATSPKTHDR Req;
+ audioTestSvcClientReqHdrInit(&Req, sizeof(Req), "ACK ", 0);
+
+ return audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+}
+
+/**
+ * Sends a greeting command (handshake) to an ATS server.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to send command for.
+ */
+static int audioTestSvcClientDoGreet(PATSCLIENT pClient)
+{
+ ATSPKTREQHOWDY Req;
+ Req.uVersion = ATS_PROTOCOL_VS;
+ audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_HOWDY, 0);
+ int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+ if (RT_SUCCESS(rc))
+ rc = audioTestSvcClientRecvAck(pClient);
+ return rc;
+}
+
+/**
+ * Tells the ATS server that we want to disconnect.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to disconnect.
+ */
+static int audioTestSvcClientDoBye(PATSCLIENT pClient)
+{
+ ATSPKTHDR Req;
+ audioTestSvcClientReqHdrInit(&Req, sizeof(Req), ATSPKT_OPCODE_BYE, 0);
+ int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+ if (RT_SUCCESS(rc))
+ rc = audioTestSvcClientRecvAck(pClient);
+ return rc;
+}
+
+/**
+ * Creates an ATS client.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to create.
+ */
+int AudioTestSvcClientCreate(PATSCLIENT pClient)
+{
+ audioTestSvcClientInit(pClient);
+
+ /*
+ * The default transporter is the first one.
+ */
+ pClient->pTransport = g_apTransports[0]; /** @todo Make this dynamic. */
+
+ return pClient->pTransport->pfnCreate(&pClient->pTransportInst);
+}
+
+/**
+ * Destroys an ATS client.
+ *
+ * @param pClient Client to destroy.
+ */
+void AudioTestSvcClientDestroy(PATSCLIENT pClient)
+{
+ if (!pClient)
+ return;
+
+ /* ignore rc */ audioTestSvcClientDisconnectInternal(pClient);
+
+ if (pClient->pTransport)
+ {
+ pClient->pTransport->pfnDestroy(pClient->pTransportInst);
+ pClient->pTransportInst = NULL; /* Invalidate pointer. */
+ }
+}
+
+/**
+ * Handles a command line option.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to handle option for.
+ * @param ch Option (short) to handle.
+ * @param pVal Option union to store the result in on success.
+ */
+int AudioTestSvcClientHandleOption(PATSCLIENT pClient, int ch, PCRTGETOPTUNION pVal)
+{
+ AssertPtrReturn(pClient->pTransport, VERR_WRONG_ORDER); /* Must be created first via AudioTestSvcClientCreate(). */
+ if (!pClient->pTransport->pfnOption)
+ return VERR_GETOPT_UNKNOWN_OPTION;
+ return pClient->pTransport->pfnOption(pClient->pTransportInst, ch, pVal);
+}
+
+/**
+ * Connects to an ATS peer, extended version.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to connect.
+ * @param msTimeout Timeout (in ms) waiting for a connection to be established.
+ * Use RT_INDEFINITE_WAIT to wait indefinitely.
+ */
+int AudioTestSvcClientConnectEx(PATSCLIENT pClient, RTMSINTERVAL msTimeout)
+{
+ if (pClient->pTransportClient)
+ return VERR_NET_ALREADY_CONNECTED;
+
+ int rc = pClient->pTransport->pfnStart(pClient->pTransportInst);
+ if (RT_SUCCESS(rc))
+ {
+ rc = pClient->pTransport->pfnWaitForConnect(pClient->pTransportInst,
+ msTimeout, NULL /* pfFromServer */, &pClient->pTransportClient);
+ if (RT_SUCCESS(rc))
+ {
+ rc = audioTestSvcClientDoGreet(pClient);
+ }
+ }
+
+ if (RT_FAILURE(rc))
+ LogRelFunc(("Connecting to server (%RU32ms timeout) failed with %Rrc\n", msTimeout, rc));
+
+ return rc;
+}
+
+/**
+ * Connects to an ATS peer.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to connect.
+ */
+int AudioTestSvcClientConnect(PATSCLIENT pClient)
+{
+ return AudioTestSvcClientConnectEx(pClient, 30 * 1000 /* msTimeout */);
+}
+
+/**
+ * Tells the server to begin a new test set.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to issue command for.
+ * @param pszTag Tag to use for the test set to begin.
+ */
+int AudioTestSvcClientTestSetBegin(PATSCLIENT pClient, const char *pszTag)
+{
+ ATSPKTREQTSETBEG Req;
+
+ int rc = RTStrCopy(Req.szTag, sizeof(Req.szTag), pszTag);
+ AssertRCReturn(rc, rc);
+
+ audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TESTSET_BEGIN, 0);
+
+ rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+ if (RT_SUCCESS(rc))
+ rc = audioTestSvcClientRecvAck(pClient);
+
+ return rc;
+}
+
+/**
+ * Tells the server to end a runing test set.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to issue command for.
+ * @param pszTag Tag of test set to end.
+ */
+int AudioTestSvcClientTestSetEnd(PATSCLIENT pClient, const char *pszTag)
+{
+ ATSPKTREQTSETEND Req;
+
+ int rc = RTStrCopy(Req.szTag, sizeof(Req.szTag), pszTag);
+ AssertRCReturn(rc, rc);
+
+ audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TESTSET_END, 0);
+
+ rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+ if (RT_SUCCESS(rc))
+ rc = audioTestSvcClientRecvAck(pClient);
+
+ return rc;
+}
+
+/**
+ * Tells the server to play a (test) tone.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to issue command for.
+ * @param pToneParms Tone parameters to use.
+ * @note How (and if) the server plays a tone depends on the actual implementation side.
+ */
+int AudioTestSvcClientTonePlay(PATSCLIENT pClient, PAUDIOTESTTONEPARMS pToneParms)
+{
+ ATSPKTREQTONEPLAY Req;
+
+ memcpy(&Req.ToneParms, pToneParms, sizeof(AUDIOTESTTONEPARMS));
+
+ audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TONE_PLAY, 0);
+
+ int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+ if (RT_SUCCESS(rc))
+ rc = audioTestSvcClientRecvAck(pClient);
+
+ return rc;
+}
+
+/**
+ * Tells the server to record a (test) tone.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to issue command for.
+ * @param pToneParms Tone parameters to use.
+ * @note How (and if) the server plays a tone depends on the actual implementation side.
+ */
+int AudioTestSvcClientToneRecord(PATSCLIENT pClient, PAUDIOTESTTONEPARMS pToneParms)
+{
+ ATSPKTREQTONEREC Req;
+
+ memcpy(&Req.ToneParms, pToneParms, sizeof(AUDIOTESTTONEPARMS));
+
+ audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TONE_RECORD, 0);
+
+ int rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+ if (RT_SUCCESS(rc))
+ rc = audioTestSvcClientRecvAck(pClient);
+
+ return rc;
+}
+
+/**
+ * Tells the server to send (download) a (packed up) test set archive.
+ * The test set must not be running / open anymore.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to issue command for.
+ * @param pszTag Tag of test set to send.
+ * @param pszPathOutAbs Absolute path where to store the downloaded test set archive.
+ */
+int AudioTestSvcClientTestSetDownload(PATSCLIENT pClient, const char *pszTag, const char *pszPathOutAbs)
+{
+ ATSPKTREQTSETSND Req;
+
+ int rc = RTStrCopy(Req.szTag, sizeof(Req.szTag), pszTag);
+ AssertRCReturn(rc, rc);
+
+ audioTestSvcClientReqHdrInit(&Req.Hdr, sizeof(Req), ATSPKT_OPCODE_TESTSET_SEND, 0);
+
+ RTFILE hFile;
+ rc = RTFileOpen(&hFile, pszPathOutAbs, RTFILE_O_WRITE | RTFILE_O_CREATE | RTFILE_O_DENY_WRITE);
+ AssertRCReturn(rc, rc);
+
+ rc = audioTestSvcClientSendMsg(pClient, &Req, sizeof(Req));
+ while (RT_SUCCESS(rc))
+ {
+ ATSSRVREPLY Reply;
+ RT_ZERO(Reply);
+
+ rc = audioTestSvcClientRecvReply(pClient, &Reply, false /* fNoDataOk */);
+ if (RT_SUCCESS(rc))
+ {
+ /* Extract received CRC32 checksum. */
+ const size_t cbCrc32 = sizeof(uint32_t); /* Skip CRC32 in payload for actual CRC verification. */
+
+ uint32_t uSrcCrc32;
+ memcpy(&uSrcCrc32, Reply.pvPayload, cbCrc32);
+
+ if (uSrcCrc32)
+ {
+ const uint32_t uDstCrc32 = RTCrc32((uint8_t *)Reply.pvPayload + cbCrc32, Reply.cbPayload - cbCrc32);
+
+ Log2Func(("uSrcCrc32=%#x, cbRead=%zu -> uDstCrc32=%#x\n"
+ "%.*Rhxd\n",
+ uSrcCrc32, Reply.cbPayload - cbCrc32, uDstCrc32,
+ RT_MIN(64, Reply.cbPayload - cbCrc32), (uint8_t *)Reply.pvPayload + cbCrc32));
+
+ if (uSrcCrc32 != uDstCrc32)
+ rc = VERR_TAR_CHKSUM_MISMATCH; /** @todo Fudge! */
+ }
+
+ if (RT_SUCCESS(rc))
+ {
+ if ( RTStrNCmp(Reply.szOp, "DATA ", ATSPKT_OPCODE_MAX_LEN) == 0
+ && Reply.pvPayload
+ && Reply.cbPayload)
+ {
+ rc = RTFileWrite(hFile, (uint8_t *)Reply.pvPayload + cbCrc32, Reply.cbPayload - cbCrc32, NULL);
+ }
+ else if (RTStrNCmp(Reply.szOp, "DATA EOF", ATSPKT_OPCODE_MAX_LEN) == 0)
+ {
+ rc = VINF_EOF;
+ }
+ else
+ {
+ AssertMsgFailed(("Got unexpected reply '%s'", Reply.szOp));
+ rc = VERR_NOT_SUPPORTED;
+ }
+ }
+ }
+
+ audioTestSvcClientReplyDestroy(&Reply);
+
+ int rc2 = audioTestSvcClientSendAck(pClient);
+ if (rc == VINF_SUCCESS) /* Might be VINF_EOF already. */
+ rc = rc2;
+
+ if (rc == VINF_EOF)
+ break;
+ }
+
+ int rc2 = RTFileClose(hFile);
+ if (RT_SUCCESS(rc))
+ rc = rc2;
+
+ return rc;
+}
+
+/**
+ * Disconnects from an ATS server, internal version.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to disconnect.
+ */
+static int audioTestSvcClientDisconnectInternal(PATSCLIENT pClient)
+{
+ if (!pClient->pTransportClient) /* Not connected (yet)? Bail out early. */
+ return VINF_SUCCESS;
+
+ int rc = audioTestSvcClientDoBye(pClient);
+ if (RT_SUCCESS(rc))
+ {
+ if (pClient->pTransport->pfnNotifyBye)
+ pClient->pTransport->pfnNotifyBye(pClient->pTransportInst, pClient->pTransportClient);
+
+ pClient->pTransport->pfnDisconnect(pClient->pTransportInst, pClient->pTransportClient);
+ pClient->pTransportClient = NULL;
+
+ pClient->pTransport->pfnStop(pClient->pTransportInst);
+ }
+
+ return rc;
+}
+
+/**
+ * Disconnects from an ATS server.
+ *
+ * @returns VBox status code.
+ * @param pClient Client to disconnect.
+ */
+int AudioTestSvcClientDisconnect(PATSCLIENT pClient)
+{
+ return audioTestSvcClientDisconnectInternal(pClient);
+}
+