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(接受答案后):它确实有效!请参阅我对已接受答案的最后评论。
您没有正确管理管道,或者更具体地说,您没有控制管道句柄的继承。不要让子进程继承管道 ( output_pipes[0]) 的读取句柄,否则当子进程结束时管道将无法正确断开。
阅读 MSDN 了解更多详细信息:
\n\n\n\n即使子进程已退出,重定向的标准句柄也不会关闭\xe2\x80\x99t
\n\n使用SetHandleInformation()或PROC_THREAD_ATTRIBUTE_LIST来防止CreateProcess()将output_pipes[0]句柄作为可继承句柄传递给子进程。子进程不需要访问该句柄,因此无论如何都不需要将其传递到进程边界。它只需要访问管道的写入句柄 ( output_pipes[1])。