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

问题描述 投票:0回答:2

我正在开发我的库,它需要捕获和处理子进程运行时的标准输出(和错误)。当使用 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;
}

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

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

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

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

winapi pipe readfile child-process io-redirection
2个回答
5
投票

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

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

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

即使子进程退出,重定向的标准句柄也不会关闭的情况

使用

SetHandleInformation()

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


1
投票

我可以重现这个

ReadFile

问题,如果在设置子进程的

hStdOutput
hStdError
后关闭写入处理程序,则
ReadFile
将在子进程退出后返回 0。

这是我的代码示例, 父.cpp:

#include <windows.h> #include <iostream> #include <stdio.h> HANDLE childInRead = NULL; HANDLE W1 = NULL; HANDLE W2 = NULL; HANDLE R2 = NULL; HANDLE R1 = NULL; #define BUFSIZE 4096 void CreateChildProcess() { TCHAR applicationName[] = TEXT("kids.exe"); PROCESS_INFORMATION pi; STARTUPINFO si; BOOL success = FALSE; ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); si.hStdError = W1; si.hStdOutput = W1; si.hStdInput = R2; si.dwFlags |= STARTF_USESTDHANDLES; success = CreateProcess(NULL, applicationName, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); if (!success) { printf("Error creating child process \n"); } else { printf("Child process successfuly created \n"); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } } int main() { printf("Parent process running.... \n"); DWORD dRead, dWritten; CHAR chBuf[BUFSIZE] = { 0 }; BOOL bSuccess = FALSE; SECURITY_ATTRIBUTES secAttr; secAttr.nLength = sizeof(SECURITY_ATTRIBUTES); secAttr.bInheritHandle = TRUE; secAttr.lpSecurityDescriptor = NULL; printf("Creating first pipe \n"); if (!CreatePipe(&R1, &W1, &secAttr, 0)) { printf("\n error creating first pipe \n"); } printf("Creating second pipe \n"); if (!CreatePipe(&R2, &W2, &secAttr, 0)) { printf("\n error creating second pipe \n"); } if (!SetHandleInformation(R1, HANDLE_FLAG_INHERIT, 0)) { printf("\n R1 SetHandleInformation \n"); } if (!SetHandleInformation(W2, HANDLE_FLAG_INHERIT, 0)) { printf("\n W1 SetHandleInformation \n"); } printf("\n Creating child process..... \n"); HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); CreateChildProcess(); CloseHandle(W1); CloseHandle(R2); for (;;) { printf("Inside for loop \n"); //1. read from stdin printf("read from stdin:\n"); bSuccess = ReadFile(hStdIn, chBuf, BUFSIZE, &dRead, NULL); if (!bSuccess) { printf("error reading \n"); break; } //2. write to Pipe2 printf("write to Pipe2...\n"); bSuccess = WriteFile(W2, chBuf, 100, &dWritten, NULL); if (!bSuccess) { printf("error reading \n"); break; } //3. read from Pipe1 printf("read from Pipe1...\n"); bSuccess = ReadFile(R1, chBuf, BUFSIZE, &dRead, NULL); if (!bSuccess) { printf("error reading :%d \n", GetLastError()); break; } //4. write to stdout printf("write to stdout:\n"); bSuccess = WriteFile(hStdOut, chBuf, 100, &dWritten, NULL); if (!bSuccess) { printf("error reading \n"); break; } } getchar(); return 0; }

儿童.cpp:

#include <windows.h> #include <stdio.h> #define BUFSIZE 4096 int main() { DWORD dRead, dWritten; CHAR chBuf[BUFSIZE]; BOOL success = FALSE; HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE); HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE); printf("Child process running...."); if (stdIn == INVALID_HANDLE_VALUE || stdOut == INVALID_HANDLE_VALUE) { ExitProcess(1); } //for (;;) { success = ReadFile(stdIn, chBuf, BUFSIZE, &dRead, NULL); //if (!success || dRead == 0) break; success = WriteFile(stdOut, chBuf, dRead, &dWritten, NULL); //if (!success) break; //} return 0; }

	
© www.soinside.com 2019 - 2024. All rights reserved.