65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (61投票s)

2003年11月28日

3分钟阅读

viewsIcon

451090

downloadIcon

15500

如何以简单、优雅的方式重定向任意控制台的输入/输出

Sample Image - redir.gif

引言

重定向控制台应用程序的输入/输出既有趣又有用。您可以将子程序的输出显示在窗口中(就像 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 中进行测试

  1. 在 **Command** 编辑框中输入 'cmd.exe',然后按 **Run** 按钮。
  2. 在 **Input** 编辑框中输入 16 位基于控制台的应用程序的名称(例如 dosapp.exe),然后按 **Input** 按钮。现在我们可以看到 16 位控制台的输出。
  3. 在 **Input** 编辑框中输入 'exit',然后按 **Input** 按钮以终止 cmd.exe

显然,这不是一个好的解决方案,因为它太复杂了。一个更有效的方法是使用批处理文件作为存根。像这样编辑 stub.bat 文件

%1 %2 %3 %4 %5 %6 %7 %8 %9

然后,运行类似 'stub.bat dosapp.exe' 的命令,16 位 DOS 控制台应用程序就可以正常运行了。

许可证

本文没有明确的许可附加,但可能包含在文章文本或下载文件中。如有疑问,请通过下方的讨论区联系作者。作者可能使用的许可列表可以在这里找到。

© . All rights reserved.