5

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

rtpax
  • 1,687
  • 1
  • 18
  • 32
  • 4
    *WriteConsole fails if it is used with a standard handle that is redirected to a file* – RbMm Aug 21 '17 at 21:32
  • @RbMm where is that from? – rtpax Aug 21 '17 at 21:33
  • 1
    from msdn - https://learn.microsoft.com/en-us/windows/console/writeconsole – RbMm Aug 21 '17 at 21:33
  • 1
    Add some error checking and see if your API calls succeeded. Otherwise you are helpless. Almost every winapi question asked here gets that wrong. Don't neglect error checking. Reading the documentation doesn't hurt either. Ignoring that is another common folly. – David Heffernan Aug 21 '17 at 21:34
  • @RbMm Then I suppose that is my answer, go ahead and post so that I may accept – rtpax Aug 21 '17 at 21:37
  • @DavidHeffernan Suprisingly, checking the return value returns one even on this failure – rtpax Aug 21 '17 at 21:38
  • It's like talking in a void. Read the documentation and tell me what value is returned in case of an error. – David Heffernan Aug 21 '17 at 21:40
  • It definitely says it should return zero on a failure – rtpax Aug 21 '17 at 21:42
  • You changed your comment. Before it said that it returned zero. I'm not sure I can really believe what you've written. Anyway, the point remains. Check errors. Read docs. Don't skimp. – David Heffernan Aug 21 '17 at 21:45
  • Sorry, though I changed it quick enough, it was a typo – rtpax Aug 21 '17 at 21:46

3 Answers3

8

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...

Anders
  • 97,548
  • 12
  • 110
  • 164
  • 1
    Specifically, in Windows 8+ `WriteConsole` request an I/O control (IOCTL) that's only supported by the ConDrv console device. Some other device, such as a filesystem, will fail this IOCTL as an invalid parameter, and in turn the console API will report it as an invalid handle. In Windows 7 and earlier, the console API uses LPC instead of the ConDrv device, so console buffer handles are flagged for routing to the LPC connection and thus immediately detectable in user mode (by the API, *not* client code) by checking that the lower two bits are set (e.g. 3, 7, 11, and so on). – Eryk Sun Aug 23 '17 at 15:14
  • 1
    That old blog post has some useful information, but it's not helping matters when it confuses the console with the CMD shell. – Eryk Sun Aug 23 '17 at 15:16
2

Edit 2021:

Windows 10 now has the ConPTY API (aka pseudo-console), which basically allows any program to act like the console for another program, thus enables capturing output that is directly written to the console.

This renders my original answer obsolete for Windows versions that support ConPTY.


Original answer:

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.

zett42
  • 25,437
  • 3
  • 35
  • 72
1

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;
}
Arnoud Mulder
  • 139
  • 1
  • 5