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

How can I read process output that has not been flushed?

考虑将这个小程序编译为 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);

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

但是 我如何获取我无法修改且未在 "realtime" 中使用 fflush() 的应用程序的输出(意味着在它们退出之前)? . windows cmd 是如何做到的?

你不能。 因为还没有flushed的数据是程序自己拥有的。

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

在 C 程序中自动打开的流的缓冲会随着所连接设备的类型而变化,这一事实让您感到困惑。

这有点奇怪 — 让 *nixes 很好玩的原因之一(并且反映在 C 标准库中)是进程不太关心它们从哪里获取数据以及它们从哪里获取数据写下来。您只需随意传输和重定向,它通常是即插即用的,而且速度非常快。

这个规则明显被打破的地方是互动;你举了一个很好的例子。如果程序的输出是块缓冲的,则可能在累积 4k 数据或进程退出之前看不到它。

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

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

the standard input and standard output streams are fully buffered if and only if the stream can be determined not to refer to an interactive device.

这就是您的程序发生的情况:标准库看到它是 运行 "non-interactively"(写入管道),试图变得聪明和高效并打开块缓冲。写一个换行符不再刷新输出。通常这是一件好事:想象一下写入二进制数据,平均每 256 字节写入一次磁盘!太可怕了。

值得注意的是,您和磁盘之间可能存在一整套缓冲区级联;在 C 标准库之后是操作系统的缓冲区,然后是磁盘本身。

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

恕我直言,这是 IO 缓冲中逻辑性较低的部分之一:当指向终端或文件或管道时,它的行为不同。如果 IO 被定向到文件或管道,它通常 缓冲,这意味着输出实际上仅在缓冲区已满或发生显式刷新时写入 => 这就是当你通过 popen.

执行程序时你会看到

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

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

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

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

在深入研究 winapi 文档后,我发现这是 不正确的 。其实你可以创造 您自己的控制台屏幕缓冲区,并将其用于 stdout 您的进程,然后将无缓冲。
遗憾的是,这不是一个非常舒适的解决方案,因为没有事件处理程序,我们需要轮询新数据。 另外目前我不确定当这个屏幕缓冲区已满时如何处理滚动。
但即使仍然存在一些问题 left 我想我已经为那些想要获取无缓冲(和未刷新)的人创建了一个非常有用(和有趣)的起点 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, '[=10=]', 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 '[=10=]'
            //   so we read char by char and if it is '[=10=]' we do 
            //   new line and forward to next real char ]
            for(int i = 0; i < len; i++)
            {
                if(buffer[i] != '[=10=]') printf("%c",buffer[i]);
                else
                {
                    printf("\n");
                    while((i + 1) < len && buffer[i + 1] == '[=10=]')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;
}