重定向任意控制台的输入/输出






4.91/5 (61投票s)
2003年11月28日
3分钟阅读

451090

15500
如何以简单、优雅的方式重定向任意控制台的输入/输出
引言
重定向控制台应用程序的输入/输出既有趣又有用。您可以将子程序的输出显示在窗口中(就像 Visual Studio 的输出窗口一样),或者在输出字符串中搜索某些关键字,以确定子进程是否已成功完成工作。一个老旧的、"丑陋"的 DOS 程序可以成为您精美的 Win32 GUI 程序的一个有用组件。
我的想法是开发一个简单易用的重定向类,它可以重定向任意控制台,并且不受子进程行为的影响。
背景
重定向控制台进程的输入/输出的技术非常简单:CreateProcess()
API 通过 STARTUPINFO
结构使我们能够重定向基于子控制台进程的标准句柄。因此,我们可以将这些句柄设置为管道句柄、文件句柄或任何我们可以读取和写入的句柄。这项技术的细节在 MSDN 中已得到清晰描述:HOWTO: Spawn Console Processes with Redirected Standard Handles.
然而,MSDN 的示例代码有两个大问题。首先,它假设子进程先发送输出,然后等待输入,然后刷新输出缓冲区并退出。如果子进程的行为不是这样,父进程就会挂起。原因在于 ReadFile()
函数会一直阻塞,直到子进程发送一些输出或退出。
其次,重定向 16 位控制台(包括基于控制台的 MS-DOS 应用程序)存在问题。在 Windows 9x 上,即使子进程已终止,ReadFile
仍然阻塞;在 Windows NT/XP 上,如果子进程是 DOS 应用程序,ReadFile
总是返回 FALSE
,错误代码设置为 ERROR_BROKEN_PIPE
。
解决 ReadFile 的阻塞问题
为了防止父进程被 ReadFile
阻塞,我们可以简单地将文件句柄作为 stdout
传递给子进程,然后监视该文件。更简单的方法是在调用 ReadFile()
之前调用 PeekNamedPipe()
函数。PeekNamedPipe
函数检查管道中数据的相关信息,然后立即返回。如果没有可用的数据在管道中,则不要调用 ReadFile
。
通过在 ReadFile
之前调用 PeekNamedPipe
,我们也解决了在 Windows 9x 上重定向 16 位控制台的阻塞问题。
CRedirector
类首先创建管道并启动子进程,然后创建一个监听线程来监视子进程的输出。这是监听线程的主循环
for (;;)
{
// redirect stdout till there's no more data.
nRet = pRedir->RedirectStdout();
if (nRet <= 0)
break;
// check if the child process has terminated.
DWORD dwRc = ::WaitForMultipleObjects(
2, aHandles, FALSE, pRedir->m_dwWaitTime);
if (WAIT_OBJECT_0 == dwRc) // the child process ended
{
...
break;
}
if (WAIT_OBJECT_0+1 == dwRc) // m_hEvtStop was signalled, exit
{
...
break;
}
}
这是 RedirectStdout()
函数的主循环
for (;;)
{
DWORD dwAvail = 0;
if (!::PeekNamedPipe(m_hStdoutRead, NULL, 0, NULL,
&dwAvail, NULL)) // error, the child process might ended
break;
if (!dwAvail) // no data available, return
return 1;
char szOutput[256];
DWORD dwRead = 0;
if (!::ReadFile(m_hStdoutRead, szOutput, min(255, dwAvail),
&dwRead, NULL) || !dwRead)
// error, the child process might ended
break;
szOutput[dwRead] = 0;
WriteStdOut(szOutput); // display the output
}
WriteStdOut
是一个 virtual
成员函数。它在 CRedirector
类中什么也不做。但是,它可以被重写以实现我们的特定目标,就像我在演示项目中做的那样
int nSize = m_pWnd->GetWindowTextLength();
// m_pWnd points to a multiline Edit control
m_pWnd->SetSel(nSize, nSize);
m_pWnd->ReplaceSel(pszOutput);
// add the message to the end of Edit control
重定向 NT/2000/XP 上的基于 DOS 的控制台应用程序
MSDN 的解决方案是启动一个中间的 Win32 控制台应用程序,作为 Win32 父进程和 16 位基于控制台的子进程之间的存根进程。事实上,DOS 提示符程序(在 NT/XP 上是 cmd.exe,在 9x 上是 command.com)是一个天然的存根进程,我们只需要它。我们可以在 RedirDemo.exe 中进行测试
- 在 **Command** 编辑框中输入 'cmd.exe',然后按 **Run** 按钮。
- 在 **Input** 编辑框中输入 16 位基于控制台的应用程序的名称(例如 dosapp.exe),然后按 **Input** 按钮。现在我们可以看到 16 位控制台的输出。
- 在 **Input** 编辑框中输入 'exit',然后按 **Input** 按钮以终止 cmd.exe。
显然,这不是一个好的解决方案,因为它太复杂了。一个更有效的方法是使用批处理文件作为存根。像这样编辑 stub.bat 文件
%1 %2 %3 %4 %5 %6 %7 %8 %9
然后,运行类似 'stub.bat dosapp.exe
' 的命令,16 位 DOS 控制台应用程序就可以正常运行了。
许可证
本文没有明确的许可附加,但可能包含在文章文本或下载文件中。如有疑问,请通过下方的讨论区联系作者。作者可能使用的许可列表可以在这里找到。