0
votes

I'm desperately trying to create a child process and redirect its output to new pipes and read from those pipes, but I just can't get it to work. I am very new the Win32API, please be nice to me. :)

After having failed on using the Win32API "normally", I created wrappers to focus on finding an error in the logic and/or order of API calls. You can find the interface for the wrappers below. Since most of the methods directly translate to Win32API calls, it should (hopefully) not be an obstacle to answering this question.

I get the same behaviour with using the wrapper classes as I have experienced originally.

I've read a lot of online resources about this topic and one says something different than the other. The one that has been most useful until now was https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx, especially this information (emphasis mine):

The parent process uses the opposite ends of these two pipes to write to the child process's input and read from the child process's output. As specified in the STARTUPINFO structure, these handles are also inheritable. However, these handles must not be inherited. Therefore, before creating the child process, the parent process uses the SetHandleInformation function to ensure that the write handle for the child process's standard input and the read handle for the child process's standard input cannot be inherited. For more information, see Pipes.

Before I found this topic and closed the ends that I'm not using from the parent process side, I head ReadFile() blocking forever on the standard output read handle of the child process. Now, it always immediately returns that the pipe is broken.

This is how I create the Pipes and Process:

Popen(const String& command, const String& args,
      Bool use_current_pipes = false, Bool merge_stderr = true)
{
    Bool ok = true;
    _error = 0;
    ZeroMemory(&_pi, sizeof(_pi));
    STARTUPINFO si;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    if (!use_current_pipes) {
        // Create pipes for standard input, output and error.
        _stdin = Pipe(true);
        _stdout = Pipe(true);
        if (_stdout && merge_stderr)
            _stderr = _stdout.Duplicate();
        else
            _stderr = Pipe(true);

        if (_stdin && _stdout && _stderr) {
            _stdin.w.SetInheritable(false);
            _stderr.r.SetInheritable(false);
            _stdout.r.SetInheritable(false);

            si.hStdInput = _stdin.r.Get();
            si.hStdOutput = _stdout.w.Get();
            si.hStdError = _stderr.w.Get();
            si.dwFlags |= STARTF_USESTDHANDLES;
        }
        else {
            ok = false;
        }
    }
    else {
        si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
        si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
        si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
        si.dwFlags |= STARTF_USESTDHANDLES;
    }

    // Create the process. Enclose the actual command in quotes.
    ok = ok && CreateProcess(
        nullptr,  // command might contain whitespace, pass it quoted in arg 2 instead.
        AutoString("\"" + command + "\" " + args),
        nullptr,  // Process handle not inheritable
        nullptr,  // Thread handle not inheritable
        true,     // handles are inherited
        0,        // No creation flags
        nullptr,  // Use parent's environment block
        nullptr,  // Use parent's starting directory
        &si,      // Pointer to STARTUPINFO
        &_pi);    // Pointer to PROCESS_INFORMATION

    // Something went wrong? Well, bad.
    if (!ok) {
        _error = GetLastError();
    }

    // Close the handles that have been inherited by the child process
    // and to which we don't need access to, otherwise they will not
    // close when the child exits.
    _stdin.r.Close();
    _stdout.w.Close();
    _stderr.w.Close();
}

And this is how I read from the standard output (_stdout.r):

UInt Read(UInt num_bytes, char* buffer) {
    if (!_stdout.r) return 0;
    DWORD bytes_read = 0;
    if (!ReadFile(_stdout.r.Get(), buffer, num_bytes - 1, &bytes_read, nullptr)) {
        _error = GetLastError();
        ConsoleOut("[ERROR]: ReadFile() : " + String::IntToString((Int32) _error));
        if (_error == ERROR_BROKEN_PIPE) {
            ConsoleOut("No Wait, the Pipe is just broken.");
            _error = 0;  // that's fine
        }
        return 0;
    }
    buffer[bytes_read] = '\0';
    return bytes_read;
}

When I comment out the last lines of the Popen constructor (closing the pipe handles that are not used from the parent process) ReadFile() blocks forever. With these lines enabled, the Pipe is always immediately broken (the child process exits pretty quickly).

Question

  • Can someone see what is wrong in my code/logic?
  • If not, I would already appreciate if there is a complete working example of opening a child process and reading its output

Wrapper Interface

struct Handle {
    HANDLE h;
    explicit Handle();
    explicit Handle(HANDLE h);
    Handle(Handle&& other);
    Handle& operator = (Handle&& other);
    ~Handle();
    void Close();
    HANDLE Get();
    HANDLE Release();
    Handle Duplicate(DWORD options = DUPLICATE_SAME_ACCESS, HANDLE src_proc = nullptr, HANDLE dst_proc = nullptr) const;
    DWORD GetInfo() const;                        // uses GetHandleInformation
    void SetInheritable(bool inheritable) const;  // uses SetHandleInformation
    bool GetInheritable() const;
    operator bool() const;

    explicit Handle(const Handle&) = delete;
    Handle* operator = (const Handle&) = delete;
};
struct Pipe {
    Handle r, w;
    DWORD error;
    explicit Pipe();
    explicit Pipe(bool inheritable);
    Pipe(Pipe&& other);
    Pipe& operator = (Pipe&& other);
    ~Pipe();
    void Close();
    Pipe Duplicate(DWORD options = DUPLICATE_SAME_ACCESS, HANDLE src_proc = nullptr, HANDLE dst_proc = nullptr) const;
    operator bool() const;
    explicit Pipe(const Pipe&) = delete;
    Pipe* operator = (const Pipe&) = delete;
};
1
Are you sure the child process is in fact writing to standard output? Have you tried using command-line redirection, child.exe > output.txt to see whether that works? - Harry Johnston
These lines are probably trouble, there's a temporary Pipe object being destroyed immediately: _stdin = Pipe(true); _stdout = Pipe(true); But you haven't shown the important code. - Ben Voigt

1 Answers

1
votes

Without using either threads or overlapped I/O, you risk deadlock. The child process could be trying to read from its stdin or waiting for space in its stdout buffer so it can write, you cannot tell which, and when you choose wrong, you get the observed behavior. The blocking read on the child's output means you guessed wrong, and it is actually waiting for input.

Read Raymond Chen's blog article Be careful when redirecting both a process's stdin and stdout to pipes, for you can easily deadlock which I also linked in your earlier question today. It specifically calls out the horrible brokenness in the very same sample you linked in your question.