/******************************************************************************
 *
 * Project:  GDAL
 * Purpose:  Server application that is forked by libgdal
 * Author:   Even Rouault, <even dot rouault at mines-paris dot org>
 *
 ******************************************************************************
 * Copyright (c) 2013, Even Rouault <even dot rouault at mines-paris dot org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

// So that __USE_XOPEN2K is defined to have getaddrinfo
#ifndef __sun__
#define _XOPEN_SOURCE 600
#endif

#include "cpl_port.h"

#ifdef WIN32

  /* To disable 'warning C4996: 'WSADuplicateSocketA': Use WSADuplicateSocketW()' and 'WSASocketA': Use WSASocketW() instead */
  #define _WINSOCK_DEPRECATED_NO_WARNINGS

  #ifdef _WIN32_WINNT
    #undef _WIN32_WINNT
  #endif
  #define _WIN32_WINNT 0x0501
  #include <winsock2.h>
  #include <ws2tcpip.h>
  typedef SOCKET CPL_SOCKET;
  #ifndef HAVE_GETADDRINFO
    #define HAVE_GETADDRINFO 1
  #endif
#else
  #include <sys/select.h>
  #include <sys/time.h>
  #include <sys/types.h>
  #include <sys/wait.h>
  #include <sys/socket.h>
  #include <sys/un.h>
  #include <netinet/in.h>
  #include <signal.h>
  #include <unistd.h>
  #ifdef HAVE_GETADDRINFO
    #include <netdb.h>
  #endif
  typedef int CPL_SOCKET;
  #define INVALID_SOCKET -1
  #define SOCKET_ERROR -1
  #define SOCKADDR struct sockaddr
  #define WSAGetLastError() errno
  #define WSACleanup()
  #define closesocket(s) close(s)
  #ifndef SOMAXCONN
  #define SOMAXCONN 128
  #endif
#endif


#include <gdal.h>
#include "cpl_spawn.h"
#include "cpl_string.h"

CPL_C_START
int CPL_DLL GDALServerLoop(CPL_FILE_HANDLE fin, CPL_FILE_HANDLE fout);
int CPL_DLL GDALServerLoopSocket(CPL_SOCKET nSocket);
void CPL_DLL* GDALServerLoopInstanceCreateFromSocket(CPL_SOCKET nSocket);
int  CPL_DLL  GDALServerLoopInstanceRunIteration(void* pInstance);
void CPL_DLL  GDALServerLoopInstanceDestroy(void* pInstance);
CPL_C_END

CPL_CVSID("$Id: gdalserver.c 36420 2016-11-21 23:13:06Z rouault $");

static int bVerbose = FALSE;

/************************************************************************/
/*                               Usage()                                */
/************************************************************************/

static void Usage(const char* pszErrorMsg)

{
#ifdef WIN32
    printf( "Usage: gdalserver [--help-general] [--help] [-v]\n");
    printf( "                  [-tcpserver port | -stdinout]\n");
#else
    printf( "Usage: gdalserver [--help-general] [--help] [-v]\n");
    printf( "                  [-tcpserver port | -unixserver filename | -stdinout | \n");
    printf( "                  [-pipe_in fdin,fdtoclose -pipe_out fdout,fdtoclose]]\n");
    printf( "                  [-nofork]\n");
#endif
    printf( "\n" );
    printf( "-tcpserver : Launch a TCP server on the specified port that can accept.\n");
    printf( "             connections from GDAL clients.\n");
    printf( "-stdinout  : This mode is not meant at being directly used by a user.\n");
    printf( "             It is a helper utility for the client/server working of GDAL.\n");
#ifndef WIN32
    printf( "-pipe_in/out:This mode is not meant at being directly used by a user.\n");
    printf( "             It is a helper utility for the client/server working of GDAL.\n");
    printf( "-nofork     :This mode enables sharing of datasets among several clients,\n");
    printf( "             for example in concurrent write scenarios. But in that mode,\n");
    printf( "             only one thread is used, reducing scalability and client isolation.\n");
#endif
    printf("\n");

    if( pszErrorMsg != NULL )
        // cppcheck-suppress nullPointer
        fprintf(stderr, "\nFAILURE: %s\n", pszErrorMsg);

    exit( 1 );
}


/************************************************************************/
/*                CreateSocketAndBindAndListen()                        */
/************************************************************************/

static CPL_SOCKET CreateSocketAndBindAndListen(const char* pszService,
                                 int *pnFamily,
                                 int *pnSockType,
                                 int *pnProtocol)
{
    CPL_SOCKET nListenSocket = INVALID_SOCKET;

#ifdef HAVE_GETADDRINFO
    int nRet;
    struct addrinfo sHints;
    struct addrinfo* psResults = NULL, *psResultsIter;
    memset(&sHints, 0, sizeof(struct addrinfo));
    sHints.ai_family = AF_UNSPEC;
    sHints.ai_socktype = SOCK_STREAM;
    sHints.ai_flags = AI_PASSIVE;
    sHints.ai_protocol = IPPROTO_TCP;

    nRet = getaddrinfo(NULL, pszService, &sHints, &psResults);
    if (nRet)
    {
        fprintf(stderr, "getaddrinfo(): %s\n", gai_strerror(nRet));
        return INVALID_SOCKET;
    }

    for( psResultsIter = psResults;
         psResultsIter != NULL;
         psResultsIter = psResultsIter->ai_next)
    {
        nListenSocket = socket(psResultsIter->ai_family,
                               psResultsIter->ai_socktype,
                               psResultsIter->ai_protocol);
        if (nListenSocket == INVALID_SOCKET)
            continue;

        if (bind(nListenSocket, psResultsIter->ai_addr,
                 (int)psResultsIter->ai_addrlen) != (int)SOCKET_ERROR)
        {
            if( pnFamily )   *pnFamily =   psResultsIter->ai_family;
            if( pnSockType ) *pnSockType = psResultsIter->ai_socktype;
            if( pnProtocol ) *pnProtocol = psResultsIter->ai_protocol;

            break;
        }

        closesocket(nListenSocket);
    }

    freeaddrinfo(psResults);

    if (psResultsIter == NULL)
    {
        fprintf(stderr, "Could not bind()\n");
        return INVALID_SOCKET;
    }

#else

    struct sockaddr_in sockAddrIn;

    memset( &sockAddrIn, 0, sizeof(sockAddrIn) );

    if( pnFamily )   *pnFamily = AF_INET;
    if( pnSockType ) *pnSockType = SOCK_STREAM;
    if( pnProtocol ) *pnProtocol = IPPROTO_TCP;

    nListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (nListenSocket == INVALID_SOCKET)
    {
        fprintf(stderr, "socket() failed with error: %d\n", WSAGetLastError());
        return INVALID_SOCKET;
    }

    sockAddrIn.sin_family = AF_INET;
    sockAddrIn.sin_addr.s_addr = INADDR_ANY;
    sockAddrIn.sin_port = htons(atoi(pszService));

    if (bind(nListenSocket, (SOCKADDR *)&sockAddrIn, sizeof (sockAddrIn)) == SOCKET_ERROR)
    {
        fprintf(stderr, "bind() function failed with error: %d\n", WSAGetLastError());
        closesocket(nListenSocket);
        return INVALID_SOCKET;
    }

#endif

    if (listen(nListenSocket, SOMAXCONN) == SOCKET_ERROR)
    {
        fprintf(stderr, "listen() function failed with error: %d\n", WSAGetLastError());
        closesocket(nListenSocket);
        return INVALID_SOCKET;
    }

    return nListenSocket;
}

#ifdef WIN32

/************************************************************************/
/*                             RunServer()                              */
/************************************************************************/

static
int RunServer(const char* pszApplication,
              const char* pszService,
              CPL_UNUSED const char* unused_pszUnixSocketFilename,
              CPL_UNUSED int bFork)
{
    int nRet;
    WSADATA wsaData;
    SOCKET nListenSocket;
    int nFamily, nSockType, nProtocol;

    nRet = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (nRet != NO_ERROR)
    {
        fprintf(stderr, "WSAStartup() failed with error: %d\n", nRet);
        return 1;
    }

    nListenSocket = CreateSocketAndBindAndListen(pszService, &nFamily, &nSockType, &nProtocol);
    if (nListenSocket == INVALID_SOCKET)
    {
        WSACleanup();
        return 1;
    }

    while(TRUE)
    {
        WSAPROTOCOL_INFO sSocketInfo;
        struct sockaddr sockAddr;
        socklen_t nLen = sizeof(sockAddr);
        CPLSpawnedProcess* psProcess;
        CPL_FILE_HANDLE fin, fout;
        CPL_PID nPid;
        CPL_SOCKET nConnSocket;
        char szReady[5];
        int bOK = TRUE;
        const char* apszArgs[] = { NULL, "-newconnection", NULL };

        apszArgs[0] = pszApplication;
        nConnSocket = accept(nListenSocket, &sockAddr, &nLen);
        if( nConnSocket == (CPL_SOCKET)SOCKET_ERROR )
        {
            fprintf(stderr, "accept() function failed with error: %d\n", WSAGetLastError());
            closesocket(nListenSocket);
            WSACleanup();
            return 1;
        }

        psProcess = CPLSpawnAsync( NULL,
                                   apszArgs,
                                   TRUE,
                                   TRUE,
                                   FALSE,
                                   NULL);
        if( psProcess == NULL )
        {
            fprintf(stderr, "CPLSpawnAsync() function failed.\n");
            closesocket(nConnSocket);
            closesocket(nListenSocket);
            WSACleanup();
            return 1;
        }

        nPid = CPLSpawnAsyncGetChildProcessId(psProcess);
        fin = CPLSpawnAsyncGetInputFileHandle(psProcess);
        fout = CPLSpawnAsyncGetOutputFileHandle(psProcess);

        if( WSADuplicateSocket(nConnSocket, nPid, &sSocketInfo) != 0 )
        {
            fprintf(stderr, "WSADuplicateSocket() failed: %d\n", WSAGetLastError());
            bOK = FALSE;
        }

        /* Send socket parameters over the pipe */
        if( bOK &&
            (!CPLPipeWrite(fout, &sSocketInfo, sizeof(sSocketInfo)) ||
             !CPLPipeWrite(fout, &nFamily, sizeof(nFamily)) ||
             !CPLPipeWrite(fout, &nSockType, sizeof(nSockType)) ||
             !CPLPipeWrite(fout, &nProtocol, sizeof(nProtocol))) )
        {
            fprintf(stderr, "CPLWritePipe() failed\n");
            bOK = FALSE;
        }

        /* Wait for child to be ready before closing the socket */
        if( bOK && !CPLPipeRead(fin, szReady, sizeof(szReady)) )
        {
            fprintf(stderr, "CPLReadPipe() failed\n");
            bOK = FALSE;
        }

        if( !bOK )
        {
            CPLSpawnAsyncFinish(psProcess, FALSE, TRUE);
            closesocket(nConnSocket);
            closesocket(nListenSocket);
            WSACleanup();
            return 1;
        }


        closesocket(nConnSocket);

        CPLSpawnAsyncFinish(psProcess, FALSE, FALSE);
    }

    // closesocket(nConnSocket);
    // WSACleanup();
}

/************************************************************************/
/*                          RunNewConnection()                          */
/************************************************************************/

static
int RunNewConnection()
{
    int nRet;
    WSADATA wsaData;
    WSAPROTOCOL_INFO sSocketInfo;
    int nFamily, nSockType, nProtocol;
    SOCKET nConnSocket;
    CPL_FILE_HANDLE fin = GetStdHandle(STD_INPUT_HANDLE);
    CPL_FILE_HANDLE fout = GetStdHandle(STD_OUTPUT_HANDLE);

    /* Get socket parameters from the pipe */
    if (!CPLPipeRead(fin, &sSocketInfo, sizeof(sSocketInfo)) ||
        !CPLPipeRead(fin, &nFamily, sizeof(nFamily)) ||
        !CPLPipeRead(fin, &nSockType, sizeof(nSockType)) ||
        !CPLPipeRead(fin, &nProtocol, sizeof(nProtocol)) )
    {
        fprintf(stderr, "CPLPipeRead() failed\n");
        return 1;
    }

    nRet = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (nRet != NO_ERROR)
    {
        fprintf(stderr, "WSAStartup() failed with error: %d\n", nRet);
        return 1;
    }

    nConnSocket = WSASocket(nFamily, nSockType, nProtocol, &sSocketInfo, 0, WSA_FLAG_OVERLAPPED);
    if (nConnSocket == INVALID_SOCKET)
    {
        fprintf(stderr, "ConnSocket() failed with error: %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    /* Warn the parent that we are now ready */
    if (!CPLPipeWrite(fout, "ready", 5))
    {
        fprintf(stderr, "CPLPipeWrite() failed\n");
        WSACleanup();
        return 1;
    }
    CloseHandle(fout);

#ifdef _MSC_VER
    __try {
#endif
    nRet = GDALServerLoopSocket(nConnSocket);
#ifdef _MSC_VER
    } __except(1)
    {
        fprintf(stderr, "gdalserver exited with a fatal error.\n");
        nRet = 1;
    }
#endif

    closesocket(nConnSocket);
    WSACleanup();

    return nRet;
}

#else

/************************************************************************/
/*                             RunServer()                              */
/************************************************************************/

typedef struct
{
    int nSocket;
    void* hSrvLoopInstance;
} ClientInfo;

#define MAX_CLIENTS 100

static int RunServer(CPL_UNUSED const char* pszApplication,
              const char* pszService,
              const char* pszUnixSocketFilename,
              int bFork)
{
    int nListenSocket;
    int i;
    int nClients = 0;
    ClientInfo asClientInfos[MAX_CLIENTS];

    memset( asClientInfos, 0, sizeof(asClientInfos) );

    if( !bFork )
        signal(SIGPIPE, SIG_IGN);

    if( pszUnixSocketFilename != NULL )
    {
        struct sockaddr_un sockAddrUnix;
        int len;
        memset( &sockAddrUnix, 0, sizeof(sockAddrUnix) );

        nListenSocket = socket(AF_UNIX, SOCK_STREAM, 0);
        if (nListenSocket < 0)
        {
            perror("socket");
            return 1;
        }

        sockAddrUnix.sun_family = AF_UNIX;
        CPLStrlcpy(sockAddrUnix.sun_path, pszUnixSocketFilename, sizeof(sockAddrUnix.sun_path));
        unlink(sockAddrUnix.sun_path);
        len = (int)(strlen(sockAddrUnix.sun_path) + sizeof(sockAddrUnix.sun_family));
        if (bind(nListenSocket, (struct sockaddr *)&sockAddrUnix, len) == -1)
        {
            perror("bind");
            closesocket(nListenSocket);
            return 1;
        }
        if (listen(nListenSocket, SOMAXCONN) == SOCKET_ERROR)
        {
            fprintf(stderr, "listen() function failed with error: %d\n", WSAGetLastError());
            closesocket(nListenSocket);
            return 1;
        }
    }
    else
    {
        nListenSocket = CreateSocketAndBindAndListen(pszService, NULL, NULL, NULL);
        if (nListenSocket < 0)
        {
            return 1;
        }
    }

    while(TRUE)
    {
        struct sockaddr sockAddr;
        socklen_t nLen = sizeof(sockAddr);
        pid_t pid;
        int nStatus;
        struct timeval tv;
        fd_set read_fds;
        int nMaxSocket;

        memset(&tv, 0, sizeof(tv));

        /* Select on the listen socket, and rip zombie children every second */
        do
        {
            nMaxSocket = nListenSocket;
            FD_ZERO(&read_fds);
            FD_SET(nListenSocket, &read_fds);
            for(i=0;i<nClients;i++)
            {
                FD_SET(asClientInfos[i].nSocket, &read_fds);
                if(asClientInfos[i].nSocket > nMaxSocket )
                    nMaxSocket = asClientInfos[i].nSocket;
            }
            tv.tv_sec = 1;
            tv.tv_usec = 0;
            waitpid(-1, &nStatus, WNOHANG);
        }
        while( select(nMaxSocket + 1, &read_fds, NULL, NULL, &tv) < 1 );

        if( bFork )
        {
            int nConnSocket = accept(nListenSocket, &sockAddr, &nLen);
            if( nConnSocket < 0 )
            {
                fprintf(stderr, "accept() function failed with error: %d\n", errno);
                close(nListenSocket);
                return 1;
            }

            pid = fork();
            if( pid < 0 )
            {
                fprintf(stderr, "fork() failed: %d\n", errno);
                close(nListenSocket);
                close(nConnSocket);
                return 1;
            }
            else if( pid == 0 )
            {
                int nRet;
                close(nListenSocket);
                nRet = GDALServerLoopSocket(nConnSocket);
                close(nConnSocket);
                return nRet;
            }
            else
            {
                close(nConnSocket);
            }
        }
        else
        {
            if( FD_ISSET(nListenSocket, &read_fds) )
            {
                int nConnSocket;
                nConnSocket = accept(nListenSocket, &sockAddr, &nLen);
                if( nConnSocket < 0 )
                {
                    fprintf(stderr, "accept() function failed with error: %d\n", errno);
                }
                else
                {
                    if( nClients == MAX_CLIENTS  )
                    {
                        fprintf(stderr, "Refusing new connection: too many clients (%d)\n",
                                MAX_CLIENTS);
                        close(nConnSocket);
                    }
                    else
                    {
                        void* hSrvLoopInstance = GDALServerLoopInstanceCreateFromSocket(nConnSocket);
                        asClientInfos[nClients].nSocket = nConnSocket;
                        asClientInfos[nClients].hSrvLoopInstance = hSrvLoopInstance;
                        if( bVerbose )
                            fprintf(stderr, "Accepting new client %d\n", nClients);
                        nClients ++;
                    }
                }
            }
            for(i=0;i<nClients;)
            {
                if( FD_ISSET(asClientInfos[i].nSocket, &read_fds) )
                {
                    //fprintf(stderr, "receiving info from client %d\n", i);
                    if( !GDALServerLoopInstanceRunIteration(asClientInfos[i].hSrvLoopInstance) )
                    {
                        if( bVerbose )
                            fprintf(stderr, "Removing client %d\n", i);
                        GDALServerLoopInstanceDestroy(asClientInfos[i].hSrvLoopInstance);
                        close(asClientInfos[i].nSocket);
                        memmove(asClientInfos + i, asClientInfos + i + 1,
                                (nClients - 1 - i) * sizeof(ClientInfo));
                        nClients --;
                        if( nClients == 0 && bVerbose )
                            fprintf(stderr, "No more clients. Server can be safely shut down\n");
                        continue;
                    }
                }
                i++;
            }
        }
    }
}

#endif

/************************************************************************/
/*                                main()                                */
/************************************************************************/

#define CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(nExtraArg) \
    do { if (i + nExtraArg >= argc) \
        Usage(CPLSPrintf("%s option requires %d argument(s)", argv[i], nExtraArg)); } while(0)

int main(int argc, char* argv[])
{
    int i, nRet, bStdinout = FALSE, bPipeIn = FALSE, bPipeOut = FALSE, bNewConnection = FALSE;
    const char* pszService = NULL, *pszUnixSocketFilename = NULL;
#ifndef WIN32
    int pipe_in = fileno(stdin);
    int pipe_out = fileno(stdout);
#endif
    int bFork = TRUE;
    /*for( i = 1; i < argc; i++ )
    {
        if( EQUAL(argv[i], "-daemonize") )
        {
            daemon(0, 0);
            break;
        }
    }*/

    GDALAllRegister();

    argc = GDALGeneralCmdLineProcessor( argc, &argv, 0 );
    if( argc < 1 )
        exit( -argc );

    for( i = 1; i < argc; i++ )
    {
        if( EQUAL(argv[i], "--utility_version") )
        {
            printf("%s was compiled against GDAL %s and is running against GDAL %s\n",
                   argv[0], GDAL_RELEASE_NAME, GDALVersionInfo("RELEASE_NAME"));
            return 0;
        }
        else if( EQUAL(argv[i],"--help") )
            Usage(NULL);
        else if( EQUAL(argv[i],"-tcpserver") )
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            i++;
            pszService = argv[i];
        }
#ifndef WIN32
        else if( EQUAL(argv[i],"-unixserver") )
        {
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            i++;
            pszUnixSocketFilename = argv[i];
        }
#endif
#ifdef WIN32
        else if( EQUAL(argv[i],"-newconnection") )
        {
            bNewConnection = TRUE;
        }
#endif
        else if( EQUAL(argv[i],"-stdinout") )
            bStdinout = TRUE;
#ifndef WIN32
        else if( EQUAL(argv[i],"-pipe_in") )
        {
            const char* pszComma;
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            i++;
            pipe_in = atoi(argv[i]);
            bPipeIn = TRUE;
            pszComma = strchr(argv[i], ',');
            if( pszComma )
                close(atoi(pszComma + 1));
        }
        else if( EQUAL(argv[i],"-pipe_out") )
        {
            const char* pszComma;
            CHECK_HAS_ENOUGH_ADDITIONAL_ARGS(1);
            i++;
            pipe_out = atoi(argv[i]);
            bPipeOut = TRUE;
            pszComma = strchr(argv[i], ',');
            if( pszComma )
                close(atoi(pszComma + 1));
        }
        else if( EQUAL(argv[i],"-nofork") )
        {
            bFork = FALSE;
        }
#endif
        else if( EQUAL(argv[i], "-daemonize") )
            ;
        else if( EQUAL(argv[i], "-v") )
            bVerbose = TRUE;
        else if( argv[i][0] == '-' )
            Usage(CPLSPrintf("Unknown option name '%s'", argv[i]));
        else
            Usage("Too many command options.");
    }
    if( !bStdinout && !(bPipeIn && bPipeOut) &&
        pszService == NULL && pszUnixSocketFilename == NULL && !bNewConnection )
        Usage(NULL);

    if( pszService != NULL || pszUnixSocketFilename != NULL )
        nRet = RunServer(argv[0], pszService, pszUnixSocketFilename, bFork);
#ifdef WIN32
    else if( bNewConnection )
        nRet = RunNewConnection();
#endif

    else
    {
#ifdef WIN32
#ifdef _MSC_VER
    __try
#endif
    {
        nRet = GDALServerLoop(GetStdHandle(STD_INPUT_HANDLE),
                              GetStdHandle(STD_OUTPUT_HANDLE));
    }
#ifdef _MSC_VER
    __except(1)
    {
        fprintf(stderr, "gdalserver exited with a fatal error.\n");
        nRet = 1;
    }
#endif
#else
    if( !bFork )
        fprintf(stderr, "-nofork option incompatible with direct pipe specification.\n");
    nRet = GDALServerLoop(pipe_in, pipe_out);
#endif
    }

    CSLDestroy(argv);

    return nRet;
}
