在Windows 10 C ++中通过控制台屏幕缓冲区重定向子进程标准输出

时间:2018-07-08 18:28:17

标签: c++ winapi

我正在尝试重定向Windows中子进程的stdout。两者都是控制台程序。我没有子进程的源代码,因此无法强制其刷新缓冲区。如herehere所述,对于printf和类似的实现,C运行时会缓冲除控制台和打印机以外的所有内容。因此,解决方案显然是适当地使用CreateConsoleScreenBuffer创建控制台屏幕缓冲区。我正在使用the approach from codeproject

PROCESS_INFORMATION pi;
HANDLE hConsole;
const COORD origin = { 0, 0 };

// Default security descriptor. Set inheritable. 
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; // So the child can use it

// Create and initialize screen buffer
hConsole = CreateConsoleScreenBuffer(
    GENERIC_READ | GENERIC_WRITE,       // Desired access       
    FILE_SHARE_WRITE | FILE_SHARE_READ, // share mode to child processes
    &sa,                                // SECURITY_ATTRIBUTES      
    CONSOLE_TEXTMODE_BUFFER,            // Must be this.   
    NULL                                // Reserved. Must be NULL 
);
DWORD dwDummy;
FillConsoleOutputCharacter(hConsole, '\0', MAXLONG, origin, &dwDummy)

现在,我将孩子的标准输出指向控制台并开始该过程

STARTUPINFO si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_FORCEOFFFEEDBACK | STARTF_USESTDHANDLES; // first one prevents cursor from showing loading.
si.hStdOutput = hConsole;

//...
// Get the command line and environmental block
//...

if (! CreateProcess(
    NULL,                         // module name. 
    (char*)command_line.c_str(),  // command line
    NULL,                         // process SECURITY_ATTRIBUTES
    NULL,                         // thread SECURITY_ATTRIBUTES
    TRUE,                         // inherit handles
    NULL,                         // creation flags
    enviros,                      // environmentBlock (enviros=NULL for testing)
    cDir,                         // working directory
    &si,                          // STARTUP_INFO object
    &pi                           // PROCESSINFO
) ){
    auto test = GetLastError();
    CloseHandle(hConsole);
    return false;
}
CloseHandle(pi.hThread);

然后,我可以循环使用ReadConsoleOutputCharacter来获取输出,如codeproject链接所示。看起来像

//... some initialization

GetConsoleScreenBufferInfo(hConsole, &csbi);
DWORD count = (csbi.dwCursorPosition.Y - lastpos.Y)*lineWidth + csbi.dwCursorPosition.X - lastpos.X;
LPTSTR buffer = (LPTSTR)LocalAlloc(0, count * sizeof(TCHAR));
ReadConsoleOutputCharacter(hConsole, buffer, count, lastpos, &count);
DWORD dwDummy;
FillConsoleOutputCharacter(hConsole, '\0', count, lastpos, &dwDummy);

//... Now move the cursor and grab the data from `buffer`

在Windows 7 / 8.1上,这对于所有程序均适用。 在Windows 10上,某些程序似乎绕过提供的句柄并直接打印到父控制台上,从而阻止我按需获取输出。

我还有其他线索。如果我强制该过程创建新的控制台窗口,即

CreateProcess(NULL, (char*)command_line.c_str(), NULL, NULL, TRUE, CREATE_NEW_CONSOLE, enviros, cDir, &si, &pi)

但仍重定向STARTUPINFO对象中的句柄,新控制台将显示一行The system cannot write to the specified device,该行恰好是Windows错误代码ERROR_WRITE_FAULT = 29的确切措辞在the MSDN docs中。仅对于那些无法按预期运行的程序会发生这种情况。在其他程序中,新控制台为空白,并且仍可以按预期运行。

我的第一个想法是权限问题,但是我对相关可执行文件的目录具有开放权限。

我尝试过的事情

  • 另一台装有Windows 10的计算机
  • MS Visual C ++运行时的不同版本
  • CreateProcess中显式设置工作目录
  • 向传递到SECURITY_ATTRIBUTES的{​​{1}}对象添加超许可DACL
  • 向传递到CreateProcess的{​​{1}}对象添加超许可DACL
  • 将所有内容移动到Windows用户目录下的新创建的目录中
  • 以管理员身份运行

更深

谢谢@PaulSanders的建议。

为了帮助可能需要帮助的任何人,我提供了a modified version of the RTConsole code from the codeproject page。我认为它应该在Visual Studio中进行编译,只需重新定位即可。在第135行附近,我在输出的前面添加了一个小字符串,该字符串采用了预期的路径。为了方便起见,我还在其中提供了预编译的版本。

EWBF miner是一个不起作用的软件示例。为了使用上面提供的代码进行快速测试,您可以运行

SECURITY_ATTRIBUTES

您将看到输出中不存在前置标志。

另一方面,使用ccminer,您将在运行时获得预期的行为

CreateConsoleScreenBuffer

1 个答案:

答案 0 :(得分:3)

Windows 10中新的控制台实现存在一个错误,其中高级别WriteConsoleWriteFile到非活动屏幕缓冲区,而总是写入活动屏幕缓冲区。低级WriteConsoleOutput[Character]可以正常工作。使用旧版控制台也可以。您可以在属性对话框中启用旧版控制台。


请注意,如果进程由于CREATE_NEW_CONSOLE标志而分配了新的控制台,则该进程不能在父级控制台的屏幕缓冲区中使用继承的句柄。尝试写入屏幕缓冲文件将失败,因为它未绑定到调用方的附加控制台(即conhost.exe实例)。

已绑定的控制台文件包括“ CON”,“ CONIN $”,“ CONOUT $”和来自CreateConsoleScreenBuffer的新屏幕缓冲区。还有一些未绑定的输入和输出控制台文件,这些文件被设置为分配新控制台时的标准句柄(例如,通过AllocConsole())。这些句柄访问任何连接的控制台[*]的输入缓冲区和活动屏幕缓冲区。请注意,进程可以具有绑定到多个控制台的控制台文件的句柄,并且可以使用AttachConsole在控制台之间切换。

还请注意,某些程序打开“ CONOUT $”而不是写入StandardOutputStandardError句柄,尤其是在它们需要控制台而不是标准句柄的情况下(例如,管道或磁盘文件)。在这种情况下,在hStdOutput中设置STARTUPINFO是不够的。您必须通过SetConsoleActiveScreenBuffer暂时使新的屏幕缓冲区处于活动状态。这不会影响调用方的标准句柄。它在连接的控制台中设置活动屏幕缓冲区,这是“ CONOUT $”打开的内容。子进程退出后,或者您知道子进程已经打开并写入新的屏幕缓冲区之后,可以恢复以前的屏幕缓冲区。


[*]在Windows 8+中,这些控制台文件由condrv.sys设备驱动程序实现,并在“ \ Device \ ConDrv”上打开。它们分别被命名为“控制台”,“ CurrentIn”,“ CurrentOut”,“ ScreenBuffer”,“ Input”和“ Output”。控制台连接本身的文件名是“ Connect”。在内部将后者作为进程ConsoleHandle打开,在某些情况下,控制台API会隐式使用该进程(例如GetConsoleWindowGetConsoleCPGetConsoleTitle)。

相关问题