问题描述:

OK, so the basic task for me is to create function that will run a process and grab console output (stdout and stderr) from it with possibility to specify created process execution timeout.

First approach was to create temporary file (with FILE_FLAG_DELETE_ON_CLOSE) and specify it as hStdOutput/hStdError for STARTUPINFO, wait for process and read file's content. This works fine. But I don't like the idea of temporary file creation. I found the example from msdn with redirecting console output using pipes (see here). Here is rewritten/fixed code (as command line tool for tests):

#include <Windows.h>

#include <tchar.h>

#include <cassert>

#include <string>

#include <stdexcept>

#include <memory>

#include <type_traits>

#include <iostream>

using string_t = std::basic_string<TCHAR>;

#define THROW_E(X) throw std::runtime_error{(X) + std::string(": ") + std::to_string(::GetLastError())};

#define THROW(X) throw std::runtime_error{(X)};

namespace {

// From

// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx

void ArgvQuote(

const string_t& Argument,

string_t& CommandLine,

bool Force)

{

if (Force == false &&

Argument.empty () == false &&

Argument.find_first_of (_T(" \t\n\v\"")) == Argument.npos)

{

CommandLine.append (Argument);

}

else {

CommandLine.push_back (_T('"'));

for (auto It = Argument.begin () ; ; ++It) {

unsigned NumberBackslashes = 0;

while (It != Argument.end () && *It == _T('\\')) {

++It;

++NumberBackslashes;

}

if (It == Argument.end ()) {

CommandLine.append (NumberBackslashes * 2, _T('\\'));

break;

}

else if (*It == _T('"')) {

CommandLine.append (NumberBackslashes * 2 + 1, _T('\\'));

CommandLine.push_back (*It);

}

else {

CommandLine.append (NumberBackslashes, _T('\\'));

CommandLine.push_back (*It);

}

}

CommandLine.push_back (_T('"'));

}

}

using handle_ptr = std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&::CloseHandle)>;

handle_ptr CreateHandlePtr(HANDLE h)

{

return handle_ptr{h, &::CloseHandle};

}

handle_ptr CreateChildProcess(string_t cmd_line, handle_ptr&& std_out, const string_t& dir)

{

assert(!cmd_line.empty());

assert(std_out.get());

PROCESS_INFORMATION pi = {};

STARTUPINFO si = {};

si.cb = sizeof(STARTUPINFO);

si.hStdError = std_out.get();

si.hStdOutput = std_out.get();

si.wShowWindow = SW_HIDE;

si.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;

LPTSTR p_cmd_line = &cmd_line[0];

auto created = CreateProcess(NULL,

p_cmd_line,

nullptr,

nullptr,

TRUE, // Handles are inherited

CREATE_NEW_CONSOLE,

NULL, // Use parent's environment

dir.empty() ? nullptr : dir.c_str(),

&si,

&pi);

if(!created)

THROW_E("CreateProcess()");

std_out.reset();

::CloseHandle(pi.hThread);

return CreateHandlePtr(pi.hProcess);

}

std::string ReadFromPipe(handle_ptr&& std_out)

{

std::string content;

DWORD read = 0;

CHAR buf[1 * 1024] = {};

BOOL success = FALSE;

for(;;)

{

success = ::ReadFile(std_out.get(), buf, _countof(buf), &read, nullptr);

if(!success && (::GetLastError() != ERROR_BROKEN_PIPE))

THROW_E("ReadFile()");

if(!success || (read == 0))

break;

content.append(buf, read);

}

return content;

}

} // namespace

std::string ConsoleOutFromExec(const string_t& cmd_line, std::size_t timeout_msecs = INFINITE,

const string_t& directory = string_t{})

{

auto child_stdout_r = CreateHandlePtr(nullptr);

auto child_stdout_w = CreateHandlePtr(nullptr);

{

SECURITY_ATTRIBUTES sa = {};

sa.nLength = sizeof(SECURITY_ATTRIBUTES);

sa.bInheritHandle = TRUE;

sa.lpSecurityDescriptor = nullptr;

HANDLE raw_child_stdout_r = nullptr;

HANDLE raw_child_stdout_w = nullptr;

if(!::CreatePipe(&raw_child_stdout_r, &raw_child_stdout_w, &sa, 0))

THROW_E("CreatePipe()");

child_stdout_r = CreateHandlePtr(raw_child_stdout_r);

child_stdout_w = CreateHandlePtr(raw_child_stdout_w);

if(!::SetHandleInformation(child_stdout_r.get(), HANDLE_FLAG_INHERIT, 0))

THROW_E("SetHandleInformation()");

}

auto child_proc = CreateChildProcess(cmd_line, std::move(child_stdout_w), directory);

auto status = ::WaitForSingleObject(child_proc.get(), timeout_msecs);

switch(status)

{

case WAIT_OBJECT_0:

break;

default:

THROW("WaitForSingleObject()");

}

return ReadFromPipe(std::move(child_stdout_r));

}

std::size_t ParseTimeout(const string_t& str)

{

std::size_t timeout = INFINITE;

if(str == _T("INFINITE"))

return timeout;

auto p_begin = str.c_str();

TCHAR* p_end = nullptr;

timeout = static_cast<std::size_t>(_tcstol(p_begin, &p_end, 10));

if((timeout == 0) || !p_end || (*p_end != _T('\0')))

THROW("Invalid timeout string");

return timeout;

}

int _tmain(int argc, TCHAR* argv[])

{

if(argc <= 2)

{

std::cout << "Invalid command line:" << "\n";

std::cout << "\t" << "<timeout msecs> (or INFINITE) <exe> <arg0> <arg1> ..." << "\n";

return EXIT_FAILURE;

}

try {

std::size_t timeout = ParseTimeout(argv[1]);

string_t cmd_line;

if(_tcslen(argv[2]) == 0)

THROW("Executable path is empty");

for(int i = 2; i < argc; ++i)

{

#if(1)

ArgvQuote(argv[i], cmd_line, true);

#else

cmd_line.append(argv[i]);

#endif

if(i < (argc - 1))

cmd_line.push_back(_T(' '));

}

auto content = ConsoleOutFromExec(cmd_line, timeout);

std::cout << content << "\n";

} catch(const std::exception& e) {

std::cout << "EXCEPTION: " << e.what() << "\n";

return EXIT_FAILURE;

}

return EXIT_SUCCESS;

}

As you can see there is nothing new (in comparison to the link above), just using RAII for more clear code and without stdin redirection (ArgvQuote() is from this link).

Next, if I will run:

  • child_read.exe INFINITE cmd.exe /c whoami
  • child_read.exe INFINITE cmd.exe /c ipconfig

everything works fine. But next command:

  • child_read.exe INFINITE cmd.exe /c ipconfig /all

will freeze execution. Debugger shows that the problem is that ::WaitForSingleObject() never returns. And, actually, ipconfig process alive:

This happens for Windows 8.1 and above, but on Windows 7 everything works fine without ipconfig freeze !! Why ?

Btw, If I remove waiting for process and start reading from pipe - everything works and I will have application output (but I need to have execution timeout)!

Can someone explain why this happens ? is this relative to ipconfig /all command only or I have something wrong in the code and this can happen with any other application too ?

(Everything that I found in the google/stackoverflow is relative to stuck if stdin is redirected too...)

网友答案:

Many thanks to @Cheers and hth. - Alf. As he said in the comments:

You need to replace the infinite wait wait a wait-and-read loop. A pipe doesn't have an infinite buffer. –

To avoid ::ReadFile() block if there is no data on child process console, need to use PeekNamedPipe() function, that will tell us how many bytes available in the pipe (child process console, in our case). So, here is fixed ReadFromPipe() function from my question:

std::string ReadFromPipe(
    handle_ptr&& std_out, // handle to child console
    handle_ptr&& wait_on, // handle to child process
    std::size_t timeout_msecs,
    bool* timeout)
{
    std::string content;
    DWORD read = 0;
    DWORD bytes_available = 0;
    DWORD total_read = 0;
    CHAR buf[1 * 1024] = {};
    BOOL done = FALSE;
    std::size_t current_waittime = 0;

    while(!done)
    {
        bytes_available = static_cast<DWORD>(-1);
        switch(::WaitForSingleObject(wait_on.get(), 1))
        {
        case WAIT_OBJECT_0:
            break;
        case WAIT_TIMEOUT:
            if(!::PeekNamedPipe(std_out.get(), nullptr, 0, nullptr, &bytes_available, nullptr) &&
                (::GetLastError() != ERROR_BROKEN_PIPE))
                    THROW_E("PeekNamedPipe()");
            break;
        }

        total_read = 0;
        while(total_read < bytes_available)
        {
            DWORD count = (std::min)(static_cast<DWORD>(_countof(buf)), static_cast<DWORD>(bytes_available - total_read));
            if(!::ReadFile(std_out.get(), buf, count, &read, nullptr) &&
                (::GetLastError() != ERROR_BROKEN_PIPE))
                THROW_E("ReadFile()");
            else if(read == 0)
            {
                done = TRUE;
                break;
            }

            content.append(buf, read);
            total_read += read;
        }

        if(++current_waittime >= timeout_msecs)
        {
            *timeout = true;
            content.clear();
            break;
        }
    }

    return content;
}

And here is all code together:

#include <Windows.h>
#include <tchar.h>

#include <cassert>

#include <string>
#include <stdexcept>
#include <memory>
#include <type_traits>
#include <iostream>
#include <algorithm>

using string_t = std::basic_string<TCHAR>;

#define THROW_E(X) THROW((X) + std::string(": ") + std::to_string(::GetLastError()))
#define THROW(X) throw std::runtime_error{(X)}

namespace {

using handle_ptr = std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&::CloseHandle)>;

handle_ptr CreateHandlePtr(HANDLE h)
{
    return handle_ptr{h, &::CloseHandle};
}

handle_ptr CreateChildProcess(string_t cmd_line, handle_ptr&& std_out, const string_t& dir)
{
    assert(!cmd_line.empty());
    assert(std_out.get());

    PROCESS_INFORMATION pi = {};
    STARTUPINFO si = {};
    si.cb = sizeof(STARTUPINFO);
    si.hStdError = std_out.get();
    si.hStdOutput = std_out.get();
    si.wShowWindow = SW_HIDE;
    si.dwFlags |= STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;

    LPTSTR p_cmd_line = &cmd_line[0];
    auto created = CreateProcess(NULL, 
        p_cmd_line,
        nullptr,
        nullptr,
        TRUE, // Handles are inherited 
        CREATE_NEW_CONSOLE,
        NULL, // Use parent's environment 
        dir.empty() ? nullptr : dir.c_str(),
        &si,
        &pi);
    if(!created)
        THROW_E("CreateProcess()");

    std_out.reset();
    ::CloseHandle(pi.hThread);
    return CreateHandlePtr(pi.hProcess);
}

std::string ReadFromPipe(
    handle_ptr&& std_out, // handle to child console
    handle_ptr&& wait_on, // handle to child process
    std::size_t timeout_msecs,
    bool* timeout)
{
    std::string content;
    DWORD read = 0;
    DWORD bytes_available = 0;
    DWORD total_read = 0;
    CHAR buf[1 * 1024] = {};
    BOOL done = FALSE;
    std::size_t current_waittime = 0;

    while(!done)
    {
        bytes_available = static_cast<DWORD>(-1);
        switch(::WaitForSingleObject(wait_on.get(), 1))
        {
        case WAIT_OBJECT_0:
            break;
        case WAIT_TIMEOUT:
            if(!::PeekNamedPipe(std_out.get(), nullptr, 0, nullptr, &bytes_available, nullptr) &&
                (::GetLastError() != ERROR_BROKEN_PIPE))
                    THROW_E("PeekNamedPipe()");
            break;
        }

        total_read = 0;
        while(total_read < bytes_available)
        {
            DWORD count = (std::min)(static_cast<DWORD>(_countof(buf)), static_cast<DWORD>(bytes_available - total_read));
            if(!::ReadFile(std_out.get(), buf, count, &read, nullptr) &&
                (::GetLastError() != ERROR_BROKEN_PIPE))
                THROW_E("ReadFile()");
            else if(read == 0)
            {
                done = TRUE;
                break;
            }

            content.append(buf, read);
            total_read += read;
        }

        if(++current_waittime >= timeout_msecs)
        {
            *timeout = true;
            content.clear();
            break;
        }
    }

    return content;
}

} // namespace

std::string ConsoleOutFromExec(const string_t& cmd_line, std::size_t timeout_msecs = INFINITE,
    const string_t& directory = string_t{})
{
    auto child_stdout_r = CreateHandlePtr(nullptr);
    auto child_stdout_w = CreateHandlePtr(nullptr);
    {
        SECURITY_ATTRIBUTES sa = {};
        sa.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa.bInheritHandle = TRUE;
        sa.lpSecurityDescriptor = nullptr;

        HANDLE raw_child_stdout_r = nullptr;
        HANDLE raw_child_stdout_w = nullptr;
        if(!::CreatePipe(&raw_child_stdout_r, &raw_child_stdout_w, &sa, 0))
            THROW_E("CreatePipe()");
        child_stdout_r = CreateHandlePtr(raw_child_stdout_r);
        child_stdout_w = CreateHandlePtr(raw_child_stdout_w);

        if(!::SetHandleInformation(child_stdout_r.get(), HANDLE_FLAG_INHERIT, 0))
            THROW_E("SetHandleInformation()");
    }

    auto child_proc = CreateChildProcess(cmd_line, std::move(child_stdout_w), directory);

    bool timeout = false;
    auto content = ReadFromPipe(std::move(child_stdout_r), std::move(child_proc), timeout_msecs, &timeout);
    if(timeout)
        THROW("Execution timeout");
    return content;
}

std::size_t ParseTimeout(const string_t& str)
{
    std::size_t timeout = INFINITE;
    if(str == _T("INFINITE"))
        return timeout;
    auto p_begin = str.c_str();
    TCHAR* p_end = nullptr;
    timeout = static_cast<std::size_t>(_tcstol(p_begin, &p_end, 10));
    if((timeout == 0) || !p_end || (*p_end != _T('\0')))
        THROW("Invalid timeout string");
    return timeout;
}

int _tmain(int argc, TCHAR* argv[])
{
    if(argc <= 2)
    {
        std::cout << "Invalid command line:" << "\n";
        std::cout << "\t" << "<timeout msecs> (or INFINITE) <exe> <arg0> <arg1> ..." << "\n";
        return EXIT_FAILURE;
    }

    try {
        std::size_t timeout = ParseTimeout(argv[1]);
        string_t cmd_line;
        if(_tcslen(argv[2]) == 0)
            THROW("Executable path is empty");

        for(int i = 2; i < argc; ++i)
        {
            // TODO: quote arguments correctly
            cmd_line.append(argv[i]);
            if(i < (argc - 1))
                cmd_line.push_back(_T(' '));
        }

        auto content = ConsoleOutFromExec(cmd_line, timeout);
        std::cout << content << "\n";
    } catch(const std::exception& e) {
        std::cout << "EXCEPTION: " << e.what() << "\n";
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Again, many thanks to @Cheers and hth. - Alf for the help !

相关阅读:
Top