int ExecuteCommand()

in src/common/commonutils/CommandUtils.c [59:305]


int ExecuteCommand(void* context, const char* command, bool replaceEol, bool forJson, unsigned int maxTextResultBytes, unsigned int timeoutSeconds,
    char** textResult, CommandCallback callback, OsConfigLogHandle log)
{
    int workerPid = -1;
    int pipefd[2] = {0};
    long startTime = 0;

    if (NULL == command)
    {
        OsConfigLogDebug(log, "Command cannot be NULL");
        return -1;
    }
    if (strlen(command) > (size_t)sysconf(_SC_ARG_MAX))
    {
        OsConfigLogError(log, "Command '%.40s...' is too long, %lu characters (maximum %lu characters)", command, strlen(command), (size_t)sysconf(_SC_ARG_MAX));
        return E2BIG;
    }

#ifdef TEST_CODE
    // Allow mocked call for unit testing of things that execute commands.
    struct MockCommand* mock = g_mockCommand;
    while (NULL != mock) {
        size_t stringLen = strlen(mock->expectedCommand);
        if (!mock->matchPrefix && (strlen(command) > stringLen))
        {
            stringLen = strlen(command);
        }
        if (0 == strncmp(mock->expectedCommand, command, stringLen))
        {
            if ((NULL != textResult) && (NULL != mock->output))
            {
                *textResult = DuplicateString(mock->output);
            }
            return mock->returnCode;
        }
	mock = mock->next;
    }
#endif

    // Create a pipe, then fork. Forked process duplicates the write pipe end to stdout and stderr,
    // then execs the shell with the given command. The main process uses select() with a timeout.
    // to read from the pipe, and keep track of both command  timeout and callbacks. The read loop
    // ends when the read() returns EOF or when the command times out or the callback returns a non-zero value.
    // The read loop also replaces the EOL characters with spaces if requested, and replaces all special
    // characters with spaces if requested. The output is returned in the textResult buffer, which is
    // allocated by this function. The caller is responsible for freeing the buffer when done.

    startTime = MonotonicTime();
    if (startTime < 0)
    {
        OsConfigLogError(log, "Cannot get time for command '%s', clock_gettime() failed with %d (%s)", command, errno, strerror(errno));
        return errno;
    }

    if (0 != pipe(pipefd))
    {
        OsConfigLogError(log, "Cannot create pipe for command '%s', pipe() failed with %d (%s)", command, errno, strerror(errno));
        return errno;
    }

    workerPid = fork();
    if (workerPid < 0)
    {
        OsConfigLogError(log, "Cannot fork for command '%s', fork() failed with %d (%s)", command, errno, strerror(errno));
        close(pipefd[0]);
        close(pipefd[1]);
        return errno;
    }

    if (0 == workerPid)
    {
        // Child process
        close(pipefd[0]);
        if (STDOUT_FILENO != dup2(pipefd[1], STDOUT_FILENO))
        {
            exit(errno);
        }
        if (STDERR_FILENO != dup2(pipefd[1], STDERR_FILENO))
        {
            exit(errno);
        }
        execl("/bin/sh", "sh", "-c", command, (char*)NULL);
        // If execl() fails, exit with the error code
        exit(errno);
    }
    else
    {
        // Main process
        const int defaultCommandTimeout = 60;  // seconds
        const int callbackIntervalSeconds = 5; // seconds
        long lastCallbackTime = 0;
        int status = -1;
        int childStatus = 0;
        unsigned int outputBufferPos = 0;
        unsigned int outputBufferSize = 0;

        close(pipefd[1]);

        if ((NULL != callback) && (timeoutSeconds == 0))
        {
            timeoutSeconds = defaultCommandTimeout;
        }

        for (;;)
        {
            const struct timeval selectInterval = {0, 100 * 1000}; // 100 ms, accuracy of timeouts.
            struct timeval tv;
            int bytesRead = 0;
            int inputBufferPos = 0;
            long currentTime = 0;
            char buffer[BUFFER_SIZE] = {0};
            fd_set fdset;
            int ret = 0;
            char* tmp = NULL;

            FD_ZERO(&fdset);
            FD_SET(pipefd[0], &fdset);

            tv = selectInterval;
            ret = select(pipefd[0] + 1, &fdset, NULL, NULL, &tv);
            if (ret < 0)
            {
                if (EINTR == errno)
                {
                    continue;
                }
                OsConfigLogError(log, "Error doing select for command '%s', select() failed with %d (%s)", command, errno, strerror(errno));
                status = errno;
                break;
            }

            currentTime = MonotonicTime();
            if (currentTime < 0)
            {
                OsConfigLogError(log, "Error getting time for command '%s', clock_gettime() failed with %d (%s)", command, errno, strerror(errno));
                status = errno;
                break;
            }
            if ((timeoutSeconds > 0) && (currentTime - startTime >= timeoutSeconds))
            {
                OsConfigLogError(log, "Timeout reading from pipe for command '%s', %d seconds", command, (int)(currentTime - startTime));
                status = ETIME;
                break;
            }
            if ((NULL != callback) && (currentTime - lastCallbackTime >= callbackIntervalSeconds))
            {
                if (0 != callback(context))
                {
                    OsConfigLogError(log, "Canceled reading from pipe for command '%s'", command);
                    status = ECANCELED;
                    break;
                }
                lastCallbackTime = currentTime;
            }

            if (!FD_ISSET(pipefd[0], &fdset))
            {
                // It was a timeout, nothing to read, loop.
                continue;
            }

            bytesRead = read(pipefd[0], buffer, BUFFER_SIZE);
            if (bytesRead == 0)
            {
                // Child closed the pipe, we are done.
                status = 0;
                break;
            }
            if (bytesRead < 0)
            {
                if (EINTR == errno)
                {
                    continue;
                }
                OsConfigLogError(log, "Error reading from pipe for command '%s', read() failed with %d (%s)", command, errno, strerror(errno));
                status = errno;
                break;
            }

            if (((maxTextResultBytes > 0) && (outputBufferPos == maxTextResultBytes)) || textResult == NULL)
            {
                // We don't want any more data, loop to read the rest of the output.
                continue;
            }

            outputBufferSize = bytesRead + outputBufferPos;
            if ((maxTextResultBytes > 0) && (outputBufferSize > maxTextResultBytes - 1))
            {
                outputBufferSize = maxTextResultBytes - 1;
            }

            tmp = realloc(*textResult, outputBufferSize + 1);
            if (NULL == tmp)
            {
                OsConfigLogError(log, "Cannot allocate buffer for command '%s'", command);
                status = ENOMEM;
                FREE_MEMORY(*textResult);
                break;
            }
            *textResult = tmp;

            for (inputBufferPos = 0; (inputBufferPos < bytesRead) && (outputBufferPos < outputBufferSize); inputBufferPos++, outputBufferPos++)
            {
                // Copy the data. Following characters are replaced with spaces:
                // all special characters from 0x00 to 0x1F except 0x0A (LF) when replaceEol is false
                // plus 0x22 (") and 0x5C (\) characters that break the JSON envelope when forJson is true
                const char c = buffer[inputBufferPos];
                if ((replaceEol && (EOL == c)) || ((c < 0x20) && (EOL != c)) || (0x7F == c) || (forJson && (('"' == c) || ('\\' == c))))
                {
                    (*textResult)[outputBufferPos] = ' ';
                }
                else
                {
                    (*textResult)[outputBufferPos] = c;
                }
            }
        }

        if ((NULL != textResult) && (NULL != *textResult))
        {
            (*textResult)[outputBufferPos] = '\0';
        }

        close(pipefd[0]);
        kill(workerPid, SIGKILL);
        waitpid(workerPid, &childStatus, 0);
        if (status == 0)
        {
            // The command was successful, but we need to check the child process status.
            if (WIFEXITED(childStatus))
            {
                status = WEXITSTATUS(childStatus);
            }
            else
            {
                status = childStatus;
            }
        }

        OsConfigLogDebug(log, "Context: '%p'", context);
        OsConfigLogDebug(log, "Command: '%s'", command);
        OsConfigLogDebug(log, "Status: %d (errno: %d)", status, errno);
        OsConfigLogDebug(log, "Text result: '%s'", (NULL != textResult && NULL != *textResult) ? (*textResult) : "");

        return status;
    }
}