【问题标题】:Reading from a pipe randomly fails从管道读取随机失败
【发布时间】:2016-01-24 02:29:00
【问题描述】:

我正在为命令行可执行文件编写集成测试驱动程序。我同时控制驱动程序和可执行文件,因此我可以保证它们的行为——例如,可执行文件从不从标准输入读取,它只接受命令行参数,执行它的操作,然后将输出写入文件和标准输出。

我希望同时捕获进程的退出代码和标准输出以进行验证。

这是我正在使用的代码:

#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 会失败 - 每次都不同。我在调试器中查看了结果,标准输出在中途被切断。如果我不尝试捕获标准输出,那么所有测试每次都会成功,所以我知道它是基于标准输出捕获的。

指定了超时,但它是一个非常大的 60 秒 - 比正常运行测试所需的时间长得多。我为每个测试生成一个新进程。

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

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

【问题讨论】:

    标签: c++ windows


    【解决方案1】:

    我对此有一个理论,但我并不完全确定。关键在于读取进程stdout的循环条件。

    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 可能会在它有我们要求的大块之前返回,即使管道没有终止。这将导致输入读取循环终止。

    由于父级不再读取标准输出,尝试写入标准输出的子级可能会阻塞等待某人清除缓冲区——实际上是死锁,因为没有人会这样做。因此,超时将触发并终止进程,记录失败。

    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;
    

    到目前为止,我无法通过此循环重现故障(并且测试完成的速度明显更快)。

    【讨论】:

    • 是的,管道上的 ReadFile() 将始终在到达相应的 WriteFile() 末尾时返回。除非写入的长度与缓冲区大小一致,否则即使管道尚未关闭,我也 认为 即使仍有稍后写入的待处理数据。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-13
    • 1970-01-01
    • 2011-10-26
    • 1970-01-01
    • 1970-01-01
    • 2023-03-06
    • 1970-01-01
    相关资源
    最近更新 更多