Why can't I redirect output from WriteConsole?

我是研究僧i 提交于 2021-01-21 09:03:52

问题


In the following program I print to the console using two different functions

#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);
}

If when I execute this program and redirect it's output using a > out.txt or a 1> out.txt nothing gets printed to the console (as expected) but the contents of out.txt are only

WriteFile

What is different between the two that allows calls to WriteFile to be redirected to the file and calls to WriteConsole to go to ... nowhere

Tested with gcc and msvc on windows 10


回答1:


WriteConsole only works with console screen handles, not files nor pipes.

If you are only writing ASCII content you can use WriteFile for everything.

If you need to write Unicode characters you can use GetConsoleMode to detect the handle type, it fails for everything that is not a console handle.

When doing raw output like this you also have to deal with the BOM if the handle is redirected to a file.

This blog post is a good starting point for dealing with Unicode in the Windows console...




回答2:


The code below can be used to redirect console output if the other party uses WriteConsole. The code reads the output via a hidden console screen buffer. I've written this code to intercept debug output some directshow drivers write to the console. Directshow drivers have the habit of doing things drivers should not do, like writing unwanted logfiles, writing to console and crashing.

// 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;
}



回答3:


From the reference:

WriteConsole fails if it is used with a standard handle that is redirected to a file. If an application processes multilingual output that can be redirected, determine whether the output handle is a console handle (one method is to call the GetConsoleMode function and check whether it succeeds). If the handle is a console handle, call WriteConsole. If the handle is not a console handle, the output is redirected and you should call WriteFile to perform the I/O.

This is only applicable if you control the source code of the application that you want to redirect. I recently had to redirect output from a closed-source application that unconditionally called WriteConsole() so it could not be redirected normally.

Reading the console screen buffer (as suggested by this answer) prooved to be unreliable, so I used Microsoft Detours library to hook the WriteConsole() API in the target process and call WriteFile() if necessary. Otherwise call the original WriteConsole() function.

I created a hook DLL based on the example of Using Detours:

#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;
}

Note: In the WriteFile() branch I don't write a BOM (byte order mark), because it is not always wanted (e. g. when redirecting to a pipe instead of a file or when appending to an existing file). An application that is using the DLL to redirect process output to a file can simply write the UTF-16 LE BOM on its own before launching the redirected process.

The target process is created using DetourCreateProcessWithDllExW(), specifying the name of our hook DLL as argument for the lpDllName parameter. The other arguments are identical to how you create a redirected process via the CreateProcessW() API. I won't go into detail, because these are all well documented.



来源:https://stackoverflow.com/questions/45805785/why-cant-i-redirect-output-from-writeconsole

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!