为什么我不能重定向 WriteConsole 的输出?

Why can't I redirect output from 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.txta 1> out.txt 重定向它的输出时,没有任何内容打印到控制台(如预期的那样),但是 out.txt 的内容只是

WriteFile

两者之间的区别是允许对 WriteFile 的调用被重定向到文件并允许对 WriteConsole 的调用转到...无处

在 windows 10

上使用 gcc 和 msvc 进行了测试

WriteConsole 仅适用于控制台屏幕句柄,不适用于文件或管道。

如果您只编写 ASCII 内容,您可以使用 WriteFile 来编写所有内容。

如果您需要编写 Unicode 字符,您可以使用 GetConsoleMode 来检测句柄类型,对于不是控制台句柄的所有内容,它都会失败。

像这样进行原始输出时,如果句柄被重定向到文件,您还必须处理 BOM

This blog post 是在 Windows 控制台中处理 Unicode 的良好起点...

如果对方使用WriteConsole,下面的代码可以用来重定向控制台输出。该代码通过隐藏的控制台屏幕缓冲区读取输出。我已经编写了这段代码来拦截一些 directshow 驱动程序写入控制台的调试输出。 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;
}

编辑 2021 年:

Windows 10 现在有 ConPTY API(又名伪控制台),它基本上允许任何程序充当另一个程序的控制台,从而可以捕获直接写入的输出控制台。

这使我的原始答案对于支持 ConPTY 的 Windows 版本已过时。


原回答:

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

这仅适用于您控制要重定向的应用程序的源代码的情况。我最近不得不 重定向来自一个无条件调用 WriteConsole() 的闭源应用程序 的输出,因此它无法正常重定向。

读取控制台屏幕缓冲区(如 ) prooved to be unreliable, so I used Microsoft Detours library 所建议,挂钩目标进程中的 WriteConsole() API 并在必要时调用 WriteFile()。否则调用原始进程WriteConsole() 函数。

我根据Using Detours的例子创建了一个hook 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 BOM。

目标进程是使用 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 创建的。我就不细说了,因为这些都有据可查。