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

CaptureConsole.DLL - 所有编译器通用的控制台输出重定向器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (40投票s)

2009年1月31日

CPOL

7分钟阅读

viewsIcon

230385

downloadIcon

3028

捕获控制台输出

引言

您可以在网上找到很多用于捕获控制台输出的代码。
但通常,您只能找到为特定编程语言编写的类。
这个易于使用的 DLL 的最大优点是,您可以将其用于所有项目,无论编程语言或编译器如何

只需在您的应用程序中编写 2 行代码,您就可以加载此 DLL,它将

  1. 在后台无声地执行控制台应用程序或 DOS 脚本
  2. 等待控制台完成
  3. 通过 stdoutstderr 将所有打印的输出作为字符串返回给调用应用程序

特点

  1. 对于任何想要启动控制台进程/脚本并需要控制台输出的应用程序来说,这个 DLL **极其容易**使用。
  2. 这个 DLL 中唯一的导出函数 "Execute" 使用与所有 Windows DLL 相同的**调用约定**。(WINAPI = __stdcall
  3. 您可以在支持 API 调用的**任何编译器**中的**任何编程语言**中使用此 DLL。(如果您可以调用 Kernel32.dll,您也可以调用 CaptureConsole.dll
  4. 此下载包含一个用于 C++、Visual Basic 6、VB.NET 和 C# 的**演示应用程序**,演示了如何加载 DLL 并调用 "Execute" 函数。
  5. 该 DLL 是**线程安全的**:您可以从不同的线程同时执行多个控制台应用程序。
  6. 您可以选择是否要**分开** stdoutstderr,还是将它们混合输出。
  7. 控制台应用程序的**退出代码**将返回给调用者。
  8. 您可以为控制台应用程序定义**工作目录**(GetCurrentDirectory)。
  9. 您可以将额外的**环境变量**(GetEnvironmentVariable)传递给控制台应用程序,或替换现有变量。
  10. 该 DLL 可以编译为**Unicode**(导出 ExecuteW)或 ANSI(导出 ExecuteA)。该 DLL 可以编译为**32 位**和**64 位**。
  11. 该 DLL 是一个 MFC C++ 项目,但由于 MFC 已**静态链接**,因此不需要外部 MFCxx.DLL。(CaptureConsole.DLL 仅依赖于标准的 Windows DLL)
  12. 该 DLL 内部使用 CString 并导出 BSTR,因此**缓冲区溢出**是不可能的。如果您的控制台打印了 50MB 的文本输出,那也不是问题。
  13. 在启动控制台进程时可能发生的**所有 API 错误**都会被处理并作为**人类可读的错误**消息返回。
  14. 您可以指定一个可选的**超时**。如果超时,控制台进程将被终止。这可以防止在服务器上使用时出现死进程。
  15. 特殊字符(如 äöüáéú)作为命令行参数传递时会被转换为 DOS **代码页**。
  16. 当返回给调用应用程序时,特殊字符会被转换回 ANSI 代码页。

实时捕获

如果您需要**实时**的控制台输出,那么这个项目不适合您。
CaptureConsole.DLL 等待控制台进程退出,然后**再**返回 stdout 和 stderr。

如果您需要实时控制台输出,请阅读 Oliver 的文章,该文章直接从控制台缓冲区读取。
但是 Oliver 的技术有严重的缺点:

  1. 代码薄弱:如果控制台应用程序打印速度快于缓冲区读取速度,或者控制台滚动,您可能会丢失字符。
  2. 您无法分开获取 stdout 和 stderr。
  3. 您需要一个额外的 EXE 文件来调用控制台进程。
  4. 您必须将接收管道的代码实现到您的主应用程序中。这段代码非常复杂,有很多陷阱。调用 DLL 中的一个函数来完成所有繁琐的工作要简单得多。

工作原理

CaptureConsole.DLL 使用 CreateProcess() 来启动控制台进程。
stdout 和 stderr 的输出被重定向到一个或两个管道,然后将打印的字符发送到调用应用程序。
在控制台应用程序运行时,调用进程会读取管道,并将输出存储在 string 中。
当控制台应用程序退出后,string 被返回给调用应用程序。

一个非常愚蠢的微软设计

当控制台应用程序使用 printf()perror() 编写其输出时,CRT 中的一个**愚蠢**设计定义打印的字符**不会立即**发送到管道。
只要控制台不写入 stderr,这**无关紧要**。
如果您不关心 stdout 和 stderr 的原始顺序,这**也无关紧要**。

如果您的控制台应用程序执行此操作

printf("Text  1");
perror("Error 1");
printf("Text  2");
perror("Error 2");

您会在控制台窗口中看到相同顺序的输出。

Text  1
Error 1
Text  2
Error 2

但是,如果输出被重定向到管道,微软会将打印的字符写入一个内部缓冲区,并在控制台退出时一起发送到管道。
即使您使用相同的一个管道捕获 stdout 和 stderr,您也会得到错误的顺序。

Text  1
Text  2
Error 1
Error 2
在启动控制台应用程序的调用进程中,您**无能为力**来影响此行为!
如果您有控制台进程的源代码,可以在 main() 开始时包含以下命令来关闭此愚蠢的缓冲:
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);

另一个选择是使用 _write() 而不是 printf(),或者调用 fflush()

如何使用此 DLL

如果 DLL 编译为**Unicode**,它将导出 ExecuteW 函数。
如果 DLL 编译为**MBCS**,它将导出 ExecuteA 函数。

ExecuteA/W 参数

  • s_CommandLine = 要执行的完整命令行。例如:"C:\Test\Test.bat Param1 Param2"
  • u32_FirstConvert = 0 -> 关闭命令行参数代码页转换
  • u32_FirstConvert > 0 -> 第一个要转换为 DOS 代码页的命令行参数(有关更多详细信息,请参阅下一章)
  • s_CurrentDir = 控制台应用程序的当前工作目录,如果未使用则为 null
  • s_Environment = 要传递给控制台应用程序的附加环境变量"UserVar1=Value1\nUserVar2=Value2\n"
    您还可以用自己的值覆盖系统变量。如果未使用,请传递 null
  • b_SeparatePipes = true -> 使用两个单独的管道捕获 stdout 和 stderr,并将它们分别返回到 s_StdOuts_StdErr
  • b_SeparatePipes = false -> 使用一个公共管道捕获 stdout 和 stderr,并将它们全部返回到 s_StdOut
  • u32_Timeout = 0 -> 无超时
  • u32_Timeout > 0 -> 超时(毫秒),在此之后控制台进程将被终止。

ExecuteA/W 返回值

返回控制台应用程序的退出代码以及 string s_ApiErrors_StdOuts_StdErr
如果 s_ApiError 非空,则表示在创建控制台进程或通信管道时发生了错误。您将收到人类可读的错误消息。

重要提示:您必须**始终**检查 s_ApiError。如果此 string 非空,则其他返回值无效!

重要提示:不要忘记之后使用 SysFreeString() 释放 BSTR,以避免内存泄漏!
在演示项目中,您可以看到如何正确执行此操作。
在 .NET 中这不需要,因为封送处理会自动为您释放 string

C#

[DllImport("CaptureConsole.dll", EntryPoint="ExecuteW", CharSet=CharSet.Unicode)]
static extern UInt32 ExecuteW(string   s_Commandline, 
                              UInt32 u32_FirstConvert,
                              string   s_CurrentDir, 
                              string   s_Environment, 
                              bool     b_SeparatePipes,
                              UInt32 u32_Timeout,
                              [MarshalAs(UnmanagedType.BStr)] out string s_ApiError, 
                              [MarshalAs(UnmanagedType.BStr)] out string s_StdOut, 
                              [MarshalAs(UnmanagedType.BStr)] out string s_StdErr);

string s_ApiError, s_StdOut, s_StdErr;
UInt32 u32_ExitCode = ExecuteW(@"C:\Test\Console.exe Hello Wörld", 1, null, null, 
                               true, 120000, out s_ApiError, out s_StdOut, out s_StdErr);

VB .NET

<DllImport("CaptureConsole.dll", EntryPoint:="ExecuteW", CharSet:=CharSet.Unicode)> _
Public Shared Function ExecuteW(ByVal   s_Commandline   As String,  _
                                ByVal u32_FirstConvert  As Int32,   _
                                ByVal   s_CurrentDir    As String,  _
                                ByVal   s_Environment   As String,  _
                                ByVal   b_SeparatePipes As Boolean, _
                                ByVal u32_Timeout       As Int32,   _
                                <MarshalAs(UnmanagedType.BStr)> ByRef s_ApiError As String, _
                                <MarshalAs(UnmanagedType.BStr)> ByRef s_StdOut   As String, _
                                <MarshalAs(UnmanagedType.BStr)> ByRef s_StdErr   As String) As UInt32

Dim s_ApiError, s_StdOut, s_StdErr As String
Dim u32_ExitCode As UInt32 = ExecuteW("C:\Test\Console.exe Hello Wörld", 1, Nothing, Nothing, 
                                      True, 120000, s_ApiError, s_StdOut, s_StdErr)

C++

typedef DWORD (WINAPI* tExecute)(const WCHAR*, DWORD, const WCHAR*, const WCHAR*, BOOL, DWORD, BSTR*, BSTR*, BSTR*);
  
HMODULE h_Dll = LoadLibraryW(L"CaptureConsole.dll"); 
tExecute f_Execute = (tExecute)GetProcAddress(h_Dll, "ExecuteW");

BSTR s_ApiError, s_StdOut, s_StdErr;
DWORD u32_ExitCode = f_Execute(L"C:\\Test\\Console.exe Hello Wörld", 1, NULL, NULL, 
                               TRUE, 120000, &s_ApiError, &s_StdOut, &s_StdErr);

VB 6

Private Declare Function ExecuteW Lib "CaptureConsole" (
        ByVal s_CommandLine As Long, 
        ByVal s32_FirstConvert As Long,
        ByVal s_CurrentDir As Long, 
        ByVal s_Environment As Long, 
        ByVal b_SeparatePipes As Boolean, 
        ByVal s32_Timeout As Long,
        ByRef s_ApiError As Long, 
        ByRef s_StdOut As Long, 
        ByRef s_StdErr As Long) As Long
  
Dim bs_ApiError, bs_StdOut, bs_StdErr, s32_ExitCode As Long
s32_ExitCode = ExecuteW(StrPtr("C:\Test\Console.exe Hello Wörld"), 1, 0, 0, _
                        True, 120000, bs_ApiError, bs_StdOut, bs_StdErr)

Dim s_ApiError, s_StdOut, s_StdErr As String
s_ApiError = ConvertBSTR(bs_ApiError)
s_StdOut   = ConvertBSTR(bs_StdOut)
s_StdErr   = ConvertBSTR(bs_StdErr)

您可以在演示项目中找到 ConvertBSTR() 函数的定义。

代码页转换

控制台应用程序和 DOS 脚本使用 OEM 代码页来显示 ASCII 码 127 以上的字符。
为了正确显示 äöüáéú 等字符,必须进行转换。

CaptureConsole.dll 将所有**输出**(stdout 和 stderr)从 OEM(DOS 代码页)转换为 ANSI/Unicode,然后再返回给调用应用程序。

但是,到控制台应用程序或 DOS 脚本的**输入**则更为复杂。
如果您调用 Console.exe 应用程序,命令行参数必须在 CaptureConsole.dll 中进行转换。
但是,如果您调用 Console.bat 脚本,该脚本将由 CMD.EXE 执行,它会自动执行此转换。
因此,当您调用 DOS 脚本时,必须在 CaptureConsole.dll 中关闭转换(u32_FirstConvert = 0)。

但在其他不需要转换的情况下也存在

java -cp "C:\Programación\Transacción.jar" Classname Hello Wörld

如果您传递的命令行参数包含带有特殊字符的路径或文件名,则该路径/文件名不应被转换。
在这种情况下,转换必须从第三个参数开始,因为第一个 "-cp" 和第二个 "C:\Programación\Transacción.jar" 不应被转换。
所以这里您必须设置 u32_FirstConvert = 3

如果情况更复杂,请在 CaptureConsole.dll 中关闭转换(u32_FirstConvert = 0),并在您的调用应用程序中转换命令行。

C:\Железнодо\Console.exe Param1 Param2

命令行第一部分始终是要执行的 EXE、BAT 或 CMD 文件。
如果您在 Unicode 编译的 CaptureConsole.dll 中使用 ExecuteW,此路径可能包含中文、俄语或希腊语 Unicode 字符。
但是,所有后续参数都不应超过 ASCII 码 255!!

历史

  • 2009年1月31日:初始发布
  • 2009 年 2 月 3 日:添加了 Visual Basic .NET 的代码示例。
  • 2009 年 2 月 6 日:添加了环境变量、当前目录,并修复了一个管道问题。
  • 2009 年 10 月 30 日:更新了源代码。
  • 2009 年 11 月 19 日:更新了源代码。

Elmü

© . All rights reserved.