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

Arc*_*ord 12 c++ winapi stdout process 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;
}
Run Code Online (Sandbox Code Playgroud)

现在我使用此代码启动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;
}
Run Code Online (Sandbox Code Playgroud)

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

fflush(stdout);
Run Code Online (Sandbox Code Playgroud)

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

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

Pet*_*ica 10

在C程序中自动打开的流的缓冲随附加的设备类型而变化,这一点已经被您所困扰.

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

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

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

stdinopen group手册页在缓冲方面相当模糊,以便为实现提供足够的余地以提高效率,但它确实说:

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

这就是你的程序发生的事情:标准库看到它正在"非交互式"运行(写入管道),试图变得智能和高效并开启块缓冲.编写换行符不会再刷新输出.通常这是一件好事:想象一下写二进制数据,平均每256字节写入磁盘!可怕.

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

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


Ser*_*sta 8

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

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

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

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


Arc*_*ord 3

我原来的帖子中的问题已经在其他答案中得到了很好的解释。
控制台应用程序使用名为 的函数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;
}
Run Code Online (Sandbox Code Playgroud)