ReadFile 在结束后从子进程读取 stdout 时不返回

The*_*der 3 winapi pipe child-process io-redirection readfile

我正在开发我的库,它需要捕获和处理子进程运行时的标准输出(和错误)。当使用 ReadFile 读取输出时会出现问题,一旦进程结束(被杀死或退出),它就不会返回。

看起来ReadFile无法检测到管道的另一端(写入句柄)已关闭。根据文档,它应该返回FALSE并将最后一个错误设置为ERROR_BROKEN_PIPE

如果正在使用匿名管道并且写入句柄已关闭,则当 ReadFile 尝试使用管道相应的读取句柄进行读取时,该函数将返回 FALSE,并且 GetLastError 将返回 ERROR_BROKEN_PIPE。

这是我的代码,我已经删除了不相关的部分:(注意:我已经更新了以allium_start遵循建议的更改,我保留原始代码以供参考,请使用较新的功能代码来查找缺陷)

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
    // Prepare startup info with appropriate information
    SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
    instance->startup_info.dwFlags = STARTF_USESTDHANDLES;

    SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};

    HANDLE pipes[2];
    if (output_pipes == NULL) {
        CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
        output_pipes = pipes;
    }
    instance->startup_info.hStdOutput = output_pipes[1];
    instance->startup_info.hStdError = output_pipes[1];
    instance->stdout_pipe = output_pipes[0]; // Stored for internal reference

    // Create the process
    bool success = CreateProcessA(
        NULL,
        cmd,
        NULL,
        NULL,
        config ? true : false,
        0,
        NULL,
        NULL,
        &instance->startup_info,
        SecureZeroMemory(&instance->process, sizeof instance->process)
    );

    // Return on failure
    if (!success) return false;
}

char *allium_read_stdout_line(struct TorInstance *instance) {
    char *buffer = instance->buffer.data;

    // Process the input
    unsigned int read_len = 0;
    while (true) {
        // Read data
        unsigned long bytes_read;
        if (ReadFile(instance->stdout_pipe, buffer, 1, &bytes_read, NULL) == false || bytes_read == 0) return NULL;

        // Check if we have reached end of line
        if (buffer[0] == '\n') break;

        // Proceed to the next character
        ++buffer; ++read_len;
    }

    // Terminate the new line with null character and return
    // Special handling for Windows, terminate at CR if present
    buffer[read_len >= 2 && buffer[-1] == '\r' ? -1 : 0] = '\0';

    return instance->buffer.data;
}
Run Code Online (Sandbox Code Playgroud)

创建allium_start用于输出重定向的管道(它对 stdout 和 stderr 使用相同的管道来获取合并流),然后创建子进程。另一个allium_read_stdout_line函数负责读取管道的输出并在遇到新行时返回它。

问题发生在ReadFile函数调用处,如果进程退出后没有任何内容可读取,则它永远不会返回,根据我的理解,进程结束时 Windows 会关闭所有句柄,因此看起来无法ReadFile检测到这一事实另一端的管道(写句柄)已关闭。

我该如何解决?我一直在寻找解决方案,但到目前为止还没有找到,一个潜在的选择是使用多线程并放入ReadFile一个单独的线程,这样它就不会阻塞整个程序,通过使用该方法我可以检查是否当我等待读取完成时,进程仍然定期存在...或者如果进程消失则终止/停止线程。

我确实更喜欢解决问题而不是选择解决方法,但我愿意接受任何其他解决方案以使其发挥作用。提前致谢!


编辑:在阅读了 @RemyLebeau 的答案和 @RbMm 在该答案中的评论后,很明显我对句柄继承如何工作的理解从根本上是有缺陷的。因此,我将他们的建议(SetHandleInformation禁用读取句柄的继承并在创建子进程后关闭它)合并到我的allium_start函数中:

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
    // Prepare startup info with appropriate information
    SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
    instance->startup_info.dwFlags = STARTF_USESTDHANDLES;

    SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};

    HANDLE pipes[2];
    if (output_pipes == NULL) {
        CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
        output_pipes = pipes;
    }
    SetHandleInformation(output_pipes[0], HANDLE_FLAG_INHERIT, 0);
    instance->startup_info.hStdOutput = output_pipes[1];
    instance->startup_info.hStdError = output_pipes[1];
    instance->stdout_pipe = output_pipes[0]; // Stored for internal reference

    // Create the process
    bool success = CreateProcessA(
        NULL,
        cmd,
        NULL,
        NULL,
        config ? true : false,
        0,
        NULL,
        NULL,
        &instance->startup_info,
        SecureZeroMemory(&instance->process, sizeof instance->process)
    );

    // Close the write end of our stdout handle
    CloseHandle(output_pipes[1]);

    // Return on failure
    if (!success) return false;
}
Run Code Online (Sandbox Code Playgroud)

(下面的文字最初是在编辑2之前出现的)

但遗憾的是它仍然不起作用:(

编辑2(接受答案后):它确实有效!请参阅我对已接受答案的最后评论。

Rem*_*eau 5

您没有正确管理管道,或者更具体地说,您没有控制管道句柄的继承。不要让子进程继承管道 ( output_pipes[0]) 的读取句柄,否则当子进程结束时管道将无法正确断开。

\n\n

阅读 MSDN 了解更多详细信息:

\n\n

创建具有重定向输入和输出的子进程

\n\n

即使子进程已退出,重定向的标准句柄也不会关闭\xe2\x80\x99t

\n\n

使用SetHandleInformation()PROC_THREAD_ATTRIBUTE_LIST来防止CreateProcess()output_pipes[0]句柄作为可继承句柄传递给子进程。子进程不需要访问该句柄,因此无论如何都不需要将其传递到进程边界。它只需要访问管道的写入句柄 ( output_pipes[1])。

\n

  • 具体[msdn代码](https://learn.microsoft.com/en-us/windows/desktop/procthread/creating-a-child-process-with-redirected-input-and-output) `ReadFile( g_hChildStd_OUT_Rd ,`即使在子进程退出后也不会失败,因为我们没有关闭`g_hChildStd_OUT_Wr`。所以这里不关闭句柄不仅仅是临时(直到进程退出)资源泄漏,而是导致永远挂起。必须是最小的 `CloseHandle(g_hChildStd_OUT_Wr); CloseHandle (g_hChildStd_IN_Rd);` 在 `CreateProcess` 调用之后添加。只有在此之后我们才打破 `ReadFromPipe` 无限循环。但无论如何解决方案都很糟糕 (4认同)
  • @TheDcoder - 仅当另一端的**最后**句柄将关闭时,您才会收到“ERROR_BROKEN_PIPE”的读取错误。但在您的代码中,这永远不会是因为您也将此句柄保留在自我进程中。您不会在自身进程中关闭任何“pipes[2]” - 因此即使子进程退出并且它处理的所有内容都将被关闭,也不会出现“ERROR_BROKEN_PIPE” (3认同)
  • 确实[来自msdn的代码](https://learn.microsoft.com/en-us/windows/desktop/procthread/creating-a-child-process-with-redirected-input-and-output)非常糟糕和错误无论如何(不关闭**必须**关闭的句柄)。我不明白多少年才能以简单的错误代码为例。而且作为解决方案也效率不高 (3认同)