diff options
Diffstat (limited to 'src/VBox/Devices/Audio/AudioTestServiceClient.cpp')
-rw-r--r-- | src/VBox/Devices/Audio/AudioTestServiceClient.cpp | 608 |
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); +} + |