通过重叠和事件从控制台程序捕获输出

时间:2019-06-07 17:43:00

标签: winapi

我知道之前曾对此问题提出过许多类似的问题,但到目前为止,我还没有找到真正可行的解决方案。我想从我的程序启动一个控制台程序并捕获其输出。我的实现应采用与WaitForMultipleObjects()兼容的方式,即,我希望在管道中要读取新数据时得到通知。

我的实现基于MSDN上的this example。但是,我不得不对其进行一些修改,因为我需要重叠的I / O,以便能够等待ReadFile()完成。因此,我使用的是使用here中Dave Hart的MyCreatePipeEx()函数创建的命名管道。

这是我的实际代码。我出于可读性原因删除了错误检查。

HANDLE hReadEvent;
HANDLE hStdIn_Rd, hStdIn_Wr;
HANDLE hStdOut_Rd, hStdOut_Wr;
SECURITY_ATTRIBUTES saAttr; 
PROCESS_INFORMATION piProcInfo; 
STARTUPINFO siStartInfo;
OVERLAPPED ovl;
HANDLE hEvt[2];
DWORD mask, gotbytes;
BYTE buf[4097];

saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 
saAttr.bInheritHandle = TRUE; 
saAttr.lpSecurityDescriptor = NULL; 

MyCreatePipeEx(&hStdOut_Rd, &hStdOut_Wr, &saAttr, 0, FILE_FLAG_OVERLAPPED, FILE_FLAG_OVERLAPPED);        
MyCreatePipeEx(&hStdIn_Rd, &hStdIn_Wr, &saAttr, 0, FILE_FLAG_OVERLAPPED, FILE_FLAG_OVERLAPPED); 

SetHandleInformation(hStdOut_Rd, HANDLE_FLAG_INHERIT, 0);
SetHandleInformation(hStdIn_Wr, HANDLE_FLAG_INHERIT, 0);

memset(&piProcInfo, 0, sizeof(PROCESS_INFORMATION));
memset(&siStartInfo, 0, sizeof(STARTUPINFO));

siStartInfo.cb = sizeof(STARTUPINFO); 
siStartInfo.hStdError = hStdOut_Wr;
siStartInfo.hStdOutput = hStdOut_Wr;
siStartInfo.hStdInput = hStdIn_Rd;
siStartInfo.dwFlags |= STARTF_USESTDHANDLES;

CreateProcess(NULL, "test.exe", NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &piProcInfo);

hReadEvent = CreateEvent(NULL, TRUE, FALSE, NULL);      

for(;;) {

    int i = 0;

    hEvt[i++] = piProcInfo.hProcess;

    memset(&ovl, 0, sizeof(OVERLAPPED));
    ovl.hEvent = hReadEvent;

    if(!ReadFile(hStdOut_Rd, buf, 4096, &gotbytes, &ovl)) {     
        if(GetLastError() == ERROR_IO_PENDING) hEvt[i++] = hReadEvent;          
    } else {
        buf[gotbytes] = 0;
        printf("%s", buf);
    }               

    mask = WaitForMultipleObjects(i, hEvt, FALSE, INFINITE);

    if(mask == WAIT_OBJECT_0 + 1) {

        if(GetOverlappedResult(hStdOut_Rd, &ovl, &gotbytes, FALSE)) {                   
            buf[gotbytes] = 0;
            printf("%s", buf);
        }

    } else if(mask == WAIT_OBJECT_0) {

        break;
    }   
}

此代码的问题如下:如您所见,我正在使用ReadFile()读取4kb的块,因为我显然不知道外部程序test.exe将要处理多少数据输出。建议这样做here

  

要从客户端进程中读取可变数量的数据,只是发出   阅读您认为方便的任何大小的请求,并做好准备   处理比您要求的短的读取事件。别   将较短但非零的长度解释为EOF。继续阅读   请求,直到您读取零长度或错误。

但是,这不起作用。作为ReadFile()结构一部分传递给OVERLAPPED的事件对象仅在缓冲区中有4kb时才触发。如果外部程序仅打印“ Hello”,则该事件根本不会触发。 hReadEvent的缓冲区中必须有4kb才能真正触发。

所以我认为我应该逐字节读取,并修改程序以使用ReadFile(),如下所示:

if(!ReadFile(hStdOut_Rd, buf, 1, &gotbytes, &ovl)) {

但是,这也不起作用。如果我这样做,根本不会触发读取事件,这真的使我感到困惑。如果使用4096字节,则事件确实会在管道中有4096字节时立即触发,但是使用1字节时,该事件将完全无效。

那我应该怎么解决呢?我在这里几乎没有想法。每当管道中有一些新数据时,是否没有办法触发ReadFile()事件?不会那么困难,是吗?

1 个答案:

答案 0 :(得分:0)

仅作记录,虽然我的代码存在一些问题(请参阅OP下方注释中的讨论),但总的问题是,实际上不可能捕获任意外部程序的输出,因为它们通常会使用块缓冲当它们的输出重定向到管道时,这意味着输出仅在刷新该缓冲区后才会到达捕获程序,因此实际上不可能进行实时捕获。

尽管有人建议了一些解决方法:

1)(Windows)Here是一种解决方法,它使用GetConsoleScreenBuffer()捕获来自任意控制台程序的输出,但目前仅支持一个控制台页面的输出。

2)(Linux)在Linux上,显然有可能使用伪终端来强制外部程序使用无缓冲输出。 Here是一个例子。