Почему я не могу перенаправить вывод из WriteConsole?

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

#include <windows.h>

int main() {
    HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
    DWORD byteswritten;
    WriteConsole(h, "WriteConsole", 12, &byteswritten, NULL);
    WriteFile(h, "WriteFile", 9, &byteswritten, NULL);
}

Если, когда я выполняю эту программу и перенаправляю ее вывод с помощью a > out.txt или a 1> out.txt, на консоль ничего не выводится (как и ожидалось), но содержимое out.txt только

WriteFile

В чем разница между ними, что позволяет перенаправлять вызовы WriteFile в файл и вызовы WriteConsole для перехода в ... никуда

Протестировано с помощью gcc и msvc в Windows 10


person rtpax    schedule 21.08.2017    source источник
comment
WriteConsole не работает, если он используется со стандартным дескриптором, который перенаправляется в файл   -  person RbMm    schedule 22.08.2017
comment
@RbMm откуда это?   -  person rtpax    schedule 22.08.2017
comment
из msdn - docs.microsoft.com/en-us/windows/console/writeconsole   -  person RbMm    schedule 22.08.2017
comment
Добавьте некоторую проверку ошибок и посмотрите, успешны ли ваши вызовы API. В противном случае вы беспомощны. Почти каждый задаваемый здесь вопрос о winapi ошибается. Не пренебрегайте проверкой ошибок. Чтение документации тоже не повредит. Игнорирование этого - еще одна распространенная глупость.   -  person David Heffernan    schedule 22.08.2017
comment
@RbMm Тогда я полагаю, что это мой ответ, продолжайте и опубликуйте, чтобы я мог принять   -  person rtpax    schedule 22.08.2017
comment
@DavidHeffernan Удивительно, но проверка возвращаемого значения возвращает единицу даже при этой ошибке   -  person rtpax    schedule 22.08.2017
comment
Это как говорить в пустоте. Прочтите документацию и скажите, какое значение возвращается в случае ошибки.   -  person David Heffernan    schedule 22.08.2017
comment
Он определенно говорит, что должен возвращать ноль при сбое.   -  person rtpax    schedule 22.08.2017
comment
Вы изменили свой комментарий. Раньше он сказал, что вернул ноль. Я не уверен, что могу действительно поверить в то, что вы написали. Во всяком случае, суть остается. Проверить ошибки. Прочтите документы. Не экономьте.   -  person David Heffernan    schedule 22.08.2017
comment
Извините, хотя я поменял его достаточно быстро, это была опечатка   -  person rtpax    schedule 22.08.2017


Ответы (3)


WriteConsole работает только с дескрипторами экрана консоли, но не с файлами и каналами.

Если вы пишете только содержимое ASCII, вы можете использовать WriteFile для всего.

Если вам нужно написать символы Unicode, вы можете использовать GetConsoleMode для обнаружения дескриптора типа, он терпит неудачу для всего, что не является дескриптором консоли.

При выполнении подобного необработанного вывода вам также придется иметь дело с BOM, если дескриптор перенаправлен на файл.

Это сообщение в блоге является хорошей отправной точкой для работы с Юникод в консоли Windows ...

person Anders    schedule 22.08.2017
comment
В частности, в Windows 8+ WriteConsole запросить элемент управления вводом-выводом (IOCTL), который поддерживается только консольным устройством ConDrv. Некоторое другое устройство, такое как файловая система, не выполнит этот IOCTL как недопустимый параметр, и, в свою очередь, консольный API сообщит об этом как о недопустимом дескрипторе. В Windows 7 и более ранних версиях API консоли использует LPC вместо устройства ConDrv, поэтому дескрипторы буфера консоли помечаются для маршрутизации к соединению LPC и, таким образом, немедленно обнаруживаются в пользовательском режиме (API, не клиентом code), проверив, что установлены два младших бита (например, 3, 7, 11 и т. д.). - person Eryk Sun; 23.08.2017
comment
В этом старом сообщении в блоге есть некоторая полезная информация, но она не помогает, когда путает консоль с оболочкой CMD. - person Eryk Sun; 23.08.2017

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

// info to redirected console output
typedef struct tagRedConInfo
{
  // hidden console
  HANDLE     hCon;

  // old console handles
  HANDLE     hOldConOut;
  HANDLE     hOldConErr;

  // buffer to read screen content
  CHAR_INFO *BufData;
  INT        BufSize;

  //
} TRedConInfo;




//------------------------------------------------------------------------------
// GLOBALS
//------------------------------------------------------------------------------

// initial handles
HANDLE gv_hOldConOut;
HANDLE gv_hOldConErr;



//------------------------------------------------------------------------------
// PROTOTYPES
//------------------------------------------------------------------------------

/* init redirecting the console output */
BOOL Shell_InitRedirectConsole(BOOL,TRedConInfo*);

/* done redirecting the console output */
BOOL Shell_DoneRedirectConsole(TRedConInfo*);

/* read string from hidden console, then clear */
BOOL Shell_ReadRedirectConsole(TRedConInfo*,TCHAR*,INT);

/* clear buffer of hidden console */
BOOL Shell_ClearRedirectConsole(TRedConInfo*);





//------------------------------------------------------------------------------
// IMPLEMENTATIONS
//------------------------------------------------------------------------------


/***************************************/
/* init redirecting the console output */
/***************************************/

BOOL Shell_InitRedirectConsole(BOOL in_SetStdHandles, TRedConInfo *out_RcInfo)
{
    /* locals */
    HANDLE              lv_hCon;
    SECURITY_ATTRIBUTES lv_SecAttr;


  // preclear structure
  memset(out_RcInfo, 0, sizeof(TRedConInfo));

  // prepare inheritable handle just in case an api spans an external process
  memset(&lv_SecAttr, 0, sizeof(SECURITY_ATTRIBUTES));
  lv_SecAttr.nLength        = sizeof(SECURITY_ATTRIBUTES);
  lv_SecAttr.bInheritHandle = TRUE;

  // create hidden console buffer
  lv_hCon = CreateConsoleScreenBuffer(
     GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
    &lv_SecAttr, CONSOLE_TEXTMODE_BUFFER, 0);

  // failed to create console buffer?
  if (lv_hCon == INVALID_HANDLE_VALUE)
    return FALSE;

  // store
  out_RcInfo->hCon = lv_hCon;

  // set as standard handles for own process?
  if (in_SetStdHandles)
  {
    // mutex the globals
    WaitForGlobalVarMutex();

    // remember the old handles
    out_RcInfo->hOldConOut = GetStdHandle(STD_OUTPUT_HANDLE);
    out_RcInfo->hOldConErr = GetStdHandle(STD_ERROR_HANDLE);

    // set hidden console as std output
    SetStdHandle(STD_OUTPUT_HANDLE, lv_hCon);
    SetStdHandle(STD_ERROR_HANDLE,  lv_hCon);

    // is this the first instance?
    if (!gv_hOldConOut)
    {
      // inform our own console output code about the old handles so our own
      // console will be writing to the real console, only console output from
      // other parties will write to the hidden console
      gv_hOldConOut = out_RcInfo->hOldConOut;
      gv_hOldConErr = out_RcInfo->hOldConErr;
    }

    // release mutex
    ReleaseGlobalVarMutex();
  }

  // done
  return TRUE;
}




/***************************************/
/* done redirecting the console output */
/***************************************/

BOOL Shell_DoneRedirectConsole(TRedConInfo *in_RcInfo)
{
  // validate
  if (!in_RcInfo->hCon)
    return FALSE;

  // restore original handles?
  if (in_RcInfo->hOldConOut)
  {
    // mutex the globals
    WaitForGlobalVarMutex();

    // restore original handles
    SetStdHandle(STD_OUTPUT_HANDLE, in_RcInfo->hOldConOut);
    SetStdHandle(STD_ERROR_HANDLE,  in_RcInfo->hOldConErr);

    // was this the first instance?
    if (in_RcInfo->hOldConOut == gv_hOldConOut)
    {
      // clear
      gv_hOldConOut = NULL;
      gv_hOldConErr = NULL;
    }

    // release mutex
    ReleaseGlobalVarMutex();
  }

  // close the console handle
  CloseHandle(in_RcInfo->hCon);

  // free read buffer
  if (in_RcInfo->BufData)
    MemFree(in_RcInfo->BufData);

  // clear structure
  memset(in_RcInfo, 0, sizeof(TRedConInfo));

  // done
  return TRUE;
}




/***********************************************/
/* read string from hidden console, then clear */
/***********************************************/

BOOL Shell_ReadRedirectConsole(TRedConInfo *in_RcInfo, TCHAR *out_Str, INT in_MaxLen)
{
    /* locals */
    TCHAR                      lv_C;
    INT                        lv_X;
    INT                        lv_Y;
    INT                        lv_W;
    INT                        lv_H;
    INT                        lv_N;
    INT                        lv_Len;
    INT                        lv_Size;
    INT                        lv_PrvLen;
    COORD                      lv_DstSze;
    COORD                      lv_DstOfs;
    DWORD                      lv_Written;
    SMALL_RECT                 lv_SrcRect;
    CHAR_INFO                 *lv_BufData;
    CONSOLE_SCREEN_BUFFER_INFO lv_Info;


  // preclear output
  out_Str[0] = 0;

  // validate
  if (!in_RcInfo->hCon)
    return FALSE;

  // reserve character for eos
  --in_MaxLen;

  // get current buffer info
  if (!GetConsoleScreenBufferInfo(in_RcInfo->hCon, &lv_Info))
    return FALSE;

  // check whether there is something at all
  if (!lv_Info.dwSize.X || !lv_Info.dwSize.Y)
    return FALSE;

  // limit the buffer passed onto read call otherwise it
  // will fail with out-of-resources error
  lv_DstSze.X = (INT16)(lv_Info.dwSize.X);
  lv_DstSze.Y = (INT16)(lv_Info.dwSize.Y < 8 ? lv_Info.dwSize.Y : 8);

  // size of buffer needed
  lv_Size = lv_DstSze.X * lv_DstSze.Y * sizeof(CHAR_INFO);

  // is previous buffer too small?
  if (!in_RcInfo->BufData || in_RcInfo->BufSize < lv_Size)
  {
    // free old buffer
    if (in_RcInfo->BufData)
      MemFree(in_RcInfo->BufData);

    // allocate read buffer
    if ((in_RcInfo->BufData = (CHAR_INFO*)MemAlloc(lv_Size)) == NULL)
      return FALSE;

    // store new size
    in_RcInfo->BufSize = lv_Size;
  }

  // always write to (0,0) in buffer
  lv_DstOfs.X = 0;
  lv_DstOfs.Y = 0;

  // init src rectangle
  lv_SrcRect.Left   = 0;
  lv_SrcRect.Top    = 0;
  lv_SrcRect.Right  = lv_DstSze.X;
  lv_SrcRect.Bottom = lv_DstSze.Y;

  // buffer to local
  lv_BufData = in_RcInfo->BufData;

  // start at first string position in output
  lv_Len = 0;

  // loop until no more rows to read
  do
  {
    // read buffer load
    if (!ReadConsoleOutput(in_RcInfo->hCon, lv_BufData, lv_DstSze, lv_DstOfs, &lv_SrcRect))
      return FALSE;

    // w/h of actually read content
    lv_W = lv_SrcRect.Right  - lv_SrcRect.Left + 1;
    lv_H = lv_SrcRect.Bottom - lv_SrcRect.Top  + 1;

    // remember previous position
    lv_PrvLen = lv_Len;

    // loop through rows of buffer
    for (lv_Y = 0; lv_Y < lv_H; ++lv_Y)
    {
      // reset output position of current row
      lv_N = 0;

      // loop through columns
      for (lv_X = 0; lv_X < lv_W; ++lv_X)
      {
        // is output full?
        if (lv_Len + lv_N > in_MaxLen)
          break;

        // get character from screen buffer, ignore attributes
        lv_C = lv_BufData[lv_Y * lv_DstSze.X + lv_X].Char.UnicodeChar;

        // append character
        out_Str[lv_Len + lv_N++] = lv_C;
      }

      // remove spaces at the end of the line
      while (lv_N > 0 && out_Str[lv_Len+lv_N-1] == ' ')
        --lv_N;

      // if row was not blank
      if (lv_N > 0)
      {
        // update output position
        lv_Len += lv_N;

        // is output not full?
        if (lv_Len + 2 < in_MaxLen)
        {
          // append cr/lf
          out_Str[lv_Len++] = '\r';
          out_Str[lv_Len++] = '\n';
        }
      }
    }

    // update screen position
    lv_SrcRect.Top    = (INT16)(lv_SrcRect.Top    + lv_H);
    lv_SrcRect.Bottom = (INT16)(lv_SrcRect.Bottom + lv_H);

    // until nothing is added or no more screen rows
  } while (lv_PrvLen != lv_Len && lv_SrcRect.Bottom < lv_Info.dwSize.Y);

  // remove last cr/lf
  if (lv_Len > 2)
    lv_Len -= 2;

  // append eos
  out_Str[lv_Len] = 0;

  // total screen buffer size in characters
  lv_Size = lv_Info.dwSize.X * lv_Info.dwSize.Y;

  // clear the buffer with spaces
  FillConsoleOutputCharacter(in_RcInfo->hCon, ' ', lv_Size, lv_DstOfs, &lv_Written);

  // reset cursor position to (0,0)
  SetConsoleCursorPosition(in_RcInfo->hCon, lv_DstOfs);

  // done
  return TRUE;
}




/**********************************/
/* clear buffer of hidden console */
/**********************************/

BOOL Shell_ClearRedirectConsole(TRedConInfo *in_RcInfo)
{
    /* locals */
    INT                        lv_Size;
    COORD                      lv_ClrOfs;
    DWORD                      lv_Written;
    CONSOLE_SCREEN_BUFFER_INFO lv_Info;


  // validate
  if (!in_RcInfo->hCon)
    return FALSE;

  // get current buffer info
  if (!GetConsoleScreenBufferInfo(in_RcInfo->hCon, &lv_Info))
    return FALSE;

  // clear from (0,0) onward
  lv_ClrOfs.X = 0;
  lv_ClrOfs.Y = 0;

  // total screen buffer size in characters
  lv_Size = lv_Info.dwSize.X * lv_Info.dwSize.Y;

  // clear the buffer with spaces
  FillConsoleOutputCharacter(in_RcInfo->hCon, ' ', lv_Size, lv_ClrOfs, &lv_Written);

  // reset cursor position to (0,0)
  SetConsoleCursorPosition(in_RcInfo->hCon, lv_ClrOfs);

  // done
  return TRUE;
}
person Arnoud Mulder    schedule 23.12.2017

Изменить 2021:

В Windows 10 теперь есть ConPTY API ( aka псевдоконсоль), который в основном позволяет любой программе действовать как консоль для другой программы, таким образом, позволяет захватывать вывод, который напрямую записывается на консоль.

Это делает мой исходный ответ устаревшим для версий Windows, поддерживающих ConPTY.


Оригинальный ответ:

Из справки:

WriteConsole не работает, если он используется со стандартным дескриптором, который перенаправляется в файл. Если приложение обрабатывает многоязычный вывод, который может быть перенаправлен, определите, является ли дескриптор вывода дескриптором консоли (один из методов - вызвать GetConsoleMode и проверьте, удалось ли это сделать). Если дескриптор является дескриптором консоли, вызовите WriteConsole. Если дескриптор не является дескриптором консоли, вывод перенаправляется, и вы должны вызвать WriteFile для выполнения ввода / вывода.

Это применимо только в том случае, если вы контролируете исходный код приложения, которое хотите перенаправить. Недавно мне пришлось перенаправить вывод из приложения с закрытым исходным кодом, которое безусловно вызывало WriteConsole(), поэтому его нельзя было перенаправить в обычном режиме.

Чтение экранного буфера консоли (как было предложено в этом ответе) оказалось ненадежным, поэтому я использовал библиотека Microsoft Detours для подключения WriteConsole() API к целевому процессу и вызова WriteFile() при необходимости. В противном случае вызовите исходную функцию WriteConsole().

Я создал DLL-перехватчик на основе примера Использование обходов:

#include <windows.h>
#include <detours.h>

// Target pointer for the uninstrumented WriteConsoleW API.
//
auto WriteConsoleW_orig = &WriteConsoleW;

// Detour function that replaces the WriteConsoleW API.
//
BOOL WINAPI WriteConsoleW_hooked(
  _In_             HANDLE  hConsoleOutput,
  _In_       const VOID    *lpBuffer,
  _In_             DWORD   nNumberOfCharsToWrite,
  _Out_            LPDWORD lpNumberOfCharsWritten,
  _Reserved_       LPVOID  lpReserved 
)
{
    // Check if this actually is a console screen buffer handle.
    DWORD mode;
    if( GetConsoleMode( hConsoleOutput, &mode ) )
    {
        // Forward to the original WriteConsoleW() function.
        return WriteConsoleW_orig( hConsoleOutput, lpBuffer, nNumberOfCharsToWrite, lpNumberOfCharsWritten, lpReserved );
    }
    else
    {
        // This is a redirected handle (e. g. a file or a pipe). We multiply with sizeof(WCHAR), because WriteFile()
        // expects the number of bytes, but WriteConsoleW() gets passed the number of characters.
        BOOL result = WriteFile( hConsoleOutput, lpBuffer, nNumberOfCharsToWrite * sizeof(WCHAR), lpNumberOfCharsWritten, nullptr );

        // WriteFile() returns number of bytes written, but WriteConsoleW() has to return the number of characters written.
        if( lpNumberOfCharsWritten )
            *lpNumberOfCharsWritten /= sizeof(WCHAR);
        
        return result;
    }
}

// DllMain function attaches and detaches the WriteConsoleW_hooked detour to the
// WriteConsoleW target function.  The WriteConsoleW target function is referred to
// through the WriteConsoleW_orig target pointer.
//
BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
{
    if (DetourIsHelperProcess()) {
        return TRUE;
    }

    if (dwReason == DLL_PROCESS_ATTACH) {
        DetourRestoreAfterWith();

        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourAttach(&(PVOID&)WriteConsoleW_orig, WriteConsoleW_hooked);
        DetourTransactionCommit();
    }
    else if (dwReason == DLL_PROCESS_DETACH) {
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(&(PVOID&)WriteConsoleW_orig, WriteConsoleW_hooked);
        DetourTransactionCommit();
    }
    return TRUE;
}

Примечание. В ветке WriteFile() я не пишу BOM (отметку порядка байтов), потому что это не всегда нужно (например, при перенаправлении в канал вместо файла или при добавлении к существующему файлу ). Приложение, использующее DLL для перенаправления вывода процесса в файл, может просто самостоятельно записать спецификацию UTF-16 LE перед запуском перенаправленного процесса.

Целевой процесс создается с использованием DetourCreateProcessWithDllExW() с указанием имени нашей DLL-перехватчика в качестве аргумента для параметр lpDllName. Другие аргументы идентичны тому, как вы создаете перенаправленный процесс с помощью _ 9_ API. Я не буду вдаваться в подробности, потому что все это хорошо задокументировано.

person zett42    schedule 22.07.2018