如何读取尚未刷新的流程输出?

时间:2016-09-08 10:51:14

标签: c++ winapi process stdout unbuffered

考虑将这个小程序编译为application.exe

#include <stdio.h>

int main()
{
    char str[100];
    printf ("Hello, please type something\n");
    scanf("%[^\n]s", &str);
    printf("you typed: %s\n", str);
    return 0;
}

现在我使用此代码启动application.exe并获取其输出。

#include <stdio.h>
#include <iostream>
#include <stdexcept>

int main()
{
    char buffer[128];
    FILE* pipe = popen("application.exe", "r");
    while (!feof(pipe)) {
        if (fgets(buffer, 128, pipe) != NULL)
            printf(buffer);
    }
    pclose(pipe);
    return 0;
}

我的问题是在我输入之前没有输出。然后获取两个输出行。 我可以通过在第一个printf语句之后添加此行来解决此问题。

fflush(stdout);

然后在我按预期输入之前获取第一行。

但是如何获取我无法修改且在“实时”中不使用fflush()的应用程序的输出(在退出之前的意思)? 。 windows cmd是如何做到的?

5 个答案:

答案 0 :(得分:10)

你被C程序中​​自动打开的流的缓冲因所附设备的类型而改变,这已经被你所困扰。

这有点奇怪 - 让* nixes很好玩的东西之一(并且反映在C标准库中)是流程并不关心从哪里获取数据他们写的地方。你只需要在闲暇时管道和重定向,它通常是即插即用的,速度非常快。

这个规则破坏的一个显而易见的地方是互动;你提出一个很好的例子。如果程序的输出是块缓冲的,那么在累积4k数据之前你就不会看到它,或者进程退出。

程序可以检测它是否通过isatty()写入终端(也可能通过其他方式)。终端在概念上包括用户,建议交互式程序。库代码打开stdin和stdout检查并将其缓冲策略更改为行缓冲:当遇到换行符时,刷新流。这非常适合交互式,面向行的应用程序。 (对于行编辑而言,它不太完美,正如bash所做的那样,它完全禁用了缓冲。)

open group man page for stdin在缓冲方面相当模糊,以便为实施提供足够的余地以提高效率,但确实说:

  

标准输入和标准输出流是完全缓冲的,当且仅当可以确定流不参考交互设备时。

您的程序会发生什么:标准库看到它正在运行&#34;非交互式&#34; (写入管道),尝试智能和高效,并打开块缓冲。编写换行符不会再刷新输出。通常这是一件好事:想象一下写二进制数据,平均每256字节写入磁盘!可怕。

值得注意的是,你和磁盘之间可能存在一整套缓冲区;在C标准库到达操作系统的缓冲区之后,然后是适当的磁盘。

现在解决您的问题:用于存储待写字符的标准库缓冲区位于程序的内存空间中。尽管出现了,但数据尚未离开您的程序,因此其他程序无法(正式)访问。我觉得你运气不好。您并不孤单:大多数交互式控制台程序在尝试通过管道操作时都会表现不佳。

答案 1 :(得分:8)

恕我直言,这是IO缓冲中不太合乎逻辑的部分之一:当指向终端或文件或管道时,它的行为不同。如果将IO定向到文件或管道,则通常缓冲,这意味着仅在缓冲区已满或发生显式刷新时才实际写入输出=&gt;这是您通过popen执行程序时看到的内容。

但是当IO被定向到终端时,会出现一种特殊情况:所有未决输出在从同一终端读取之前自动刷新。这种特殊情况对于允许交互式程序在阅读之前显示提示是必要的。

糟糕的是,如果您尝试通过管道驱动交互式应用程序,则会松动:只有在应用程序结束或输出足够的文本来填充缓冲区时才能读取提示。这就是Unix开发人员发明所谓的伪ttys pty)的原因。它们被实现为终端驱动程序,以便应用程序使用交互式缓冲,但IO实际上是由拥有pty的主要部分的另一个程序操纵的。

不幸的是,当您编写application .exe 时,我认为您使用的是Windows,而我不知道Windows API中的等效机制。被调用者必须使用无缓冲的IO(默认情况下,stderr是无缓冲的),以允许调用者在发送答案之前读取提示。

答案 2 :(得分:1)

我原帖中的问题已经解释得很好了 在其他答案中。
控制台应用程序使用名为isatty()的函数进行检测 如果他们的stdout处理程序连接到管道或真正的控制台。如果是管道 除非您直接调用fflush(),否则所有输出都将以块的形式进行缓冲和刷新。 在真正的控制台的情况下,输出是无缓冲的,并直接打印到 控制台输出。
在Linux中,您可以使用openpty()创建伪终端并在其中创建进程。作为一个 结果该进程会认为它在真实的终端中运行并使用无缓冲的输出。
Windows似乎没有 这样的选择。

经过大量的winapi文档挖掘后,我发现这是不正确。其实你可以创造 您自己的控制台屏幕缓冲区,并将其用于您的进程的stdout,然后将无缓冲 遗憾的是,这不是一个非常舒适的解决方案,因为没有事件处理程序,我们需要轮询新数据。 此刻,当这个屏幕缓冲区已满时,我不确定如何处理滚动。
但即使仍有一些问题 离开我想我已经为那些想要获取无缓冲(和未刷新)的人创建了一个非常有用(且有趣)的起点 Windows控制台进程输出。

#include <windows.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    char cmdline[] = "application.exe"; // process command
    HANDLE scrBuff;                     // our virtual screen buffer
    CONSOLE_SCREEN_BUFFER_INFO scrBuffInfo; // state of the screen buffer
                                            // like actual cursor position
    COORD scrBuffSize = {80, 25};       // size in chars of our screen buffer
    SECURITY_ATTRIBUTES sa;             // security attributes
    PROCESS_INFORMATION procInfo;       // process information
    STARTUPINFO startInfo;              // process start parameters
    DWORD procExitCode;                 // state of process (still alive)
    DWORD NumberOfCharsWritten;         // output of fill screen buffer func
    COORD pos = {0, 0};                 // scr buff pos of data we have consumed
    bool quit = false;                  // flag for reading loop

    // 1) Create a screen buffer, set size and clear

    sa.nLength = sizeof(sa);
    scrBuff = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE,
                                         FILE_SHARE_READ | FILE_SHARE_WRITE,
                                         &sa, CONSOLE_TEXTMODE_BUFFER, NULL);
    SetConsoleScreenBufferSize(scrBuff, scrBuffSize);
    // clear the screen buffer
    FillConsoleOutputCharacter(scrBuff, '\0', scrBuffSize.X * scrBuffSize.Y,
                               pos, &NumberOfCharsWritten);

    // 2) Create and start a process
    //      [using our screen buffer as stdout]

    ZeroMemory(&procInfo, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&startInfo, sizeof(STARTUPINFO));
    startInfo.cb = sizeof(STARTUPINFO);
    startInfo.hStdOutput = scrBuff;
    startInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
    startInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    startInfo.dwFlags |= STARTF_USESTDHANDLES;
    CreateProcess(NULL, cmdline, NULL, NULL, FALSE,
                  0, NULL, NULL, &startInfo, &procInfo);    
    CloseHandle(procInfo.hThread);

    // 3) Read from our screen buffer while process is alive

    while(!quit)
    {
        // check if process is still alive or we could quit reading
        GetExitCodeProcess(procInfo.hProcess, &procExitCode);
        if(procExitCode != STILL_ACTIVE) quit = true;

        // get actual state of screen buffer
        GetConsoleScreenBufferInfo(scrBuff, &scrBuffInfo);

        // check if screen buffer cursor moved since
        // last time means new output was written
        if (pos.X != scrBuffInfo.dwCursorPosition.X ||
            pos.Y != scrBuffInfo.dwCursorPosition.Y)            
        {
            // Get new content of screen buffer
            //  [ calc len from pos to cursor pos: 
            //    (curY - posY) * lineWidth + (curX - posX) ]
            DWORD len =  (scrBuffInfo.dwCursorPosition.Y - pos.Y)
                        * scrBuffInfo.dwSize.X 
                        +(scrBuffInfo.dwCursorPosition.X - pos.X);
            char buffer[len];
            ReadConsoleOutputCharacter(scrBuff, buffer, len, pos, &len);

            // Print new content
            // [ there is no newline, unused space is filled with '\0'
            //   so we read char by char and if it is '\0' we do 
            //   new line and forward to next real char ]
            for(int i = 0; i < len; i++)
            {
                if(buffer[i] != '\0') printf("%c",buffer[i]);
                else
                {
                    printf("\n");
                    while((i + 1) < len && buffer[i + 1] == '\0')i++;
                }
            }

            // Save new position of already consumed data
            pos = scrBuffInfo.dwCursorPosition;
        }
        // no new output so sleep a bit before next check
        else Sleep(100);
    }

    // 4) Cleanup and end

    CloseHandle(scrBuff);   
    CloseHandle(procInfo.hProcess);
    return 0;
}

答案 3 :(得分:0)

你做不到。 因为尚未刷新的数据归程序本身所有。

答案 4 :(得分:-1)

我认为您可以将数据刷新到stderr或封装fgetcfungetc的函数,以免损坏流或使用system("application.ext >>log"),然后使用mmap记录下来做你想做的事。