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

标签 winapi pipe readfile child-process io-redirection

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

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

If an anonymous pipe is being used and the write handle has been closed, when ReadFile attempts to read using the pipe's corresponding read handle, the function returns FALSE and GetLastError returns 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;
}

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;
}

(以下文字原为 edit 2 之前)

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

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

最佳答案

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

阅读 MSDN 了解更多详情:

Creating a Child Process with Redirected Input and Output

The case of the redirected standard handles that won’t close even though the child process has exited

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

关于winapi - ReadFile 在从子进程结束后读取 stdout 时不返回,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54416116/

相关文章:

c - 如何在 SYSTEM 下运行应用程序?

c++ - 为什么 Windows API 根据我的日志记录语句响应错误代码

perl - 如何正确处理 File::Slurp read_file 上的错误?

c# - 如何从 powerpoint 中读取数据

c# - 读取文本文件并检查列数对于所有行都是相同的

file - 将一个文件夹的内容 move 到另一个文件夹

c++ - Windows 中套接字发送缓冲区的大小是多少?

xcode - 可执行管道输入?

c - c中的 fork 和管道过程

date - Angular 2 日期管道添加或子小时