CaptureConsole.DLL - 所有编译器通用的控制台输出重定向器
捕获控制台输出
引言
您可以在网上找到很多用于捕获控制台输出的代码。
但通常,您只能找到为特定编程语言编写的类。
这个易于使用的 DLL 的最大优点是,您可以将其用于所有项目,无论编程语言或编译器如何。
只需在您的应用程序中编写 2 行代码,您就可以加载此 DLL,它将
- 在后台无声地执行控制台应用程序或 DOS 脚本
- 等待控制台完成
- 通过
stdout
和stderr
将所有打印的输出作为字符串返回给调用应用程序
特点
- 对于任何想要启动控制台进程/脚本并需要控制台输出的应用程序来说,这个 DLL **极其容易**使用。
- 这个 DLL 中唯一的导出函数 "
Execute
" 使用与所有 Windows DLL 相同的**调用约定**。(WINAPI
=__stdcall
) - 您可以在支持 API 调用的**任何编译器**中的**任何编程语言**中使用此 DLL。(如果您可以调用 Kernel32.dll,您也可以调用 CaptureConsole.dll)
- 此下载包含一个用于 C++、Visual Basic 6、VB.NET 和 C# 的**演示应用程序**,演示了如何加载 DLL 并调用 "
Execute
" 函数。 - 该 DLL 是**线程安全的**:您可以从不同的线程同时执行多个控制台应用程序。
- 您可以选择是否要**分开**
stdout
和stderr
,还是将它们混合输出。 - 控制台应用程序的**退出代码**将返回给调用者。
- 您可以为控制台应用程序定义**工作目录**(
GetCurrentDirectory
)。 - 您可以将额外的**环境变量**(
GetEnvironmentVariable
)传递给控制台应用程序,或替换现有变量。 - 该 DLL 可以编译为**Unicode**(导出
ExecuteW
)或 ANSI(导出ExecuteA
)。该 DLL 可以编译为**32 位**和**64 位**。 - 该 DLL 是一个 MFC C++ 项目,但由于 MFC 已**静态链接**,因此不需要外部 MFCxx.DLL。(CaptureConsole.DLL 仅依赖于标准的 Windows DLL)
- 该 DLL 内部使用
CString
并导出 BSTR,因此**缓冲区溢出**是不可能的。如果您的控制台打印了 50MB 的文本输出,那也不是问题。 - 在启动控制台进程时可能发生的**所有 API 错误**都会被处理并作为**人类可读的错误**消息返回。
- 您可以指定一个可选的**超时**。如果超时,控制台进程将被终止。这可以防止在服务器上使用时出现死进程。
- 特殊字符(如 äöüáéú)作为命令行参数传递时会被转换为 DOS **代码页**。
- 当返回给调用应用程序时,特殊字符会被转换回 ANSI 代码页。
实时捕获
如果您需要**实时**的控制台输出,那么这个项目不适合您。
CaptureConsole.DLL 等待控制台进程退出,然后**再**返回 stdout 和 stderr。
如果您需要实时控制台输出,请阅读 Oliver 的文章,该文章直接从控制台缓冲区读取。
但是 Oliver 的技术有严重的缺点:
- 代码薄弱:如果控制台应用程序打印速度快于缓冲区读取速度,或者控制台滚动,您可能会丢失字符。
- 您无法分开获取 stdout 和 stderr。
- 您需要一个额外的 EXE 文件来调用控制台进程。
- 您必须将接收管道的代码实现到您的主应用程序中。这段代码非常复杂,有很多陷阱。调用 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_StdOut
和s_StdErr
。b_SeparatePipes = false
-> 使用一个公共管道捕获 stdout 和 stderr,并将它们全部返回到s_StdOut
。u32_Timeout = 0
-> 无超时u32_Timeout > 0
-> 超时(毫秒),在此之后控制台进程将被终止。
ExecuteA/W 返回值
返回控制台应用程序的退出代码以及 string
s_ApiError
、s_StdOut
、s_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ü