Как я могу прочитать выходные данные процесса, которые не были сброшены?

Считайте, что эта маленькая программа скомпилирована как 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 это делает?


person ArcticLord    schedule 08.09.2016    source источник
comment
Есть ли причина, по которой вы не используете более простой while(fgets(...)) fputs(...);? Вы также можете сбросить стандартный вывод в программе чтения.   -  person Adrian Roman    schedule 08.09.2016
comment
Пожалуйста, определите, что вы имеете в виду под реальным временем.   -  person too honest for this site    schedule 08.09.2016
comment
while (!feof(pipe)) неправильно.   -  person too honest for this site    schedule 08.09.2016
comment
И это явно C++, а не C. (Ну, это можно было бы написать как C, но вы, видимо, компилируете как C++. Если вы хотите, чтобы это был код C, компилируйте как C! Это разные языки.)   -  person too honest for this site    schedule 08.09.2016
comment
@ Олаф, что не так с while(!feof(pipe))? Я получил этот код из этот вопрос.   -  person ArcticLord    schedule 08.09.2016
comment
И в реальном времени должно означать, что я хочу получить вывод до того, как оператор scanf запросит ввод.   -  person ArcticLord    schedule 08.09.2016
comment
stackoverflow.com/questions/4057985 /   -  person sameerkn    schedule 08.09.2016
comment
@sameerkn Спасибо за ссылку. Это очень интересное решение. Но, к сожалению, это работает только с linux.   -  person ArcticLord    schedule 08.09.2016
comment
Ваш пример с Windows cmd неверен. Он ничего не отображает, пока буфер не будет очищен.   -  person Algirdas Preidžius    schedule 08.09.2016
comment
@AlgirdasPreidžius мне подходит даже в Ubuntu.   -  person rustyx    schedule 08.09.2016
comment
@ArcticLord: stackoverflow.com/ вопросы/5431941/ . Это не то, что означает реальное время. И scanf никогда не запрашивает ввод данных. Он просто читается с stdin.   -  person too honest for this site    schedule 08.09.2016
comment
@RustyX Тогда вы не используете командную строку Windows, о которой я говорил. Прошло много времени с тех пор, как я использовал Linux, но я помню, что между терминалом Linux и Windows cmd есть некоторые принципиальные различия.   -  person Algirdas Preidžius    schedule 08.09.2016
comment
Под окнами: вы пытались создать процесс с помощью CreateProcess и дать ему дескриптор канала с очень маленьким (например... 1 байтом?) буфером? Поскольку буфер должен быть заполнен в тот момент, когда приложение пытается записать в него, оно должно выполнить очистку как можно скорее...   -  person hoffmale    schedule 09.09.2016
comment
@hoffmale Спасибо за идею. Я только что попробовал, и это не сработало. Проблема в том, что я не могу использовать 1-байтовый буфер для канала. Функция CreatePipe имеет параметр для установки размера буфера, но документ говорит, что это только предложение; система использует это значение для расчета соответствующего механизма буферизации. Есть ли другая функция для создания канала с фиксированным размером буфера?   -  person ArcticLord    schedule 09.09.2016
comment
@hoffmale: это не сработает. Проблема не в буфере канала, а в буфере библиотеки времени выполнения C.   -  person Harry Johnston    schedule 10.09.2016


Ответы (5)


Вас укусил тот факт, что буферизация для потоков, которые автоматически открываются в программе C, изменяется в зависимости от типа подключенного устройства.

Это немного странно. Одна из вещей, которые делают *nix приятными для игры (и которые отражены в стандартной библиотеке C), заключается в том, что процессы не очень заботятся о том, откуда они получают данные и куда они их записывают. Вы просто перенаправляете и перенаправляете на досуге, и обычно это подключается и работает, и довольно быстро.

Одно очевидное место, где это правило нарушается, — это взаимодействие; вы приводите хороший пример. Если выходные данные программы буферизованы блоками, вы не увидите их до тех пор, пока, возможно, не накопится 4 КБ данных или процесс не завершится.

Однако программа может определить, пишет ли она на терминал через isatty() (и, возможно, также с помощью других средств). Концептуально терминал включает в себя пользователя, предлагающего интерактивную программу. Код библиотеки, открывающий stdin и stdout, проверяет это и изменяет свою политику буферизации на line buffered: когда встречается новая строка, поток сбрасывается. Это идеально подходит для интерактивных линейно-ориентированных приложений. (Он не идеален для редактирования строки, как это делает bash, который полностью отключает буферизацию.)

справочная страница открытой группы для стандартного ввода довольно расплывчата в отношении буферизации в чтобы дать реализациям достаточную свободу действий, чтобы быть эффективной, но он говорит:

стандартный входной и стандартный выходные потоки полностью буферизуются тогда и только тогда, когда можно определить, что поток не относится к интерактивному устройству.

Вот что происходит с вашей программой: стандартная библиотека видит, что она работает «не в интерактивном режиме» (запись в канал), пытается быть умной и эффективной и включает буферизацию блоков. Запись новой строки больше не очищает вывод. Обычно это хорошо: представьте, что вы записываете двоичные данные, записывая на диск в среднем каждые 256 байт! Ужасный.

Примечательно, что между вами и, скажем, диском существует целый каскад буферов; после стандартной библиотеки C идут буферы операционной системы, а затем уже сам диск.

Теперь к вашей проблеме: буфер стандартной библиотеки, используемый для хранения символов, которые нужно записать, находится в пространстве памяти программы. Несмотря на внешний вид, данные еще не покинули вашу программу и, следовательно, недоступны (официально) для других программ. Я думаю, вам не повезло. Вы не одиноки: большинство интерактивных консольных программ будут работать плохо, если вы попытаетесь управлять ими через конвейеры.

person Peter - Reinstate Monica    schedule 08.09.2016
comment
спасибо за этот потрясающе хорошо объясненный ответ. Теперь мне намного яснее. - person ArcticLord; 08.09.2016

ИМХО, это одна из наименее логичных частей буферизации ввода-вывода: она действует по-разному, когда направлена ​​на терминал, файл или канал. Если ввод-вывод направляется в файл или канал, он обычно буферизуется, это означает, что вывод фактически записывается только тогда, когда буфер заполнен или когда происходит явный сброс => это то, что вы видите, когда вы выполняете программу через popen.

Но когда ввод-вывод направляется на терминал, возникает особый случай: весь ожидающий вывод автоматически сбрасывается перед чтением с того же терминала. Этот особый случай необходим, чтобы интерактивные программы могли отображать подсказки перед чтением.

Плохо то, что если вы попытаетесь запустить интерактивное приложение через каналы, вы проиграете: подсказки можно прочитать только тогда, когда приложение завершится или когда будет выведено достаточно текста, чтобы заполнить буфер. Вот почему разработчики Unix изобрели так называемые псевдотерминалы (pty). Они реализованы как драйверы терминала, так что приложение использует интерактивную буферизацию, но на самом деле вводом-выводом управляет другая программа, владеющая главной частью pty.

К сожалению, поскольку вы пишете application.exe, я предполагаю, что вы используете Windows, и я не знаю эквивалентного механизма в Windows API. Вызываемый объект должен использовать небуферизованный ввод-вывод (по умолчанию stderr не буферизован), чтобы вызывающая сторона могла прочитать подсказки перед отправкой ответа.

person Serge Ballesta    schedule 08.09.2016
comment
Спасибо за ответ. На самом деле я искал независимое от ОС решение. Таким образом, это возможно с Linux, как вы объяснили, и этот вопрос показывает но невозможно с окнами. - person ArcticLord; 08.09.2016

Проблемы моего вопроса в моем исходном сообщении уже очень хорошо объяснены в других ответах.
Консольные приложения используют функцию с именем isatty(), чтобы определить, подключен ли их обработчик stdout к каналу или реальной консоли. В случае канала весь вывод буферизуется и сбрасывается по частям, за исключением случаев, когда вы напрямую вызываете fflush(). В случае реальной консоли вывод не буферизуется и напрямую выводится на вывод консоли.
В Linux вы можете использовать openpty() для создания псевдотерминала и создания в нем вашего процесса. В результате процесс будет думать, что он работает в реальном терминале и использует небуферизованный вывод.
Кажется, в Windows нет такой возможности.

После долгих копаний в документации по winapi я обнаружил, что это < strong>неправда. На самом деле вы можете создать свой собственный экранный буфер консоли и использовать его для 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;
}
person ArcticLord    schedule 16.09.2016

Вы не можете. Потому что еще не сброшенные данные принадлежат самой программе.

person apple apple    schedule 08.09.2016
comment
Но windows cmd и linux ttys, похоже, могут это сделать. И я спрашиваю, почему. - person ArcticLord; 08.09.2016
comment
Даже Windows cmd не может этого сделать AFAIK. И я никогда не использую linux ttys. - person apple apple; 08.09.2016
comment
У вас есть пример? - person apple apple; 08.09.2016
comment
Мой пример в моем вопросе. Первый фрагмент кода отлично работает с Windows cmd. Но не с моим кодом для извлечения вывода, пока я не использую флеш. - person ArcticLord; 08.09.2016
comment
Почему -1? Может быть, какое-то предложение, чтобы я мог улучшить свой ответ? - person apple apple; 08.09.2016
comment
Я думаю, что ответ был точным в его первоначальной версии. Независимо от того, открываете ли вы или делаете что-то еще, это не имеет значения для буферизации stdout программы. (Я думаю.) И этот буфер находится под контролем записывающей программы и ничьей другой (по уважительной причине). - person Peter - Reinstate Monica; 08.09.2016
comment
я тоже так думаю - person apple apple; 08.09.2016
comment
Нет, суть вопроса в следующем: почему printf сбрасывает каждую строку, когда stdout является консолью, но буферизует данные, когда stdout является файлом. - person rustyx; 08.09.2016
comment
Ну, по крайней мере, его вообще не нужно промывать, так что на это никогда не стоит полагаться. - person apple apple; 08.09.2016
comment
@RustyX Я читаю вопрос о том, как я могу получить вывод приложений, которые я не могу изменить? в тексте и т. д. Как я могу прочитать вывод процесса, который не был сброшен? в заголовке, на который правильный ответ - нельзя. Да, проблема возникла при подключении stdout к пайпу, но это не главный вопрос, т.к. пайп, видимо, не обсуждается. - person Peter - Reinstate Monica; 08.09.2016

Я думаю, вы можете сбросить данные в stderr или инкапсулировать функцию fgetc и fungetc, чтобы не повредить поток, или использовать system("application.ext >>log"), а затем mmap записывать в память, чтобы делать то, что вы хотите.

person EmilyAvon    schedule 08.09.2016
comment
Как вы думаете, стандартный вывод application.exe становится небуферизованным только потому, что вы его перенаправляете? (Я не саркастичен. Я не совсем уверен. Но я почти уверен: буферизация stdout закодирована в макросе putchar, являющемся частью стандартной библиотеки c, которая находится в системе. процесс (например, оболочка или программа, устанавливающая канал в OP.) - person Peter - Reinstate Monica; 08.09.2016
comment
Чтобы ответить на мой собственный вопрос: да, это меняется; стандартная библиотека изменяет буферизацию для stdin/out в зависимости от того, подключены ли они к терминалу. Что касается вашего предложения: лог-файл не будет содержать вывод программы, в формате mmapped или нет ;-). - person Peter - Reinstate Monica; 08.09.2016