Stuck in Infinite Loop with ReadFile() in C++ Process Interaction via Pipes

What can cause that infinite loop inside ReadFile() and how to fix it?

ProcessPtr process = addProcess("cmd", true);
process->read(true);
process->write("echo Hello\n", true);
process->read(true);
process->write("exit\n", true);

When running cmd:

    ┌----- The 1st call of ReadFile() succeeded.
    v
Microsoft Windows [Version 10.0.18363.592]
(c) 2019 Microsoft Corporation. All rights reserved.

    <----- The 2nd call of ReadFile() gets stuck in an infinite loop inside ReadFile().
>echo Hello
Hello

>exit

Code for Reproducing the Exact Same Behavior

Add a breakpoint on the line with ReadFile(). There is no infinite loop during the first call of it, but an infinite loop occurs during the second call of it.

#include "windows.h"

class _Process; typedef shared_ptr<_Process> ProcessPtr;

class _Process {
    friend ProcessPtr addProcess(const char *cmd, bool usePipes_and_noWait);
private:
    HANDLE _hStdInPipe_wr  = nullptr;
    HANDLE _hStdOutPipe_rd = nullptr;
    HANDLE _hProcess       = nullptr;
    HANDLE _hThread        = nullptr;
public:
    _Process();
    ~_Process();

    void write(const char *data, bool debugOutput);
    void read(bool debugOutput);
};

_Process::_Process() {
}
ProcessPtr addProcess(const char *cmd, bool usePipes_and_noWait) {
    ProcessPtr process = ProcessPtr(new _Process());
    #if defined(__linux__) || defined(__ANDROID__)
        // TODO
    #elif defined(_WIN32)
        // [How to read output from cmd.exe using CreateProcess() and CreatePipe()](https://stackoverflow.com/questions/35969730/how-to-read-output-from-cmd-exe-using-createprocess-and-createpipe)

        HANDLE hStdInPipe_rd  = nullptr;
        HANDLE hStdInPipe_wr  = nullptr;
        HANDLE hStdOutPipe_rd = nullptr;
        HANDLE hStdOutPipe_wr = nullptr;
        if (usePipes_and_noWait) {
            // Create two pipes
            SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), nullptr, true };
            if (!CreatePipe(&hStdInPipe_rd, &hStdInPipe_wr, &sa, 0)) {
                throw string("CreatePipe() failed."); return nullptr;
            }
            if (!CreatePipe(&hStdOutPipe_rd, &hStdOutPipe_wr, &sa, 0)) {
                throw string("CreatePipe() failed."); return nullptr;
            }
        }

        // https://learn.microsoft.com/it-it/windows/win32/procthread/creating-processes
        {
            STARTUPINFO si = {};
            si.cb = sizeof(si);
            if (usePipes_and_noWait) {
                si.dwFlags    = STARTF_USESTDHANDLES;
                si.hStdInput  = hStdInPipe_rd;
                si.hStdOutput = hStdOutPipe_wr;
                si.hStdError  = hStdOutPipe_wr;
            }

            PROCESS_INFORMATION pi = {};

            if (!CreateProcess(
                nullptr,
                const_cast<LPSTR>(cmd), // Command line (https://stackoverflow.com/questions/14682480/converting-from-const-char-to-lptstr-without-uses-converstion)
                nullptr,
                nullptr,
                usePipes_and_noWait ? true : false,
                0,
                nullptr,
                nullptr,
                &si,
                &pi
            )) {
                throw string("Failed to create process."); return nullptr;
            }

            if (usePipes_and_noWait) {
                // Close pipes we do not need
                CloseHandle(hStdOutPipe_wr); hStdOutPipe_wr = nullptr;
                CloseHandle(hStdInPipe_rd);  hStdInPipe_rd  = nullptr;
            }

            process->_hStdInPipe_wr  = hStdInPipe_wr;
            process->_hStdOutPipe_rd = hStdOutPipe_rd;
            process->_hProcess       = pi.hProcess;
            process->_hThread        = pi.hThread;

            if (usePipes_and_noWait == false) {
                // Wait until child process exits
                WaitForSingleObject(pi.hProcess, INFINITE); // It can cause an infinite loop (https://stackoverflow.com/questions/28459471/createprocess-not-in-primary-thread).
            }
        }
    #endif
    return process;
}
_Process::~_Process() {
    #if defined(__linux__) || defined(__ANDROID__)
        // TODO
    #elif defined(_WIN32)
        CloseHandle(_hStdInPipe_wr);  _hStdInPipe_wr  = nullptr;
        CloseHandle(_hStdOutPipe_rd); _hStdOutPipe_rd = nullptr;
        CloseHandle(_hProcess);       _hProcess       = nullptr;
        CloseHandle(_hThread);        _hThread        = nullptr;
    #endif
}

void _Process::write(const char *data, bool debugOutput) {
    size_t dataSize = strlen(data) + 1; // Include the null-terminator.
    #if defined(__linux__) || defined(__ANDROID__)
        EMC_ERROR("TODO");
    #elif defined(_WIN32)
        DWORD bytesWritten;
        WriteFile(_hStdInPipe_wr, data, static_cast<DWORD>(dataSize), &bytesWritten, nullptr);

        if (debugOutput) {
            if (bytesWritten) {
                setConsoleTextColor(ConsoleTextColor_Yellow);
            } else {
                setConsoleTextColor(ConsoleTextColor_Red);
            }
            printf("%s", data);
            setConsoleTextColor(ConsoleTextColor_Reset);
        }
    #endif
}

void _Process::read(bool debugOutput) {
    #if defined(__linux__) || defined(__ANDROID__)
        EMC_ERROR("TODO");
    #elif defined(_WIN32)
        // https://stackoverflow.com/questions/35969730/how-to-read-output-from-cmd-exe-using-createprocess-and-createpipe

        DWORD bufSize = 1024;
        std::vector<char> buf;
        buf.resize(bufSize + 1);
        DWORD numBytesRead = 0;
        
        while (true) {
            bool ok = ReadFile(_hStdOutPipe_rd, &buf[0], bufSize, &numBytesRead, nullptr);
            if (!ok || numBytesRead == 0) {
                // Error or no more data to read, exit the loop.
                break;
            }
            buf[numBytesRead] = '\0';

            if (debugOutput) {
                setConsoleTextColor(ConsoleTextColor_Green);
                printf("%s", &buf[0]);
                setConsoleTextColor(ConsoleTextColor_Reset);
            }
        }
    #endif
}

void main() {
    ProcessPtr process = addProcess("cmd", true);
    process->read(true);
    process->write("echo Hello\n", true);
    process->read(true);
    process->write("exit\n", true);
}

Example of command without an infinite loop

printf("%s\n", executeAndCaptureOutput("cmd /c dir").c_str());

Example of command with an infinite loop

printf("%s\n", executeAndCaptureOutput("cmd").c_str());

Conclusion: When ‘cmd’ is executed, it enters into its internal input/output, causing an infinite loop.
If anyone can suggest an alternative working method, I’d greatly appreciate it.

Goal

I intend to utilize GDB in the following manner, with the goal of reading the results from C++. But in my real project, I will be utilizing GDB/MI (GDB Machine Interface):

> gdb
(gdb) print "Hello, World!"
$1 = "Hello, World!"
(gdb) print 2 + 3
$2 = 5
(gdb) quit
>

Code for Reproducing the Exact Same Behavior

#include "windows.h"

string executeAndCaptureOutput(const string &commandLine) {
    // [C++ - How to read output from cmd.exe using CreateProcess() and CreatePipe()](https://itecnote.com/tecnote/c-how-to-read-output-from-cmd-exe-using-createprocess-and-createpipe/)

    // Execute a child process, and capture it's command line output.

    string result = "";

    SECURITY_ATTRIBUTES sa = {};
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.bInheritHandle = true;
    sa.lpSecurityDescriptor = nullptr;

    HANDLE stdOutRead  = nullptr;
    HANDLE stdOutWrite = nullptr;
    if (!CreatePipe(&stdOutRead, &stdOutWrite, &sa, 0)) {
        throw std::runtime_error("CreatePipe() failed"); return "";
    }

    // Set up members of the STARTUPINFO structure.
    STARTUPINFO si = {};
    si.cb = sizeof(STARTUPINFO);

    // This structure specifies the STDIN and STDOUT handles for redirection.
    si.dwFlags |= STARTF_USESTDHANDLES; // The hStdInput, hStdOutput, and hStdError handles will be valid.
        si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); //don't forget to make it valid (zero is not valid)
        si.hStdOutput = stdOutWrite; // give the console app the writable end of the pipe
        si.hStdError = stdOutWrite; // give the console app the writable end of the pipe

    // We also want the console window to be hidden
    si.dwFlags |= STARTF_USESHOWWINDOW; // The nShowWindow member member will be valid.
        si.wShowWindow = SW_HIDE; // default is that the console window is visible

    // Set up members of the PROCESS_INFORMATION structure.
    PROCESS_INFORMATION pi = {};

    if (!CreateProcess(nullptr, const_cast<LPSTR>(commandLine.c_str()), nullptr, nullptr, true, 0, nullptr, nullptr, &si, &pi)) {
        throw std::runtime_error("CreateProcess() failed"); return "";
    }

    // CreateProcess demands that we close these two populated handles when we're done with them. We're done with them.
    CloseHandle(pi.hProcess); pi.hProcess = nullptr;
    CloseHandle(pi.hThread);  pi.hThread  = nullptr;

    // We've given the console app the writable end of the pipe during CreateProcess; we don't need it anymore.
    // We do keep the handle for the *readable* end of the pipe; as we still need to read from it.
    // The other reason to close the writable-end handle now is so that there's only one out-standing reference to the writeable end: held by the console app.
    // When the app closes, it will close the pipe, and ReadFile will return code 109 (The pipe has been ended).
    // That's how we'll know the console app is done. (no need to wait on process handles)
    CloseHandle(stdOutWrite); stdOutWrite = nullptr;

    //Read will return when the buffer is full, or if the pipe on the other end has been broken
    const DWORD bufSize = 1024;
    char buf[bufSize + 1] = {};
    DWORD bytesRead = 0;
    for (;;) {
        bool ok = ReadFile(stdOutRead, &buf[0], bufSize, &bytesRead, nullptr);
        if (!ok || bytesRead == 0) {
            break;
        }
        buf[bytesRead] = '\0';

        result += buf;
    }

    // ReadFile will either tell us that the pipe has closed, or give us an error
    DWORD le = GetLastError();
    if (le != ERROR_BROKEN_PIPE) { // The pipe has been ended.
        throw std::runtime_error("ReadFile() failed"); return "";
    }
    
    if (stdOutRead ) { CloseHandle(stdOutRead);  stdOutRead  = nullptr; }
    if (stdOutWrite) { CloseHandle(stdOutWrite); stdOutWrite = nullptr; }
    return result;
}

void main() {
    // Without an infinite loop
    //printf("%s\n", executeAndCaptureOutput("cmd /c dir").c_str());

    // With an infinite loop (not with the debug button in Visual Studio Code)
    printf("%s\n", executeAndCaptureOutput("cmd").c_str());
}

The code above has been translated from Delphi to C++. You can find the original code and related information in the following link: C++ - How to read output from cmd.exe using CreateProcess() and CreatePipe()

To ensure accuracy, I performed double translations and carefully compared the results. The fact that both translations matched indicates that there are no mistakes in the translation.

EDIT: Solution

What is the closest thing Windows has to fork()?

There is no easy way to emulate fork() on Windows. I suggest you to use threads instead.

popen() writes output of command executed to cout
std::string command("gdb");

Solution

Ensure that you do not insert ‘\0’ to prevent the possibility of either getting stuck in an infinite loop or encountering a bug, that’s because the data is repeatedly inserted, as if by analogy.

void _Process::executeCommand(const string &cmd) {
    ...
    string cmd_endl = cmd + "\n";
    const char *data = cmd_endl.c_str(); // When c_str() is called, it returns a pointer to a null-terminated C-style string (const char *).
    size_t dataSize = cmd_endl.length(); // NOTE: Excluding the null-terminator, the data to be sent should not contain '\0'.
    DWORD bytesWritten;
    EMC_VERIFY(_hStdInPipe_wr);
    WriteFile(_hStdInPipe_wr, data, static_cast<DWORD>(dataSize), &bytesWritten, nullptr);
    if (bytesWritten == 0 || bytesWritten != dataSize) {
        throw std::runtime_error("Failed to execute command."); return;
    }
    ...
}