Kawe Mazidjatari b3a68ed095 Add EABase, EAThread and DirtySDK to R5sdk
DirtySDK (EA's Dirty Sockets library) will be used for the LiveAPI implementation, and depends on: EABase, EAThread.
2024-04-05 18:29:03 +02:00

607 lines
20 KiB
C

/*H********************************************************************************/
/*!
\File testercomm_socket.c
\Description
This module provides a communication layer between the host and the client
using socket I/O. Typical operations are SendLine() and GetLine(), which
send and receive lines of text, commands, debug output, etc. Each platform
may implement its own way of communicating - through files, debugger API
calls, etc.
\Copyright
Copyright 2011 Electronic Arts Inc.
\Version 10/17/2011 (jbrookes) First Version
*/
/********************************************************************************H*/
/*** Include files ****************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "DirtySDK/dirtysock.h"
#include "DirtySDK/dirtysock/dirtylib.h"
#include "DirtySDK/dirtysock/netconn.h" // for NetConnSleep()
#include "DirtySDK/util/base64.h"
#include "DirtySDK/util/jsonformat.h"
#include "DirtySDK/util/jsonparse.h"
#include "libsample/zmem.h"
#include "libsample/zlib.h"
#include "libsample/zlist.h"
#include "libsample/zfile.h"
#include "testerprofile.h"
#include "testerregistry.h"
#include "testercomm.h"
/*** Defines **********************************************************************/
#define T2COMM_PORT (3232) //!< communications port
#define TESTERCOMM_OUTPUTTIMEOUT_DEFAULT (60*1000) //!< timeout (ms) for a output message
/*** Type Definitions *************************************************************/
//! module state
typedef struct TesterCommSocketT
{
SocketT *pSocket; //!< comm socket
SocketT *pListen; //!< listen socket used by host
int32_t iPort; //!< port to bind to
char strHostName[TESTERPROFILE_HOSTNAME_SIZEDEFAULT]; //!< host address to connect to
enum
{
ST_INIT,
ST_CONN,
ST_LIST,
ST_OPEN,
ST_FAIL
}eState; //!< module state
uint8_t bIsHost; //!< TRUE if host else FALSE
char strInputData[16*1024]; //!< input buffer
} TesterCommSocketT;
/*** Private Function Prototypes **************************************************/
static int32_t _TesterCommDisconnect(TesterCommT *pState);
static int32_t _TesterCommCheckInput(TesterCommT *pState);
/*** Variables ********************************************************************/
/*** Private Functions ************************************************************/
/*F********************************************************************************/
/*!
\Function _TesterCommConnect2
\Description
Connect the host client communication module.
\Input *pSocketState - module state
\Output
int32_t - 0=success, else failure
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommConnect2(TesterCommSocketT *pSocketState)
{
struct sockaddr SockAddr;
uint32_t uHostAddr;
int32_t iResult;
// set up sockaddr
SockaddrInit(&SockAddr, AF_INET);
SockaddrInSetPort(&SockAddr, pSocketState->iPort);
// if we have somewhere to connect to, do it
if (pSocketState->strHostName[0] != '\0')
{
// create the socket
if ((pSocketState->pSocket = SocketOpen(AF_INET, SOCK_STREAM, 0)) == NULL)
{
ZPrintf("testercomm_socket: cannot create socket\n");
pSocketState->eState = ST_FAIL;
return(-1);
}
// check for invalid addr
if ((uHostAddr = SocketInTextGetAddr(pSocketState->strHostName)) == 0)
{
ZPrintf("testercomm_socket: connect got invalid host address %s\n", pSocketState->strHostName);
pSocketState->eState = ST_FAIL;
return(-2);
}
// initiate connection
SockaddrInSetAddr(&SockAddr, uHostAddr);
if ((iResult = SocketConnect(pSocketState->pSocket, &SockAddr, sizeof(SockAddr))) != SOCKERR_NONE)
{
ZPrintf("testercomm_socket: error %d initiating connection to %s\n", iResult, pSocketState->strHostName);
SocketClose(pSocketState->pSocket);
pSocketState->pSocket = NULL;
pSocketState->eState = ST_FAIL;
return(-3);
}
pSocketState->eState = ST_CONN;
ZPrintf("testercomm_socket: connect hostname=%a\n", uHostAddr);
}
else
{
struct sockaddr BindAddr;
/* only create the listen socket the first time, if the peer disconnects we will
just go back into the listen state to accept the new connection */
if (pSocketState->pListen == NULL)
{
// create the socket
if ((pSocketState->pListen = SocketOpen(AF_INET, SOCK_STREAM, 0)) == NULL)
{
ZPrintf("testercomm_socket: cannot create socket\n");
pSocketState->eState = ST_FAIL;
return(-4);
}
// set SO_LINGER to zero on listen socket so it doesn't linger and cause errors in testing
SocketControl(pSocketState->pListen, 'soli', 0, NULL, NULL);
// set to reuse addr incase it is still bound from previous session
SocketControl(pSocketState->pListen, 'radr', 1, NULL, NULL);
// bind the socket
if ((iResult = SocketBind(pSocketState->pListen, &SockAddr, sizeof(SockAddr))) != SOCKERR_NONE)
{
ZPrintf("testercomm_socket: error %d binding to port %d\n", iResult, pSocketState->iPort);
SocketClose(pSocketState->pListen);
pSocketState->pListen = NULL;
pSocketState->eState = ST_FAIL;
ZPrintf("testercomm_socket: next bind will use a random port\n");
pSocketState->iPort = 0;
return(-5);
}
// listen on the socket
if ((iResult = SocketListen(pSocketState->pListen, 2)) != SOCKERR_NONE)
{
ZPrintf("testercomm_socket: error %d listening on socket\n", iResult);
SocketClose(pSocketState->pListen);
pSocketState->pListen = NULL;
pSocketState->eState = ST_FAIL;
return(-6);
}
}
pSocketState->eState = ST_LIST;
SocketInfo(pSocketState->pListen, 'bind', 0, &BindAddr, sizeof(BindAddr));
ZPrintf("testercomm_socket: waiting for connection on port %d\n", SockaddrInGetPort(&BindAddr));
}
// done for now
return(0);
}
/*F********************************************************************************/
/*!
\Function _TesterCommDisconnect2
\Description
Disconnect the host client communication module.
\Input *pSocketState - pointer to host client comm module
\Output
int32_t - 0=success, error code otherwise
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommDisconnect2(TesterCommSocketT *pSocketState)
{
ZPrintf("testercomm_socket: disconnect\n");
// dispose of socket(s)
if (pSocketState->pSocket != NULL)
{
SocketClose(pSocketState->pSocket);
pSocketState->pSocket = NULL;
}
// return success
return(0);
}
/*F********************************************************************************/
/*!
\Function _TesterCommCheckConnect
\Description
Check to see if a connection has been established
\Input *pState - module state
\Output
int32_t - 1=open, 0=connecting, negative=error
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommCheckConnect(TesterCommT *pState)
{
TesterCommSocketT *pSocketState = pState->pInterface->pData;
// if already open nothing to do
if (pSocketState->eState == ST_OPEN)
{
return(1);
}
if (pSocketState->eState == ST_LIST)
{
struct sockaddr RequestAddr;
int32_t iAddrLen = sizeof(RequestAddr);
// accept incoming connection
SockaddrInit(&RequestAddr, AF_INET);
if ((pSocketState->pSocket = SocketAccept(pSocketState->pListen, &RequestAddr, &iAddrLen)) != NULL)
{
ZPrintf("testercomm_socket: accepted incoming connection request from %A\n", &RequestAddr);
pSocketState->eState = ST_OPEN;
}
}
else if (pSocketState->eState == ST_CONN)
{
int32_t iResult;
// waiting for connection to be complete
if ((iResult = SocketInfo(pSocketState->pSocket, 'stat', 0, NULL, 0)) > 0)
{
ZPrintf("testercomm_socket: connection complete\n");
pSocketState->eState = ST_OPEN;
}
else if (iResult < 0)
{
pSocketState->eState = ST_FAIL;
}
}
else if (pSocketState->eState == ST_FAIL)
{
_TesterCommDisconnect2(pSocketState);
// dump all the messages
ZListClear(pState->pOutputData);
// set that we haven't received any input
pState->bGotInput = FALSE;
_TesterCommConnect2(pSocketState);
}
if (pSocketState->eState == ST_OPEN)
{
// set the tick time so we don't automatically get a timeout
pState->uLastSendTime = ZTick();
return(1);
}
return(0);
}
/*F********************************************************************************/
/*!
\Function _TesterCommCheckInput
\Description
Check for data coming from the other side (host or client) and pull
any data into our internal buffer, if possible.
\Input *pState - pointer to host client comm module
\Output
int32_t - 0 = no data, >0 = data, error code otherwise
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommCheckInput(TesterCommT *pState)
{
TesterCommSocketT *pSocketState = pState->pInterface->pData;
int32_t iBytesRecv = 0, iLinesRecv, iResult;
const char *pInputLoop;
uint16_t *pJson;
// don't do anything if we have already closed the socket
if (pSocketState->eState != ST_OPEN)
{
return(-1);
}
// try to get as much data as possible
while ((iResult = SocketRecv(pSocketState->pSocket, pSocketState->strInputData+iBytesRecv, sizeof(pSocketState->strInputData)-iBytesRecv, 0)) != 0)
{
if (iResult > 0)
{
iBytesRecv += iResult;
pState->bGotInput = TRUE;
}
else if (iResult < 0)
{
// an error occurred
ZPrintf("testercomm_socket: error %d receiving data\n", iResult);
pSocketState->eState = ST_FAIL;
return(-1);
}
}
if (iBytesRecv == 0)
{
return(0);
}
pJson = JsonParse2(pSocketState->strInputData, -1, 0, 0, NULL);
// push each line onto the list
iLinesRecv = 0;
while ((pInputLoop = JsonFind2(pJson, NULL, "msg[", iLinesRecv)) != NULL)
{
char strBuffer[sizeof(pState->LineData.strBuffer)];
int32_t iBufLen;
ds_memclr(&pState->LineData, sizeof(pState->LineData));
pState->LineData.iType = (int32_t)JsonGetInteger(JsonFind2(pJson, pInputLoop, ".TYPE", iLinesRecv), 0);
iBufLen = JsonGetString(JsonFind2(pJson, pInputLoop, ".TEXT", iLinesRecv), strBuffer, sizeof(strBuffer), "");
Base64Decode3(strBuffer, iBufLen, pState->LineData.strBuffer, sizeof(pState->LineData.strBuffer));
// add to the back of the list; remember to add one for the terminator
ZListPushBack(pState->pInputData, &pState->LineData);
iLinesRecv += 1;
}
ZMemFree(pJson);
// done - return how much we got from the file
return(iLinesRecv);
}
/*F********************************************************************************/
/*!
\Function _TesterCommCheckOutput
\Description
Check and send data from the output buffer, if possible.
\Input *pState - pointer to host client comm module
\Output
int32_t - 0=success, error code otherwise
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommCheckOutput(TesterCommT *pState)
{
TesterCommSocketT *pSocketState = pState->pInterface->pData;
int32_t iLineLength, iBytesSent, iResult, iTickDiff;
char *pWrite, *pBuffer;
// see if there's any data to send
if (ZListPeekFront(pState->pOutputData) == NULL)
{
// reset the last send time - we're not timing out because there's no data
pState->uLastSendTime = ZTick();
return(0);
}
// see if we've timed out
if ((iTickDiff = NetTickDiff(ZTick(), pState->uLastSendTime)) > TESTERCOMM_OUTPUTTIMEOUT_DEFAULT)
{
// timeout occurred - dump all pending messages and pop a warning
ZListClear(pState->pOutputData);
ZPrintf("testercomm_socket: TIMEOUT in sending data (%dms). List cleared. Comms Lost?\n", iTickDiff);
pSocketState->eState = ST_FAIL;
}
// push output queue stuff by sending commands
JsonInit(pState->strCommand, sizeof(pState->strCommand), 0);
JsonArrayStart(pState->strCommand, "msg");
while (ZListPeekFront(pState->pOutputData) != NULL)
{
char strBuffer[sizeof(pState->LineData.strBuffer)];
// snag the data into the local buffer
ZListPopFront(pState->pOutputData, &pState->LineData);
// create the stuff to write
JsonObjectStart(pState->strCommand, NULL);
JsonAddInt(pState->strCommand, "TYPE", pState->LineData.iType);
Base64Encode2(pState->LineData.strBuffer, (int32_t)strlen(pState->LineData.strBuffer), strBuffer, sizeof(strBuffer));
if ((iResult = JsonAddStr(pState->strCommand, "TEXT", strBuffer)) == JSON_ERR_FULL)
{
// Sometimes, the text we get in will be too big. Make sure this is an easy error to diagnose.
ZPrintf("testercomm_socket: text too large to send in TESTERCOMM_COMMANDSIZE_DEFAULT. Discarding.\n");
}
JsonObjectEnd(pState->strCommand);
}
JsonArrayEnd(pState->strCommand);
pBuffer = JsonFinish(pState->strCommand);
iLineLength = (int32_t)strlen(pBuffer);
// send it
for (pWrite = pBuffer, iBytesSent = 0; iLineLength != 0; )
{
if ((iResult = SocketSend(pSocketState->pSocket, pWrite, iLineLength, 0)) < 0)
{
ZPrintf("testercomm_socket: error %d sending command to remote target\n", iResult);
pSocketState->eState = ST_FAIL;
return(-1);
}
else
{
iLineLength -= iResult;
pWrite += iResult;
iBytesSent += iResult;
}
if (iLineLength != 0)
{
NetConnSleep(5);
}
}
// mark the last send time
pState->uLastSendTime = ZTick();
return(0);
}
/*F********************************************************************************/
/*!
\Function _TesterCommConnect
\Description
Connect the host client communication module.
\Input *pState - pointer to host client comm module
\Input *pParams - startup parameters
\Input bIsHost - TRUE=host, FALSE=client
\Output
int32_t - 0 for success, error code otherwise
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommConnect(TesterCommT *pState, const char *pParams, uint32_t bIsHost)
{
TesterCommSocketT *pSocketState = pState->pInterface->pData;
char *pUserPort;
uint16_t aJson[512];
// remember host status
pSocketState->bIsHost = (uint8_t)bIsHost;
pSocketState->iPort = T2COMM_PORT;
// get startup parameters
JsonParse(aJson, sizeof(aJson)/sizeof(*aJson), pParams, -1);
JsonGetString(JsonFind(aJson, "HOSTNAME"), pSocketState->strHostName, sizeof(pSocketState->strHostName), "");
// check for a user specified port
if ((pUserPort = strchr(pSocketState->strHostName, ':')) != NULL )
{
pSocketState->iPort = atoi(pUserPort+1);
}
// do the connect
return(_TesterCommConnect2(pSocketState));
}
/*F********************************************************************************/
/*!
\Function _TesterCommUpdate
\Description
Give the host/client interface module some processor time. Call this
once in a while to pump the input and output pipes.
\Input *pState - module state
\Output
int32_t - 0 for success, error code otherwise
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommUpdate(TesterCommT *pState)
{
int32_t iResult;
// quit if we are suspended (don't do any more commands)
if (pState->uSuspended)
{
return(0);
}
// check for accepted connection
if (_TesterCommCheckConnect(pState) <= 0)
{
return(0);
}
// check for outgoing and incoming data
_TesterCommCheckInput(pState);
_TesterCommCheckOutput(pState);
// now call the callbacks for incoming messages
while ((iResult = ZListPopFront(pState->pInputData, &pState->LineData)) > 0)
{
// try to access the message map
if ((pState->LineData.iType >= 0) && (pState->MessageMap[pState->LineData.iType] != NULL))
{
// protect against recursion by suspending commands until this one completes
TesterCommSuspend(pState);
(pState->MessageMap[pState->LineData.iType])(pState, pState->LineData.strBuffer, pState->pMessageMapUserData[pState->LineData.iType]);
TesterCommWake(pState);
}
}
// done
return(0);
}
/*F********************************************************************************/
/*!
\Function _TesterCommDisconnect
\Description
Disconnect the host client communication module.
\Input *pState - pointer to host client comm module
\Output
int32_t - 0=success, error code otherwise
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
static int32_t _TesterCommDisconnect(TesterCommT *pState)
{
TesterCommSocketT *pSocketState = (TesterCommSocketT *)pState->pInterface->pData;
_TesterCommDisconnect2(pSocketState);
if (pSocketState->pListen != NULL)
{
SocketClose(pSocketState->pListen);
pSocketState->pListen = NULL;
}
return(0);
}
/*** Public functions *************************************************************/
/*F********************************************************************************/
/*!
\Function TesterCommAttachSocket
\Description
Attach file module function pointers to a tester comm module.
\Input *pState - pointer to host client comm module
\Version 10/17/2011 (jbrookes)
*/
/********************************************************************************F*/
void TesterCommAttachSocket(TesterCommT *pState)
{
ZPrintf("testercomm_socket: attaching socket interface methods\n");
pState->pInterface->CommConnectFunc = &_TesterCommConnect;
pState->pInterface->CommUpdateFunc = &_TesterCommUpdate;
pState->pInterface->CommDisconnectFunc = &_TesterCommDisconnect;
pState->pInterface->pData = (TesterCommSocketT *)ZMemAlloc(sizeof(TesterCommSocketT));
ds_memclr(pState->pInterface->pData, sizeof(TesterCommSocketT));
}