从管道中随机读取失败

时间:2016-01-23 16:54:23

标签: c++ windows

我正在为命令行可执行文件编写集成测试驱动程序。我控制驱动程序和可执行文件,所以我可以保证他们的行为 - 例如,可执行文件永远不会从stdin读取,它只需要命令行参数,执行它的操作,然后将输出写入文件和标准输出。

我希望捕获退出代码和验证过程的标准输出。

以下是我使用的代码:

#include <Windows.h>

class Pipe {
    HANDLE ReadHandle;
    HANDLE writehandle;
public:
    Pipe() {
        SECURITY_ATTRIBUTES saAttr;
        saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
        saAttr.bInheritHandle = TRUE;
        saAttr.lpSecurityDescriptor = NULL;
        CreatePipe(&ReadHandle, &writehandle, &saAttr, 0);
    }
    HANDLE WriteHandle() {
        return writehandle;
    }
    std::string Contents() {
        CloseHandle(writehandle);
        DWORD dwRead;
        CHAR chBuf[1024];
        BOOL bSuccess = FALSE;

        std::string result;
        for (;;)
        {
            bSuccess = ReadFile(ReadHandle, chBuf, 1024, &dwRead, NULL);
            if (!bSuccess) break;
            result += std::string(chBuf, chBuf + dwRead);
            if (dwRead < 1024)
                break;
        }
        return result;
    }
    ~Pipe() {
        CloseHandle(ReadHandle);
    }
};
Wide::Driver::ProcessResult Wide::Driver::StartAndWaitForProcess(std::string name, std::vector<std::string> args, Util::optional<unsigned> timeout)
{
    ProcessResult result;
    Pipe stdoutpipe;
    PROCESS_INFORMATION info = { 0 };
    STARTUPINFO startinfo = { sizeof(STARTUPINFO) };
    std::string final_args = name;
    for (auto arg : args)
         final_args += " " + arg;
    startinfo.hStdOutput = stdoutpipe.WriteHandle();
    startinfo.hStdError = INVALID_HANDLE_VALUE;
    startinfo.hStdInput = INVALID_HANDLE_VALUE;
    startinfo.dwFlags |= STARTF_USESTDHANDLES;
    auto proc = CreateProcess(
        name.c_str(),
        &final_args[0],
        nullptr,
        nullptr,
        TRUE,
        NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW,
        nullptr,
        nullptr,
        &startinfo,
        &info
         );
    if (!proc) {
        DWORD dw = GetLastError();
        const char* message;
        FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
            nullptr, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&message, 0, nullptr);
        std::string err = message;
        LocalFree((void*)message);
        throw std::runtime_error(err);        
    }
    if (timeout == 0)
        timeout = INFINITE;

    result.std_out = stdoutpipe.Contents();
    if (WaitForSingleObject(info.hProcess, timeout ? *timeout : INFINITE) == WAIT_TIMEOUT)
         TerminateProcess(info.hProcess, 1);

    DWORD exit_code;
    GetExitCodeProcess(info.hProcess, &exit_code);
    CloseHandle(info.hProcess);
    CloseHandle(info.hThread);
    result.exitcode = exit_code;
    if (exit_code != 0)
        return result;
    return result;
}

我以这种方式运行了259次集成测试。有些人需要比其他人更长当我运行套件时,大约1-3会失败 - 每次都不同。我已经查看了调试器中的结果,并且stdout在中途被切断了。如果我没有尝试捕获标准输出,那么所有测试每次都会成功,所以我知道它基于stdout捕获。

指定了超时,但它是一个非常慷慨的60秒 - 比通常运行的测试要长得多。我为每个测试产生了一个新的过程。

如何以更可靠的方式捕获stdout,而不会出现随机故障?

作为最后一点,运行套件以在调试器中捕获故障需要很长时间,因此可能需要一段时间来处理任何请求以获取更多信息。

1 个答案:

答案 0 :(得分:1)

我对此有一个理论,但我并不完全确定。关键是在循环条件下读取进程的标准输出。

std::string result;
for (;;)
{
    bSuccess = ReadFile(ReadHandle, chBuf, 1024, &dwRead, NULL);
    if (!bSuccess) break;
    result += std::string(chBuf, chBuf + dwRead);
    if (dwRead < 1024)
        break;
}
return result;

这里实际上是一个隐含的假设。 ReadFile是一个阻止API,因此我们假设它一直阻塞,直到它有我们要求的数据或输入结束。但我假设事实上,ReadFile可能会在它有一个我们要求的块之前返回,即使管道没有终止。这将导致输入读取循环终止。

由于父级不再读取stdout,因此尝试编写stdout的子进程可能会阻止等待某人清除缓冲区 - 因为没有人会这样做会导致死锁。因此,超时将触发并终止进程,记录失败。

MSDN文档说:

The ReadFile function returns when one of the following conditions occur:

    The number of bytes requested is read.
    A write operation completes on the write end of the pipe.
    An asynchronous handle is being used and the read is occurring asynchronously.
    An error occurs.

表示当写操作完成并且所请求的字节数可用时它将返回。事实上,它没有对写入操作做出任何评论,因为它会产生您请求的字节数。因此,它有效地表现为半异步,即使在同步调用时也是如此。

我已按如下方式重写了循环:

std::string result;
for (;;)
{
    bSuccess = ReadFile(ReadHandle, chBuf, 1024, &dwRead, NULL);
    if (!bSuccess || dwRead == 0) break;
    result += std::string(chBuf, chBuf + dwRead);
}
return result;

到目前为止,我一直无法使用此循环重现失败(并且测试完成得非常快)。