请参阅下面的最终工作代码
我有一种在内存中执行批处理脚本的方法,方法是传递命令列表并在新的
Process
中执行它们。我使用这种方法来运行 psql
和 gpg
命令之类的东西,它非常适合我的用例,这样我就不必在网络上保留带有用户凭据等的随机 .BAT
文件。
唯一的“问题”是,我目前必须维护该方法的多个副本,以应对输出或错误处理程序中我需要的一些相对较小的变化(
.OutputDataReceived
和.ErrorDataReceived
)。我想做的基本上是创建一个“BatchFile
”类,它通过构造函数接受这些事件的自定义DataReceivedEventHandler
。
这是我当前每次需要运行批处理文件时复制/粘贴的原始代码:
Private Sub ExecuteBatchInMemory(ByVal Commands As List(Of String), ByVal CurrentUser As NetworkCredential)
Dim BatchStartInfo As New ProcessStartInfo
Dim BatchError As String = String.Empty
With BatchStartInfo
.FileName = "cmd.exe"
.WorkingDirectory = Environment.SystemDirectory
.Domain = CurrentUser.Domain
.UserName = CurrentUser.UserName
.Password = CurrentUser.SecurePassword
.UseShellExecute = False
.ErrorDialog = False
.WindowStyle = ProcessWindowStyle.Normal
.CreateNoWindow = False
.RedirectStandardOutput = True
.RedirectStandardError = True
.RedirectStandardInput = True
End With
Using BatchProcess As New Process
Dim BATExitCode As Integer = 0
Dim CommandIndex As Integer = 0
Dim ProcOutput As New Text.StringBuilder
Dim ProcError As New Text.StringBuilder
With BatchProcess
.StartInfo = BatchStartInfo
Using OutputWaitHandle As New Threading.AutoResetEvent(False)
Using ErrorWaitHandle As New Threading.AutoResetEvent(False)
Dim ProcOutputHandler = Sub(sender As Object, e As DataReceivedEventArgs)
If e.Data Is Nothing Then
OutputWaitHandle.Set()
Else
ProcOutput.AppendLine(e.Data)
End If
End Sub
'>> This is effectively the DataReceivedEventHandler for
' most of the "batch files" that execute psql.exe
Dim ProcErrorHandler = Sub(sender As Object, e As DataReceivedEventArgs)
If e.Data Is Nothing Then
ErrorWaitHandle.Set()
ElseIf e.Data.ToUpper.Contains("FAILED: ") Then
ProcError.AppendLine(e.Data)
End If
End Sub
AddHandler .OutputDataReceived, ProcOutputHandler
AddHandler .ErrorDataReceived, ProcErrorHandler
.Start()
.BeginOutputReadLine()
.BeginErrorReadLine()
While Not .HasExited
If .Threads.Count >= 1 AndAlso CommandIndex < Commands.Count Then
.StandardInput.WriteLine(Commands(Math.Min(System.Threading.Interlocked.Increment(CommandIndex), CommandIndex - 1)))
End If
End While
BATExitCode = .ExitCode
BatchError = ProcError.ToString.Trim
.WaitForExit()
RemoveHandler .OutputDataReceived, ProcOutputHandler
RemoveHandler .ErrorDataReceived, ProcErrorHandler
End Using
End Using
End With
If BATExitCode <> 0 OrElse (BatchError IsNot Nothing AndAlso Not String.IsNullOrEmpty(BatchError.Trim)) Then
Throw New BatchFileException(BATExitCode, $"An error occurred: {BatchError}")
End If
End Using
End Sub
根据我尝试从特定批处理文件的命令行捕获的内容,我将修改
ProcErrorHandler
或 ProcOutputHandler
以查找 e.Data
中的特定值。在这个特定的示例中,我正在寻找来自 GnuPG (gpg.exe
) 的错误,这些错误表明文件加密或解密失败。对于 psql
版本,我可能会更改 ProcErrorHandler
来查找 FATAL
或其他内容。
因此,我没有定义
ProcOutputHandler
和 ProcErrorHandler
与其余代码一致,而是开始使用 BatchFile
类,目前它看起来像这样:
Imports System.Net
Public Class BatchFile
Implements IDisposable
Private STDOUTWaitHandle As Threading.AutoResetEvent
Private STDERRWaitHandle As Threading.AutoResetEvent
Private Disposed As Boolean
Private STDOUTHandler As DataReceivedEventHandler
Private STDERRHandler As DataReceivedEventHandler
Public Sub New()
Initialize()
End Sub
Public Sub New(ByVal OutputHandler As DataReceivedEventHandler, ByVal ErrorHandler As DataReceivedEventHandler)
Initialize()
STDOUTHandler = OutputHandler
STDERRHandler = ErrorHandler
End Sub
Public Sub Execute(ByVal Commands As List(Of String), Optional ByVal User As NetworkCredential = Nothing)
Dim BatchStartInfo As New ProcessStartInfo
Dim BatchError As String = String.Empty
Dim CurrentUser As NetworkCredential = User
If User Is Nothing Then
CurrentUser = CredentialCache.DefaultNetworkCredentials
End If
With BatchStartInfo
.FileName = "cmd.exe"
.WorkingDirectory = Environment.SystemDirectory
.Domain = CurrentUser.Domain
.UserName = CurrentUser.UserName
.Password = CurrentUser.SecurePassword
.UseShellExecute = False
.ErrorDialog = False
.WindowStyle = ProcessWindowStyle.Normal
.CreateNoWindow = False
.RedirectStandardOutput = True
.RedirectStandardError = True
.RedirectStandardInput = True
End With
Using BatchProcess As New Process
Dim BATExitCode As Integer = 0
Dim CommandIndex As Integer = 0
Dim ProcOutput As New Text.StringBuilder
Dim ProcError As New Text.StringBuilder
With BatchProcess
.StartInfo = BatchStartInfo
.EnableRaisingEvents = True
AddHandler .OutputDataReceived, STDOUTHandler
AddHandler .ErrorDataReceived, STDERRHandler
End With
End Using
End Sub
Private Sub Initialize()
STDOUTWaitHandle = New Threading.AutoResetEvent(False)
STDERRWaitHandle = New Threading.AutoResetEvent(False)
End Sub
Protected Overridable Sub Dispose(Disposing As Boolean)
If Not Disposed Then
If Disposing Then
If STDOUTWaitHandle IsNot Nothing Then
STDOUTWaitHandle.Dispose()
End If
If STDERRWaitHandle IsNot Nothing Then
STDERRWaitHandle.Dispose()
End If
End If
Disposed = True
End If
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
End Class
我遇到的问题是尝试实际创建事件处理程序方法以传递给构造函数以分配给
STDOUTHandler
和 STDERRHANDLER
。我看过几个不同的例子,包括:
BatchFile
类外部传递到构造函数中,因为我没有要分配给的值处理程序的
sender
和
DataReceivedEventArgs
参数。我建立了一个简单的方法:
Friend Sub TestHandler(ByVal sender as Object, ByVal e As DataReceivedEventArgs)
Console.WriteLine(e.Data)
End Sub
但是,当我尝试声明一个新的BatchFile
时:
Dim testbatch As New BatchFile(TestHandler, TestHandler)
编译器显然会抛出一个错误,表明参数参数未指定。我也尝试过:
Dim testbatch As New BatchFile(DataReceivedEventHandler(AddressOf TestHandler), DataReceivedEventHandler(AddressOf TestHandler))
但这不起作用,因为 DataReceivedEventHandler
是一种类型,不能在表达式中使用。我尝试过的其他变体也得到了类似的结果,所以我不确定此时该怎么做。任何帮助或指示将不胜感激。当然,仍然存在一个“问题”,超出了这个问题的范围,那就是在类外部的处理程序定义中包含
OutputWaitHandle
和
ErrorWaitHandle
对象,但我相信我可以弄清楚一旦我将处理程序方法正确传递到我的构造函数中,就会出现这个问题。
是只是太笨了,我相信我刚刚想通了。保留上面的 TestHandler
方法,看来我要么试图让它变得太简单,要么太复杂。看起来可行的
BatchFile
声明如下:
Dim testbatch As New BatchFile(AddressOf TestHandler, AddressOf TestHandler)
我使用一些简单的 GnuPG 解密命令进行了快速测试,一切似乎都完全按照预期进行。我将 STDOUT 的结果打印到控制台,当我故意引入逻辑错误时,它会将 STDERR 的内容打印到控制台。我意识到这是一个“简单的修复”,但因为我已经经历了尽可能多的迭代,所以我有点犹豫。为了防止其他人经历同样的挫折,我将在这里留下这个问题/答案以及完整的工作代码:
Module BatchCommandTest
Sub Main()
Dim testbatch As New BatchFile(AddressOf TestHandler, AddressOf TestHandler)
testbatch.Execute(New List(Of String) From {"CLS", "C:\GnuPG\gpg.exe --batch --verbose --passphrase <SECRET PASSWORD> --output ""C:\Temp\mytest.pdf"" --decrypt ""C:\Temp\test.pgp""", "EXIT"}, CredentialCache.DefaultNetworkCredentials)
End Sub
Friend Sub TestHandler(ByVal sender As Object, ByVal e As DataReceivedEventArgs)
Console.WriteLine(e.Data)
End Sub
End Module
BATCHFILE
班级
Imports System.Net
Public Class BatchFile
Implements IDisposable
Private STDOUTWaitHandle As Threading.AutoResetEvent
Private STDERRWaitHandle As Threading.AutoResetEvent
Private Disposed As Boolean
Private STDOUTHandler As DataReceivedEventHandler
Private STDERRHandler As DataReceivedEventHandler
Public Sub New()
Initialize()
End Sub
Public Sub New(ByVal OutputHandler As Action(Of Object, DataReceivedEventArgs), ByVal ErrorHandler As Action(Of Object, DataReceivedEventArgs))
Initialize()
STDOUTHandler = TryCast(Cast(OutputHandler, GetType(DataReceivedEventHandler)), DataReceivedEventHandler)
STDERRHandler = TryCast(Cast(ErrorHandler, GetType(DataReceivedEventHandler)), DataReceivedEventHandler)
End Sub
Public Sub Execute(ByVal Commands As List(Of String), Optional ByVal User As NetworkCredential = Nothing)
Dim BatchStartInfo As New ProcessStartInfo
Dim BatchError As String = String.Empty
Dim CurrentUser As NetworkCredential = User
If User Is Nothing Then
CurrentUser = CredentialCache.DefaultNetworkCredentials
End If
With BatchStartInfo
.FileName = "cmd.exe"
.WorkingDirectory = Environment.SystemDirectory
.Domain = CurrentUser.Domain
.UserName = CurrentUser.UserName
.Password = CurrentUser.SecurePassword
.UseShellExecute = False
.ErrorDialog = False
.WindowStyle = ProcessWindowStyle.Normal
.CreateNoWindow = False
.RedirectStandardOutput = True
.RedirectStandardError = True
.RedirectStandardInput = True
End With
Using BatchProcess As New Process
Dim BATExitCode As Integer = 0
Dim CommandIndex As Integer = 0
Dim ProcOutput As New Text.StringBuilder
Dim ProcError As New Text.StringBuilder
With BatchProcess
.StartInfo = BatchStartInfo
AddHandler .OutputDataReceived, STDOUTHandler
AddHandler .ErrorDataReceived, STDERRHandler
.Start()
.BeginOutputReadLine()
.BeginErrorReadLine()
While Not .HasExited
If .Threads.Count >= 1 AndAlso CommandIndex < Commands.Count Then
.StandardInput.WriteLine(Commands(Math.Min(System.Threading.Interlocked.Increment(CommandIndex), CommandIndex - 1)))
End If
End While
.WaitForExit()
BATExitCode = .ExitCode
BatchError = ProcError.ToString.Trim
RemoveHandler .OutputDataReceived, STDOUTHandler
RemoveHandler .ErrorDataReceived, STDERRHandler
If BATExitCode <> 0 OrElse Not (BatchError Is Nothing OrElse String.IsNullOrEmpty(BatchError.Trim)) Then
Throw New BatchFileException(BATExitCode, $"An error occurred executing the in-memory batch script: {BatchError}")
End If
End With
End Using
End Sub
Private Sub Initialize()
STDOUTWaitHandle = New Threading.AutoResetEvent(False)
STDERRWaitHandle = New Threading.AutoResetEvent(False)
End Sub
Protected Overridable Sub Dispose(Disposing As Boolean)
If Not Disposed Then
If Disposing Then
If STDOUTWaitHandle IsNot Nothing Then
STDOUTWaitHandle.Dispose()
End If
If STDERRWaitHandle IsNot Nothing Then
STDERRWaitHandle.Dispose()
End If
End If
Disposed = True
End If
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
' Cast() method from Faithlife Code Blog (https://faithlife.codes/blog/2008/07/casting_delegates/)
Function Cast(ByVal source As [Delegate], ByVal type As Type) As [Delegate]
If source Is Nothing Then
Return Nothing
End If
Dim delegates As [Delegate]() = source.GetInvocationList()
If delegates.Length = 1 Then
Return [Delegate].CreateDelegate(type, delegates(0).Target, delegates(0).Method)
End If
Dim delegatesDest As [Delegate]() = New [Delegate](delegates.Length - 1) {}
For nDelegate As Integer = 0 To delegates.Length - 1
delegatesDest(nDelegate) = [Delegate].CreateDelegate(type, delegates(nDelegate).Target, delegates(nDelegate).Method)
Next
Return [Delegate].Combine(delegatesDest)
End Function
End Class
您会注意到这个“最终”迭代与我最初发布的内容有几个重要的区别:
Execute()
类的
BatchFile
方法更加“完整”,以匹配原始
ExecuteBatchInMemory()
方法的功能
Action(Of Object, DataReceivedEventArgs)
而不是
DataReceivedEventHandler
类型作为参数。我想我在某个时候做了这个改变,但忘记在其他地方注意到它,所以我想在这里指出它。
帖子中的
Cast()
方法,将 Action(Of Object, DataReceivedEventArgs)
参数转换为特定的、正确的
DataReceivedEventHandler
类型,以便该方法可以根据需要订阅/取消订阅它.