/* $Id: tstRTLocalIpc.cpp $ */
/** @file
 * IPRT Testcase - RTLocalIpc API.
 */

/*
 * 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/localipc.h>

#include <iprt/asm.h>
#include <iprt/env.h>
#include <iprt/err.h>
#include <iprt/initterm.h>
#include <iprt/mem.h>
#include <iprt/message.h>
#include <iprt/path.h>
#include <iprt/process.h>
#include <iprt/rand.h>
#include <iprt/string.h>
#include <iprt/test.h>
#include <iprt/thread.h>
#include <iprt/time.h>


/*********************************************************************************************************************************
*   Global Variables                                                                                                             *
*********************************************************************************************************************************/
/** The test instance.*/
static RTTEST g_hTest;



static void testBasics(void)
{
    RTTestISub("Basics");

    /* Server-side. */
    RTTESTI_CHECK_RC(RTLocalIpcServerCreate(NULL, NULL, 0), VERR_INVALID_POINTER);
    RTLOCALIPCSERVER hIpcServer;
    int rc;
    RTTESTI_CHECK_RC(rc = RTLocalIpcServerCreate(&hIpcServer, NULL, 0), VERR_INVALID_POINTER);
    if (RT_SUCCESS(rc)) RTLocalIpcServerDestroy(hIpcServer);
    RTTESTI_CHECK_RC(rc = RTLocalIpcServerCreate(&hIpcServer, "", 0), VERR_INVALID_NAME);
    if (RT_SUCCESS(rc)) RTLocalIpcServerDestroy(hIpcServer);
    RTTESTI_CHECK_RC(rc = RTLocalIpcServerCreate(&hIpcServer, "BasicTest", 1234 /* Invalid flags */), VERR_INVALID_FLAGS);
    if (RT_SUCCESS(rc)) RTLocalIpcServerDestroy(hIpcServer);

    RTTESTI_CHECK_RC(RTLocalIpcServerCancel(NULL), VERR_INVALID_HANDLE);
    RTTESTI_CHECK_RC(RTLocalIpcServerDestroy(NULL), VINF_SUCCESS);

    /* Basic server creation / destruction. */
    RTTESTI_CHECK_RC_RETV(RTLocalIpcServerCreate(&hIpcServer, "BasicTest", 0), VINF_SUCCESS);
    RTTESTI_CHECK_RC(RTLocalIpcServerCancel(hIpcServer), VINF_SUCCESS);
    RTTESTI_CHECK_RC(RTLocalIpcServerDestroy(hIpcServer), VINF_OBJECT_DESTROYED);

    /* Client-side (per session). */
    RTTESTI_CHECK_RC(RTLocalIpcSessionConnect(NULL, NULL, 0), VERR_INVALID_POINTER);
    RTLOCALIPCSESSION hIpcSession;
    RTTESTI_CHECK_RC(RTLocalIpcSessionConnect(&hIpcSession, NULL, 0), VERR_INVALID_POINTER);
    if (RT_SUCCESS(rc)) RTLocalIpcSessionClose(hIpcSession);
    RTTESTI_CHECK_RC(RTLocalIpcSessionConnect(&hIpcSession, "", 0), VERR_INVALID_NAME);
    if (RT_SUCCESS(rc)) RTLocalIpcSessionClose(hIpcSession);
    RTTESTI_CHECK_RC(RTLocalIpcSessionConnect(&hIpcSession, "BasicTest", 1234 /* Invalid flags */), VERR_INVALID_FLAGS);
    if (RT_SUCCESS(rc)) RTLocalIpcSessionClose(hIpcSession);

    RTTESTI_CHECK_RC(RTLocalIpcSessionCancel(NULL), VERR_INVALID_HANDLE);
    RTTESTI_CHECK_RC(RTLocalIpcSessionClose(NULL), VINF_SUCCESS);

    /* Basic client creation / destruction. */
    RTTESTI_CHECK_RC_RETV(rc = RTLocalIpcSessionConnect(&hIpcSession, "BasicTest", 0), VERR_FILE_NOT_FOUND);
    if (RT_SUCCESS(rc)) RTLocalIpcSessionClose(hIpcSession);
    //RTTESTI_CHECK_RC(RTLocalIpcServerCancel(hIpcServer), VERR_INVALID_HANDLE);  - accessing freed memory, bad idea.
    //RTTESTI_CHECK_RC(RTLocalIpcServerDestroy(hIpcServer), VERR_INVALID_HANDLE); - accessing freed memory, bad idea.
}



/*********************************************************************************************************************************
*                                                                                                                                *
*   testSessionConnection - Connecting.                                                                                          *
*                                                                                                                                *
*********************************************************************************************************************************/

static DECLCALLBACK(int) testServerListenThread(RTTHREAD hSelf, void *pvUser)
{
    RTLOCALIPCSERVER hIpcServer = (RTLOCALIPCSERVER)pvUser;
    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);

    RTTESTI_CHECK_RC_OK(RTThreadUserSignal(hSelf));

    int rc;
    for (;;)
    {
        RTLOCALIPCSESSION hIpcSession;
        rc = RTLocalIpcServerListen(hIpcServer, &hIpcSession);
        if (RT_SUCCESS(rc))
        {
            RTThreadSleep(8); /* windows output fudge (purely esthetical) */
            RTTestIPrintf(RTTESTLVL_INFO, "testServerListenThread: Got new client connection.\n");
            RTTESTI_CHECK_RC(RTLocalIpcSessionClose(hIpcSession), VINF_OBJECT_DESTROYED);
        }
        else
        {
            RTTESTI_CHECK_RC(rc, VERR_CANCELLED);
            break;
        }
    }
    return rc;
}


/**
 * Used both as a thread procedure and child process worker.
 */
static DECLCALLBACK(int) tstRTLocalIpcSessionConnectionChild(RTTHREAD hSelf, void *pvUser)
{
    RTLOCALIPCSESSION hClientSession;
    RT_NOREF_PV(hSelf); RT_NOREF_PV(pvUser);

    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);

    RTTEST_CHECK_RC_RET(g_hTest, RTLocalIpcSessionConnect(&hClientSession, "tstRTLocalIpcSessionConnection",0 /* Flags */),
                        VINF_SUCCESS, rcCheck);
    RTTEST_CHECK_RC_RET(g_hTest, RTLocalIpcSessionClose(hClientSession),
                        VINF_OBJECT_DESTROYED, rcCheck);

    return VINF_SUCCESS;
}


static void testSessionConnection(const char *pszExecPath)
{
    RTTestISub(!pszExecPath ? "Connect from thread" : "Connect from child");

    /*
     * Create the test server.
     */
    RTLOCALIPCSERVER hIpcServer;
    RTTESTI_CHECK_RC_RETV(RTLocalIpcServerCreate(&hIpcServer, "tstRTLocalIpcSessionConnection", 0), VINF_SUCCESS);

    /*
     * Create worker thread that listens and closes incoming connections until
     * cancelled.
     */
    int rc;
    RTTHREAD hListenThread;
    RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hListenThread, testServerListenThread, hIpcServer, 0 /* Stack */,
                                            RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "listen-1"));
    if (RT_SUCCESS(rc))
    {
        RTThreadUserWait(hListenThread, 32);

        /*
         * Two variations here: Client connects from thread or a child process.
         */
        if (pszExecPath)
        {
            RTPROCESS hClientProc;
            const char *apszArgs[4] = { pszExecPath, "child", "tstRTLocalIpcSessionConnectionChild", NULL };
            RTTESTI_CHECK_RC_OK(rc = RTProcCreate(pszExecPath, apszArgs, RTENV_DEFAULT, 0 /* fFlags*/, &hClientProc));
            if (RT_SUCCESS(rc))
            {
                RTPROCSTATUS ProcStatus;
                RTTESTI_CHECK_RC_OK(rc = RTProcWait(hClientProc, RTPROCWAIT_FLAGS_BLOCK, &ProcStatus));
                if (RT_SUCCESS(rc) && (ProcStatus.enmReason != RTPROCEXITREASON_NORMAL || ProcStatus.iStatus != 0))
                    RTTestIFailed("Chiled exited with enmReason=%d iStatus=%d", ProcStatus.enmReason, ProcStatus.iStatus);
            }
        }
        else
        {
            RTTHREAD hClientThread;
            RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hClientThread, tstRTLocalIpcSessionConnectionChild, NULL,
                                                    0 /* Stack */, RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "client-1"));
            if (RT_SUCCESS(rc))
            {
                int rcThread;
                RTTESTI_CHECK_RC_OK(rc = RTThreadWait(hClientThread, RT_MS_1MIN / 2, &rcThread));
                if (RT_SUCCESS(rc))
                    RTTESTI_CHECK_RC(rcThread, VINF_SUCCESS);
            }
        }


        /*
         * Terminate the server thread.
         */
        //RTTestIPrintf(RTTESTLVL_INFO, "Child terminated, waiting for server thread ...\n");
        RTTESTI_CHECK_RC(RTLocalIpcServerCancel(hIpcServer), VINF_SUCCESS);
        int rcThread;
        RTTESTI_CHECK_RC(rc = RTThreadWait(hListenThread, 30 * 1000 /* 30s timeout */, &rcThread), VINF_SUCCESS);
        if (RT_SUCCESS(rc))
            RTTESTI_CHECK_RC(rcThread, VERR_CANCELLED);
    }

    RTTESTI_CHECK_RC(RTLocalIpcServerDestroy(hIpcServer), VINF_OBJECT_DESTROYED);
}



/*********************************************************************************************************************************
*                                                                                                                                *
*   testSessionWait - RTLocalIpcSessionWaitForData.                                                                              *
*                                                                                                                                *
*********************************************************************************************************************************/

static DECLCALLBACK(int) testSessionWaitThread(RTTHREAD hSelf, void *pvUser)
{
    RTLOCALIPCSERVER hIpcServer = (RTLOCALIPCSERVER)pvUser;
    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);

    int rc;
    for (;;)
    {
        RTLOCALIPCSESSION hIpcSession;
        rc = RTLocalIpcServerListen(hIpcServer, &hIpcSession);
        if (RT_SUCCESS(rc))
        {
            RTTestIPrintf(RTTESTLVL_INFO, "testSessionWaitThread: Got new client connection.\n");

            /* Wait for the client to trigger a disconnect by writing us something. */
            RTTESTI_CHECK_RC(RTLocalIpcSessionWaitForData(hIpcSession, RT_MS_1MIN), VINF_SUCCESS);

            size_t cbRead;
            char szCmd[64];
            RT_ZERO(szCmd);
            RTTESTI_CHECK_RC(rc = RTLocalIpcSessionReadNB(hIpcSession, szCmd, sizeof(szCmd) - 1, &cbRead), VINF_SUCCESS);
            if (RT_SUCCESS(rc) && (cbRead != sizeof("disconnect") - 1 || strcmp(szCmd, "disconnect")) )
                RTTestIFailed("cbRead=%zu, expected %zu; szCmd='%s', expected 'disconnect'\n",
                              cbRead, sizeof("disconnect") - 1, szCmd);

            RTTESTI_CHECK_RC(RTLocalIpcSessionClose(hIpcSession), VINF_OBJECT_DESTROYED);
            RTTESTI_CHECK_RC_OK(RTThreadUserSignal(hSelf));
        }
        else
        {
            RTTESTI_CHECK_RC(rc, VERR_CANCELLED);
            break;
        }
    }
    RTTESTI_CHECK_RC_OK(RTThreadUserSignal(hSelf));
    return rc;
}


/**
 * Used both as a thread procedure and child process worker.
 */
static DECLCALLBACK(int) tstRTLocalIpcSessionWaitChild(RTTHREAD hSelf, void *pvUser)
{
    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);
    RT_NOREF_PV(hSelf); RT_NOREF_PV(pvUser);

    RTLOCALIPCSESSION hClientSession;
    RTTESTI_CHECK_RC_RET(RTLocalIpcSessionConnect(&hClientSession, "tstRTLocalIpcSessionWait", 0 /*fFlags*/),
                         VINF_SUCCESS, rcCheck);

    /*
     * The server side won't write anything.  It will close the connection
     * as soon as we write something.
     */
    RTTESTI_CHECK_RC(RTLocalIpcSessionWaitForData(hClientSession, 0 /*cMsTimeout*/), VERR_TIMEOUT);
    RTTESTI_CHECK_RC(RTLocalIpcSessionWaitForData(hClientSession, 8 /*cMsTimeout*/), VERR_TIMEOUT);
    uint8_t abBuf[4];
    size_t cbRead = _4M-1;
    RTTESTI_CHECK_RC(RTLocalIpcSessionReadNB(hClientSession, abBuf, sizeof(abBuf), &cbRead), VINF_TRY_AGAIN);
    RTTESTI_CHECK(cbRead == 0);

    /* Trigger server disconnect. */
    int rc;
    RTTESTI_CHECK_RC(rc = RTLocalIpcSessionWrite(hClientSession, RT_STR_TUPLE("disconnect")), VINF_SUCCESS);
    if (RT_SUCCESS(rc))
    {
        /*
         * When we wait now, we should get an broken pipe error as
         * the server has close its end.
         */
        RTTESTI_CHECK_RC(rc = RTLocalIpcSessionWaitForData(hClientSession, RT_MS_1MIN), VERR_BROKEN_PIPE);
        RTTESTI_CHECK_RC(RTLocalIpcSessionWaitForData(hClientSession, 0), VERR_BROKEN_PIPE);
        RTTESTI_CHECK_RC(RTLocalIpcSessionWaitForData(hClientSession, RT_MS_1SEC), VERR_BROKEN_PIPE);

        bool fMayPanic = RTAssertSetMayPanic(false);
        bool fQuiet    = RTAssertSetQuiet(true);

        RTTESTI_CHECK_RC(RTLocalIpcSessionWrite(hClientSession, RT_STR_TUPLE("broken")), VERR_BROKEN_PIPE);
        RTTESTI_CHECK_RC(RTLocalIpcSessionRead(hClientSession, abBuf, sizeof(abBuf), NULL), VERR_BROKEN_PIPE);
        cbRead = _4M-1;
        RTTESTI_CHECK_RC(RTLocalIpcSessionRead(hClientSession, abBuf, sizeof(abBuf), &cbRead), VERR_BROKEN_PIPE);
        cbRead = _1G/2;
        RTTESTI_CHECK_RC(RTLocalIpcSessionReadNB(hClientSession, abBuf, sizeof(abBuf), &cbRead), VERR_BROKEN_PIPE);

        RTAssertSetMayPanic(fMayPanic);
        RTAssertSetQuiet(fQuiet);
    }

    RTTESTI_CHECK_RC(RTLocalIpcSessionClose(hClientSession), VINF_OBJECT_DESTROYED);

    return VINF_SUCCESS;
}


/**
 * @note This is identical to testSessionData with a couple of string and
 *       function pointers replaced.
 */
static void testSessionWait(const char *pszExecPath)
{
    RTTestISub(!pszExecPath ? "Wait for data in thread" : "Wait for data in child");

    /*
     * Create the test server.
     */
    RTLOCALIPCSERVER hIpcServer;
    RTTESTI_CHECK_RC_RETV(RTLocalIpcServerCreate(&hIpcServer, "tstRTLocalIpcSessionWait", 0), VINF_SUCCESS);

    /*
     * Create worker thread that listens and processes incoming connections
     * until cancelled.
     */
    int rc;
    RTTHREAD hListenThread;
    RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hListenThread, testSessionWaitThread, hIpcServer, 0 /* Stack */,
                                            RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "listen-2"));
    if (RT_SUCCESS(rc))
    {
        /*
         * Create a client process or thread and connects to the server.
         * It will perform the wait-for-data test.
         */
        RTPROCESS hClientProc = NIL_RTPROCESS;
        RTTHREAD  hClientThread = NIL_RTTHREAD;
        if (pszExecPath)
        {
            const char *apszArgs[4] = { pszExecPath, "child", "tstRTLocalIpcSessionWaitChild", NULL };
            RTTESTI_CHECK_RC_OK(rc = RTProcCreate(pszExecPath, apszArgs, RTENV_DEFAULT, 0 /* fFlags*/, &hClientProc));
        }
        else
            RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hClientThread, tstRTLocalIpcSessionWaitChild, g_hTest, 0 /*cbStack*/,
                                                    RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "client-2"));

        /*
         * Wait for the server thread to indicate that it has processed one
         * connection, then shut it all down.
         */
        if (RT_SUCCESS(rc))
            RTTESTI_CHECK_RC_OK(RTThreadUserWait(hListenThread, RT_MS_1MIN / 2));

        RTTESTI_CHECK_RC(RTLocalIpcServerCancel(hIpcServer), VINF_SUCCESS);
        int rcThread;
        RTTESTI_CHECK_RC(rc = RTThreadWait(hListenThread, RT_MS_1MIN / 2, &rcThread), VINF_SUCCESS);
        if (RT_SUCCESS(rc))
            RTTESTI_CHECK_RC(rcThread, VERR_CANCELLED);

        RTTESTI_CHECK_RC(RTLocalIpcServerDestroy(hIpcServer), VINF_OBJECT_DESTROYED);

        /*
         * Check that client ran successfully.
         */
        if (pszExecPath)
        {
            if (hClientProc != NIL_RTPROCESS)
            {
                RTPROCSTATUS ProcStatus;
                RTTESTI_CHECK_RC_OK(rc = RTProcWait(hClientProc, RTPROCWAIT_FLAGS_BLOCK, &ProcStatus));
                if (RT_SUCCESS(rc) && (ProcStatus.enmReason != RTPROCEXITREASON_NORMAL || ProcStatus.iStatus != 0))
                    RTTestIFailed("Chiled exited with enmReason=%d iStatus=%d", ProcStatus.enmReason, ProcStatus.iStatus);
            }
        }
        else if (hClientThread != NIL_RTTHREAD)
        {
            RTTESTI_CHECK_RC_OK(rc = RTThreadWait(hClientThread, RT_MS_1MIN / 2, &rcThread));
            if (RT_SUCCESS(rc))
                RTTESTI_CHECK_RC(rcThread, VINF_SUCCESS);
        }
    }
}



/*********************************************************************************************************************************
*                                                                                                                                *
*   testSessionData - Data transfer integrity.                                                                                   *
*                                                                                                                                *
*********************************************************************************************************************************/

/** The max message size. */
#define MAX_DATA_MSG_SIZE   _1M

static int testSessionDataReadMessages(RTLOCALIPCSESSION hIpcSession, uint32_t cRounds)
{
    /*
     * Message scratch buffer.  Search message starts with a uint32_t word
     * that indicates the message length.  The remaining words are set to
     * the message number.
     */
    uint32_t *pau32ScratchBuf = (uint32_t *)RTMemAlloc(MAX_DATA_MSG_SIZE);
    RTTESTI_CHECK_RET(pau32ScratchBuf != NULL, VERR_NO_MEMORY);

    int rc = VINF_SUCCESS;
    for (uint32_t iRound = 0; iRound < cRounds && rc == VINF_SUCCESS; iRound++)
    {
        /* Read the message length. */
        uint32_t cbMsg;
        RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionRead(hIpcSession, &cbMsg, sizeof(cbMsg), NULL), VINF_SUCCESS);
        if (cbMsg >= sizeof(cbMsg) && cbMsg <= MAX_DATA_MSG_SIZE)
        {
            pau32ScratchBuf[0] = cbMsg;

            /* Read the message body. */
            uint32_t cbLeft = cbMsg - sizeof(uint32_t);
            uint8_t *pbCur  = (uint8_t *)&pau32ScratchBuf[1];
            while (cbLeft > 0)
            {
                uint32_t cbCur = RTRandU32Ex(1, cbLeft + cbLeft / 4);
                cbCur = RT_MIN(cbCur, cbLeft);
                if ((iRound % 3) == 1)
                {
                    size_t cbRead = _1G;
                    RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionRead(hIpcSession, pbCur, cbCur, &cbRead), VINF_SUCCESS);
                    RTTESTI_CHECK(cbCur >= cbRead);
                    cbCur = (uint32_t)cbRead;
                }
                else
                    RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionRead(hIpcSession, pbCur, cbCur, NULL), VINF_SUCCESS);
                pbCur  += cbCur;
                cbLeft -= cbCur;
            }

            /* Check the message body. */
            if (RT_SUCCESS(rc))
            {
                uint32_t offLast = cbMsg & (sizeof(uint32_t) - 1);
                if (offLast)
                    memcpy((uint8_t *)pau32ScratchBuf + cbMsg, (uint8_t const *)&iRound + offLast, sizeof(uint32_t) - offLast);

                ASMCompilerBarrier(); /* Guard against theoretical alias issues in the above code.  */

                uint32_t cWords = RT_ALIGN_32(cbMsg, sizeof(uint32_t)) / sizeof(uint32_t);
                for (uint32_t iWord = 1; iWord < cWords; iWord++)
                    if (pau32ScratchBuf[iWord] != iRound)
                    {
                        RTTestIFailed("Message body word #%u mismatch: %#x, expected %#x", iWord, pau32ScratchBuf[iWord], iRound);
                        break;
                    }
            }
        }
        else
        {
            RTTestIFailed("cbMsg=%#x is out of range", cbMsg);
            rc = VERR_OUT_OF_RANGE;
        }
    }

    RTMemFree(pau32ScratchBuf);
    return rc;
}


static int testSessionDataWriteMessages(RTLOCALIPCSESSION hIpcSession, uint32_t cRounds)
{
    /*
     * Message scratch buffer.  Search message starts with a uint32_t word
     * that indicates the message length.  The remaining words are set to
     * the message number.
     */
    uint32_t   cbScratchBuf = RTRandU32Ex(64, MAX_DATA_MSG_SIZE);
    cbScratchBuf = RT_ALIGN_32(cbScratchBuf, sizeof(uint32_t));

    uint32_t *pau32ScratchBuf = (uint32_t *)RTMemAlloc(cbScratchBuf);
    RTTESTI_CHECK_RET(pau32ScratchBuf != NULL, VERR_NO_MEMORY);

    size_t cbSent = 0;
    int rc = VINF_SUCCESS;
    for (uint32_t iRound = 0; iRound < cRounds && rc == VINF_SUCCESS; iRound++)
    {
        /* Construct the message. */
        uint32_t cbMsg  = RTRandU32Ex(sizeof(uint32_t), cbScratchBuf);
        uint32_t cWords = RT_ALIGN_32(cbMsg, sizeof(uint32_t)) / sizeof(uint32_t);

        uint32_t iWord  = 0;
        pau32ScratchBuf[iWord++] = cbMsg;
        while (iWord < cWords)
            pau32ScratchBuf[iWord++] = iRound;

        /* Send it. */
        uint32_t cbLeft = cbMsg;
        uint8_t const *pbCur = (uint8_t *)pau32ScratchBuf;
        while (cbLeft > 0)
        {
            uint32_t cbCur = RT_MIN(iRound + 1, cbLeft);
            RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionWrite(hIpcSession, pbCur, cbCur), VINF_SUCCESS);
            pbCur  += cbCur;
            cbSent += cbCur;
            cbLeft -= cbCur;
        }
    }

    RTTestIPrintf(RTTESTLVL_ALWAYS, "Sent %'zu bytes over %u rounds.\n", cbSent, cRounds);
    RTMemFree(pau32ScratchBuf);
    return rc;
}


static DECLCALLBACK(int) testSessionDataThread(RTTHREAD hSelf, void *pvUser)
{
    RTLOCALIPCSERVER hIpcServer = (RTLOCALIPCSERVER)pvUser;
    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);

    int rc;
    for (;;)
    {
        RTLOCALIPCSESSION hIpcSession;
        rc = RTLocalIpcServerListen(hIpcServer, &hIpcSession);
        if (RT_SUCCESS(rc))
        {
            RTTestIPrintf(RTTESTLVL_INFO, "testSessionDataThread: Got new client connection\n");

            /* The server is the initator. First message sets the number of rounds. */
            uint32_t cRounds = RTRandU32Ex(32, _1K);
            RTTESTI_CHECK_RC(rc = RTLocalIpcSessionWrite(hIpcSession, &cRounds, sizeof(cRounds)), VINF_SUCCESS);
            if (RT_SUCCESS(rc))
            {
                rc = testSessionDataWriteMessages(hIpcSession, cRounds);
                if (RT_SUCCESS(rc))
                    rc = testSessionDataReadMessages(hIpcSession, cRounds);
            }

            RTTESTI_CHECK_RC(RTLocalIpcSessionClose(hIpcSession), VINF_OBJECT_DESTROYED);
            RTTESTI_CHECK_RC_OK(RTThreadUserSignal(hSelf));
        }
        else
        {
            RTTESTI_CHECK_RC(rc, VERR_CANCELLED);
            break;
        }
    }
    RTTESTI_CHECK_RC_OK(RTThreadUserSignal(hSelf));
    return rc;
}


/**
 * Used both as a thread procedure and child process worker.
 */
static DECLCALLBACK(int) tstRTLocalIpcSessionDataChild(RTTHREAD hSelf, void *pvUser)
{
    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);
    RT_NOREF_PV(hSelf); RT_NOREF_PV(pvUser);

    /*
     * Connect.
     */
    RTLOCALIPCSESSION hClientSession;
    RTTESTI_CHECK_RC_RET(RTLocalIpcSessionConnect(&hClientSession, "tstRTLocalIpcSessionData", 0 /*fFlags*/),
                         VINF_SUCCESS, rcCheck);

    /*
     * The server first sends us a rounds count.
     */
    int rc;
    uint32_t cRounds = 0;
    RTTESTI_CHECK_RC(rc = RTLocalIpcSessionRead(hClientSession, &cRounds, sizeof(cRounds), NULL), VINF_SUCCESS);
    if (RT_SUCCESS(rc))
    {
        if (cRounds >= 32 && cRounds <= _1K)
        {
            rc = testSessionDataReadMessages(hClientSession, cRounds);
            if (RT_SUCCESS(rc))
                rc = testSessionDataWriteMessages(hClientSession, cRounds);
        }
        else
            RTTestIFailed("cRounds=%#x is out of range", cRounds);
    }

    RTTESTI_CHECK_RC(RTLocalIpcSessionClose(hClientSession), VINF_OBJECT_DESTROYED);

    return rc;
}


/**
 * @note This is identical to testSessionWait with a couple of strings, function
 *       pointers, and timeouts replaced.
 */
static void testSessionData(const char *pszExecPath)
{
    RTTestISub(!pszExecPath ? "Data exchange with thread" : "Data exchange with child");

    /*
     * Create the test server.
     */
    RTLOCALIPCSERVER hIpcServer;
    RTTESTI_CHECK_RC_RETV(RTLocalIpcServerCreate(&hIpcServer, "tstRTLocalIpcSessionData", 0), VINF_SUCCESS);

    /*
     * Create worker thread that listens and processes incoming connections
     * until cancelled.
     */
    int rc;
    RTTHREAD hListenThread;
    RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hListenThread, testSessionDataThread, hIpcServer, 0 /* Stack */,
                                            RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "listen-3"));
    if (RT_SUCCESS(rc))
    {
        /*
         * Create a client thread or process.
         */
        RTPROCESS hClientProc   = NIL_RTPROCESS;
        RTTHREAD  hClientThread = NIL_RTTHREAD;
        if (pszExecPath)
        {
            const char *apszArgs[4] = { pszExecPath, "child", "tstRTLocalIpcSessionDataChild", NULL };
            RTTESTI_CHECK_RC_OK(rc = RTProcCreate(pszExecPath, apszArgs, RTENV_DEFAULT, 0 /* fFlags*/, &hClientProc));
        }
        else
            RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hClientThread, tstRTLocalIpcSessionDataChild, g_hTest, 0 /*cbStack*/,
                                                    RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "client-2"));

        /*
         * Wait for the server thread to indicate that it has processed one
         * connection, then shut it all down.
         */
        if (RT_SUCCESS(rc))
            RTTESTI_CHECK_RC_OK(RTThreadUserWait(hListenThread, RT_MS_1MIN * 3));

        RTTESTI_CHECK_RC(RTLocalIpcServerCancel(hIpcServer), VINF_SUCCESS);
        int rcThread;
        RTTESTI_CHECK_RC(rc = RTThreadWait(hListenThread, RT_MS_1MIN / 2, &rcThread), VINF_SUCCESS);
        if (RT_SUCCESS(rc))
            RTTESTI_CHECK_RC(rcThread, VERR_CANCELLED);

        RTTESTI_CHECK_RC(RTLocalIpcServerDestroy(hIpcServer), VINF_OBJECT_DESTROYED);

        /*
         * Check that client ran successfully.
         */
        if (pszExecPath)
        {
            if (hClientProc != NIL_RTPROCESS)
            {
                RTPROCSTATUS ProcStatus;
                RTTESTI_CHECK_RC_OK(rc = RTProcWait(hClientProc, RTPROCWAIT_FLAGS_BLOCK, &ProcStatus));
                if (RT_SUCCESS(rc) && (ProcStatus.enmReason != RTPROCEXITREASON_NORMAL || ProcStatus.iStatus != 0))
                    RTTestIFailed("Chiled exited with enmReason=%d iStatus=%d", ProcStatus.enmReason, ProcStatus.iStatus);
            }
        }
        else if (hClientThread != NIL_RTTHREAD)
        {
            RTTESTI_CHECK_RC_OK(rc = RTThreadWait(hClientThread, RT_MS_1MIN / 2, &rcThread));
            if (RT_SUCCESS(rc))
                RTTESTI_CHECK_RC(rcThread, VINF_SUCCESS);
        }
    }
}


/*********************************************************************************************************************************
*                                                                                                                                *
*   testSessionPerf - Performance measurements.                                                                                  *
*                                                                                                                                *
*********************************************************************************************************************************/

#define IPC_PERF_LAST_MSG   UINT32_C(0x7fffeeee)
#define IPC_PERF_MSG_REPLY(uMsg)    ((uMsg) | RT_BIT_32(31))


static DECLCALLBACK(int) testSessionPerfThread(RTTHREAD hSelf, void *pvUser)
{
    RTLOCALIPCSERVER hIpcServer = (RTLOCALIPCSERVER)pvUser;
    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);

    int rc;
    for (;;)
    {
        RTLOCALIPCSESSION hIpcSession;
        rc = RTLocalIpcServerListen(hIpcServer, &hIpcSession);
        if (RT_SUCCESS(rc))
        {
            RTTestIPrintf(RTTESTLVL_INFO, "testSessionPerfThread: Got new client connection\n");

            /* The server is the initator, so we start sending messages. */
            uint64_t cNsElapsed = _4G;
            uint64_t nsStart    = RTTimeNanoTS();
            uint32_t cMessages  = 0;
            for (;; )
            {
                uint32_t uMsg = cMessages;
                RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionWrite(hIpcSession, &uMsg, sizeof(uMsg)), VINF_SUCCESS);
                uMsg = UINT32_MAX;
                RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionRead(hIpcSession, &uMsg, sizeof(uMsg), NULL), VINF_SUCCESS);
                if (uMsg == IPC_PERF_MSG_REPLY(cMessages))
                { /* likely */ }
                else
                {
                    RTTestIFailed("uMsg=%#x expected %#x", uMsg, IPC_PERF_MSG_REPLY(cMessages));
                    rc = VERR_OUT_OF_RANGE;
                    break;
                }

                /* next */
                cMessages++;
                if (cMessages & _16K)
                { /* likely */ }
                else
                {
                    cNsElapsed = RTTimeNanoTS() - nsStart;
                    if (cNsElapsed > 2*RT_NS_1SEC_64)
                    {
                        uMsg = IPC_PERF_LAST_MSG;
                        RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionWrite(hIpcSession, &uMsg, sizeof(uMsg)), VINF_SUCCESS);
                        break;
                    }
                }
            }
            if (RT_SUCCESS(rc))
            {
                RTThreadSleep(8); /* windows output fudge (purely esthetical) */
                RTTestIValue("roundtrip", cNsElapsed / cMessages, RTTESTUNIT_NS_PER_ROUND_TRIP);
                RTTestIValue("roundtrips", RT_NS_1SEC / (cNsElapsed / cMessages), RTTESTUNIT_OCCURRENCES_PER_SEC);
            }

            RTTESTI_CHECK_RC(RTLocalIpcSessionClose(hIpcSession), VINF_OBJECT_DESTROYED);
            RTTESTI_CHECK_RC_OK(RTThreadUserSignal(hSelf));
        }
        else
        {
            RTTESTI_CHECK_RC(rc, VERR_CANCELLED);
            break;
        }
    }
    RTTESTI_CHECK_RC_OK(RTThreadUserSignal(hSelf));
    return rc;
}


/**
 * Used both as a thread procedure and child process worker.
 */
static DECLCALLBACK(int) tstRTLocalIpcSessionPerfChild(RTTHREAD hSelf, void *pvUser)
{
    RTTEST_CHECK_RC_OK_RET(g_hTest, RTTestSetDefault(g_hTest, NULL), rcCheck);
    RT_NOREF_PV(hSelf); RT_NOREF_PV(pvUser);

    /*
     * Connect.
     */
    RTLOCALIPCSESSION hClientSession;
    RTTESTI_CHECK_RC_RET(RTLocalIpcSessionConnect(&hClientSession, "tstRTLocalIpcSessionPerf", 0 /*fFlags*/),
                         VINF_SUCCESS, rcCheck);

    /*
     * Process messages.  Server does all the timing and stuff.
     */
    int rc = VINF_SUCCESS;
    for (uint32_t cMessages = 0; ; cMessages++)
    {
        /* Read the next message from the server. */
        uint32_t uMsg = UINT32_MAX;
        RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionRead(hClientSession, &uMsg, sizeof(uMsg), NULL), VINF_SUCCESS);
        if (uMsg == cMessages)
        {
            uMsg = IPC_PERF_MSG_REPLY(uMsg);
            RTTESTI_CHECK_RC_BREAK(rc = RTLocalIpcSessionWrite(hClientSession, &uMsg, sizeof(uMsg)), VINF_SUCCESS);
        }
        else if (uMsg == IPC_PERF_LAST_MSG)
            break;
        else
        {
            RTTestIFailed("uMsg=%#x expected %#x", uMsg, cMessages);
            rc = VERR_OUT_OF_RANGE;
            break;
        }
    }

    RTTESTI_CHECK_RC(RTLocalIpcSessionClose(hClientSession), VINF_OBJECT_DESTROYED);
    return rc;
}


/**
 * @note This is identical to testSessionWait with a couple of string and
 *       function pointers replaced.
 */
static void testSessionPerf(const char *pszExecPath)
{
    RTTestISub(!pszExecPath ? "Thread performance" : "Child performance");

    /*
     * Create the test server.
     */
    RTLOCALIPCSERVER hIpcServer;
    RTTESTI_CHECK_RC_RETV(RTLocalIpcServerCreate(&hIpcServer, "tstRTLocalIpcSessionPerf", 0), VINF_SUCCESS);

    /*
     * Create worker thread that listens and processes incoming connections
     * until cancelled.
     */
    int rc;
    RTTHREAD hListenThread;
    RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hListenThread, testSessionPerfThread, hIpcServer, 0 /* Stack */,
                                            RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "listen-3"));
    if (RT_SUCCESS(rc))
    {
        /*
         * Create a client thread or process.
         */
        RTPROCESS hClientProc   = NIL_RTPROCESS;
        RTTHREAD  hClientThread = NIL_RTTHREAD;
        if (pszExecPath)
        {
            const char *apszArgs[4] = { pszExecPath, "child", "tstRTLocalIpcSessionPerfChild", NULL };
            RTTESTI_CHECK_RC_OK(rc = RTProcCreate(pszExecPath, apszArgs, RTENV_DEFAULT, 0 /* fFlags*/, &hClientProc));
        }
        else
            RTTESTI_CHECK_RC_OK(rc = RTThreadCreate(&hClientThread, tstRTLocalIpcSessionPerfChild, g_hTest, 0 /*cbStack*/,
                                                    RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "client-2"));

        /*
         * Wait for the server thread to indicate that it has processed one
         * connection, then shut it all down.
         */
        if (RT_SUCCESS(rc))
            RTTESTI_CHECK_RC_OK(RTThreadUserWait(hListenThread, RT_MS_1MIN / 2));

        RTTESTI_CHECK_RC(RTLocalIpcServerCancel(hIpcServer), VINF_SUCCESS);
        int rcThread;
        RTTESTI_CHECK_RC(rc = RTThreadWait(hListenThread, RT_MS_1MIN / 2, &rcThread), VINF_SUCCESS);
        if (RT_SUCCESS(rc))
            RTTESTI_CHECK_RC(rcThread, VERR_CANCELLED);

        RTTESTI_CHECK_RC(RTLocalIpcServerDestroy(hIpcServer), VINF_OBJECT_DESTROYED);

        /*
         * Check that client ran successfully.
         */
        if (pszExecPath)
        {
            if (hClientProc != NIL_RTPROCESS)
            {
                RTPROCSTATUS ProcStatus;
                RTTESTI_CHECK_RC_OK(rc = RTProcWait(hClientProc, RTPROCWAIT_FLAGS_BLOCK, &ProcStatus));
                if (RT_SUCCESS(rc) && (ProcStatus.enmReason != RTPROCEXITREASON_NORMAL || ProcStatus.iStatus != 0))
                    RTTestIFailed("Chiled exited with enmReason=%d iStatus=%d", ProcStatus.enmReason, ProcStatus.iStatus);
            }
        }
        else if (hClientThread != NIL_RTTHREAD)
        {
            RTTESTI_CHECK_RC_OK(rc = RTThreadWait(hClientThread, RT_MS_1MIN / 2, &rcThread));
            if (RT_SUCCESS(rc))
                RTTESTI_CHECK_RC(rcThread, VINF_SUCCESS);
        }
    }
}


int main(int argc, char **argv)
{
    int rc = RTR3InitExe(argc, &argv, 0);
    if (RT_FAILURE(rc))
        return RTMsgInitFailure(rc);

    /*
     * Main process.
     */
    if (argc == 1)
    {
        rc = RTTestCreate("tstRTLocalIpc", &g_hTest);
        if (RT_FAILURE(rc))
            return RTEXITCODE_FAILURE;
        RTTestBanner(g_hTest);

        /* Basics first. */
        bool fMayPanic = RTAssertSetMayPanic(false);
        bool fQuiet    = RTAssertSetQuiet(true);
        testBasics();
        RTAssertSetMayPanic(fMayPanic);
        RTAssertSetQuiet(fQuiet);

        /* Do real tests if the basics are fine. */
        char szExecPath[RTPATH_MAX];
        if (RTProcGetExecutablePath(szExecPath, sizeof(szExecPath)))
        {
            if (RTTestErrorCount(g_hTest) == 0)
                testSessionConnection(NULL);
            if (RTTestErrorCount(g_hTest) == 0)
                testSessionConnection(szExecPath);

            if (RTTestErrorCount(g_hTest) == 0)
                testSessionWait(NULL);
            if (RTTestErrorCount(g_hTest) == 0)
                testSessionWait(szExecPath);

            if (RTTestErrorCount(g_hTest) == 0)
                testSessionData(NULL);
            if (RTTestErrorCount(g_hTest) == 0)
                testSessionData(szExecPath);

            if (RTTestErrorCount(g_hTest) == 0)
                testSessionPerf(NULL);
            if (RTTestErrorCount(g_hTest) == 0)
                testSessionPerf(szExecPath);
        }
        else
            RTTestIFailed("RTProcGetExecutablePath failed");
    }
    /*
     * Child process.
     */
    else if (   argc == 3
             && !strcmp(argv[1], "child"))
    {
        rc = RTTestCreateChild(argv[2], &g_hTest);
        if (RT_FAILURE(rc))
            return RTEXITCODE_FAILURE;

        if (!strcmp(argv[2], "tstRTLocalIpcSessionConnectionChild"))
            tstRTLocalIpcSessionConnectionChild(RTThreadSelf(), g_hTest);
        else if (!strcmp(argv[2], "tstRTLocalIpcSessionWaitChild"))
            tstRTLocalIpcSessionWaitChild(RTThreadSelf(), g_hTest);
        else if (!strcmp(argv[2], "tstRTLocalIpcSessionDataChild"))
            tstRTLocalIpcSessionDataChild(RTThreadSelf(), g_hTest);
        else if (!strcmp(argv[2], "tstRTLocalIpcSessionPerfChild"))
            tstRTLocalIpcSessionPerfChild(RTThreadSelf(), g_hTest);
        else
            RTTestIFailed("Unknown child function '%s'", argv[2]);
    }
    /*
     * Invalid parameters.
     */
    else
        return RTEXITCODE_SYNTAX;

    /*
     * Summary.
     */
    return RTTestSummaryAndDestroy(g_hTest);
}

