如何读取子进程的输出?

Posted

技术标签:

【中文标题】如何读取子进程的输出?【英文标题】:How can I read a child process's output? 【发布时间】:2012-06-03 00:13:38 【问题描述】:

我编写了一个函数,它试图通过管道读取子进程的命令行输出。这应该是the MSDN Creating a Child Process with Redirected Input and Output article 的一个简单子集,但我显然犯了某种错误。

下面的 ReadFile(...) 调用将永远阻塞,无论我是在 WaitForSingleObject(...) 调用之前还是之后放置它,这应该标志着子进程的结束。

我已阅读所有建议“使用异步 ReadFile”的答案,如果有人能给我一些想法如何在管道上完成,我愿意接受该建议。虽然我不明白为什么这种情况需要异步 I/O。

#include "stdafx.h"
#include <string>
#include <windows.h>

unsigned int launch( const std::string & cmdline );

int _tmain(int argc, _TCHAR* argv[])

    launch( std::string("C:/windows/system32/help.exe") );  
    return 0;


void print_error( unsigned int err )

    char* msg = NULL;
    FormatMessageA(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | 
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        err,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPSTR)&msg,
        0, NULL );
    std::cout << "------ Begin Error Msg ------" << std::endl;
    std::cout << msg << std::endl;
    std::cout << "------  End Error Msg  ------" << std::endl;

    LocalFree( msg );


unsigned int launch( const std::string & cmdline )

    TCHAR cl[_MAX_PATH*sizeof(TCHAR)]; 
    memset( cl, 0, sizeof(cl) );
    cmdline.copy( cl, (_MAX_PATH*sizeof(TCHAR)) - 1);    

    HANDLE stdoutReadHandle = NULL;
    HANDLE stdoutWriteHandle = NULL;

    SECURITY_ATTRIBUTES saAttr; 
    memset( &saAttr, 0, sizeof(saAttr) ); 
    saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 
    saAttr.bInheritHandle = TRUE; 
    saAttr.lpSecurityDescriptor = NULL; 

    // Create a pipe for the child process's STDOUT. 
    if ( ! CreatePipe(&stdoutReadHandle, &stdoutWriteHandle, &saAttr, 5000) )      
        throw std::runtime_error( "StdoutRd CreatePipe" ); 
    // Ensure the read handle to the pipe for STDOUT is not inherited.
    if ( ! SetHandleInformation(stdoutReadHandle, HANDLE_FLAG_INHERIT, 0) )
        throw std::runtime_error( "Stdout SetHandleInformation" ); 

    STARTUPINFO startupInfo; 
    memset( &startupInfo, 0, sizeof(startupInfo) ); 
    startupInfo.cb = sizeof(startupInfo);
    startupInfo.hStdError = stdoutWriteHandle;
    startupInfo.hStdOutput = stdoutWriteHandle;
    startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    startupInfo.dwFlags |= STARTF_USESTDHANDLES;

    char* rawEnvVars = GetEnvironmentStringsA();

    //__asm _emit 0xcc;

    PROCESS_INFORMATION processInfo;
    memset( &processInfo, 0, sizeof(processInfo) );

    std::cout << "Start [" << cmdline << "]" << std::endl;
    if ( CreateProcessA( 0, &cl[0], 0, 0, false, 
        CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, 
        rawEnvVars, 0, &startupInfo, &processInfo ) )
           
        //CloseHandle( stdoutWriteHandle );
        DWORD wordsRead;
        char tBuf[257] = '\0';
        bool success = true;
        std::string outBuf("");
        unsigned int t;
        while(success) 
            //__asm _emit 0xcc;
            std::cout << "Just before ReadFile(...)" << std::endl;
            success = ReadFile( stdoutReadHandle, tBuf, 256, &wordsRead, NULL);
            (t=GetLastError())?print_error(t):t=t;
            std::cout << "Just after ReadFile(...) | read " << wordsRead<< std::endl;
            std::cout << ".";
            if( success == false ) break;
            outBuf += tBuf;
            tBuf[0] = '\0';
        
        std::cout << "output = [" << outBuf << "]" << std::endl;


        if ( WaitForSingleObject( processInfo.hProcess, INFINITE ) == WAIT_OBJECT_0 )
        
            unsigned int exitcode = 0;
            GetExitCodeProcess( processInfo.hProcess, (LPDWORD)&exitcode );                
            std::cout << "exitcode = [" << exitcode << "]" << std::endl;

            //__asm _emit 0xcc;
            CloseHandle( processInfo.hProcess );
            CloseHandle( processInfo.hThread );

            return exitcode;
        
    
    else
    
        DWORD procErr = GetLastError();
        std::cout << "FAILED TO CREATE PROCESS!" << std::endl;
        print_error( procErr );
    
    return -1;
 // end launch()

【问题讨论】:

你试过popen/_popen吗? 第一次调用 ReadFile 或后续调用时会挂起吗? 回复:@Harry Johnston - 它第一次通过调用 ReadFile(...) 的循环挂起 【参考方案1】:

您的代码中有一些错误,但最重要的是您为 CreateProcessbInheritHandles 参数指定了 FALSE。如果新进程没有继承它的句柄,它就不能使用管道。为了继承句柄,bInheritHandles 参数必须是 TRUE并且句柄必须启用继承。

其他问题:

您指定了 CREATE_UNICODE_ENVIRONMENT,但传递了一个 ANSI 环境块。请注意,将NULL 传递给lpEnvironment 并让系统为您复制环境块更容易。在这种情况下,您仍应指定 CREATE_UNICODE_ENVIRONMENT,如文档中所述,因为您的环境块可能包含 Unicode 字符。

同样,如果您调用 CreateProcessA,您应该使用 STARTUPINFOA。

您不会在循环中每次都以零结尾 tBuf,因此您会在输出缓冲区中获得虚假的额外字符。

您需要在进入读取循环之前关闭stdoutWriteHandle,否则您将不知道子进程何时退出。 (或者您可以使用异步 IO 并显式检查进程退出。)

如果 API 函数成功,GetLastError() 是未定义的,所以你应该只在 ReadFile 返回 FALSE 时调用它。 (当然,在这种情况下,这纯粹是装饰性的,因为您没有对错误代码采取行动。)

作为参考,这是我对您的代码的更正版本。我已经把它变成了普通的 C(对不起!),因为那是我所熟悉的。我在 Unicode 模式下编译和测试过,但我认为它也应该在 ANSI 模式下无需修改。

#define _WIN32_WINNT _WIN32_WINNT_WIN7
#include <windows.h>
#include <stdio.h>

void launch(const char * cmdline_in)

    PROCESS_INFORMATION processInfo;
    STARTUPINFOA startupInfo; 
    SECURITY_ATTRIBUTES saAttr; 

    HANDLE stdoutReadHandle = NULL;
    HANDLE stdoutWriteHandle = NULL;

    char cmdline[256];
    char outbuf[32768];
    DWORD bytes_read;
    char tBuf[257];

    DWORD exitcode;

    strcpy_s(cmdline, sizeof(cmdline), cmdline_in);

    memset(&saAttr, 0, sizeof(saAttr));
    saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 
    saAttr.bInheritHandle = TRUE; 
    saAttr.lpSecurityDescriptor = NULL; 

    // Create a pipe for the child process's STDOUT. 
    if (!CreatePipe(&stdoutReadHandle, &stdoutWriteHandle, &saAttr, 5000))
    
        printf("CreatePipe: %u\n", GetLastError());
        return;
    

    // Ensure the read handle to the pipe for STDOUT is not inherited.
    if (!SetHandleInformation(stdoutReadHandle, HANDLE_FLAG_INHERIT, 0))
    
        printf("SetHandleInformation: %u\n", GetLastError());
        return;
    

    memset(&startupInfo, 0, sizeof(startupInfo));
    startupInfo.cb = sizeof(startupInfo);
    startupInfo.hStdError = stdoutWriteHandle;
    startupInfo.hStdOutput = stdoutWriteHandle;
    startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    startupInfo.dwFlags |= STARTF_USESTDHANDLES;

    // memset(&processInfo, 0, sizeof(processInfo));  // Not actually necessary

    printf("Starting.\n");

    if (!CreateProcessA(NULL, cmdline, NULL, NULL, TRUE,
        CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, NULL, 0, &startupInfo, &processInfo))
    
        printf("CreateProcessA: %u\n", GetLastError());
        return;
    

    CloseHandle(stdoutWriteHandle);

    strcpy_s(outbuf, sizeof(outbuf), "");

    for (;;) 
        printf("Just before ReadFile(...)\n");
        if (!ReadFile(stdoutReadHandle, tBuf, 256, &bytes_read, NULL))
        
            printf("ReadFile: %u\n", GetLastError());
            break;
        
        printf("Just after ReadFile, read %u byte(s)\n", bytes_read);
        if (bytes_read > 0)
        
            tBuf[bytes_read] = '\0';
            strcat_s(outbuf, sizeof(outbuf), tBuf);
        
    

    printf("Output: %s\n", outbuf);

    if (WaitForSingleObject(processInfo.hProcess, INFINITE) != WAIT_OBJECT_0)
    
        printf("WaitForSingleObject: %u\n", GetLastError());
        return;
    

    if (!GetExitCodeProcess(processInfo.hProcess, &exitcode))
    
        printf("GetExitCodeProcess: %u\n", GetLastError());
        return;
    

    printf("Exit code: %u\n", exitcode);

    CloseHandle( processInfo.hProcess );
    CloseHandle( processInfo.hThread );

    return;


int main(int argc, char** argv)

    launch("C:\\windows\\system32\\help.exe");
    return 0;

【讨论】:

你是正确的。 CreateProcess(...) 的 bInheritHandles 参数正是它。对不起其他杂物。其中大部分是愚蠢的调试和问题最小化垃圾。其余的只是不可原谅的草率。 为了将来参考,最安全的方法是使用PROC_THREAD_ATTRIBUTE_HANDLE_LIST 明确指定要继承的句柄。不幸的是,如果您提供明确的句柄列表,文档并不清楚您应该为bInheritHandles 传递什么;可能需要进行实验。 如何在 UNICODE 项目中做到这一点?【参考方案2】:

ReadFile() 有一个“LPOVERLAPPED lpOverlapped”参数,您已将其设置为 NULL。看起来唯一的方法是允许管道上的重叠 I/O,然后将 WaitForSingleObject() 用于“overlapped.hEvent”。

另一种方法是使用 ConnectNamedPipe 函数并为管道创建 OVERLAPPED 结构。

【讨论】:

以上是关于如何读取子进程的输出?的主要内容,如果未能解决你的问题,请参考以下文章

如何读取子进程标准输出的第一个字节,然后在 Python 中丢弃其余字节?

Python:如何写入子进程的标准输入并实时读取其输出

在子进程处于活动状态时读取子进程的输出

使用 BOOST 进程在单独的线程中读取子进程标准输出

从异步子进程读取流输出

读取Python中imagemagick子进程的输出[复制]