I mentioned in a previous post that I was launching command line utilities from an ASP.NET web app and capturing the output. I wrote a little multithreaded .Process wrapper class to encapsulate this behavior. It's nothing magical, but it is handy for these scenarios:
Dim cmd As String
cmd = "whoami.exe"
Dim s As New Shell
Console.WriteLine("executing " & cmd)
s.Execute(cmd)
Console.WriteLine("output:")
Console.Write(s.Output)
Console.WriteLine("error:")
Console.Write(s.Error)
Console.WriteLine("execution took " & _
s.ExecutionTime.ToString & " milliseconds")
Console.WriteLine("exit code was " & _
s.ExitCode.ToString)
Console.ReadLine()
Don't forget to set the .WorkingDirectory if your executables aren't in the default path.
The Shell class is currently synchronous-- code execution halts until the command returns or times out. If you have long running console processes, you might want to make this class asynchronous (eg, non-blocking) and raise events for things like console lines being written, command terminating, etcetera. This was added.
Code follows...
Imports System.Text
Imports System.IO
Imports System.Diagnostics
Imports System.Threading
''' <summary>
''' Execute a command line string and return the output and/or error.
''' </summary>
Public Class Shell
Implements IDisposable
Private _p As Process
Private _intMaxWaitMs As Integer = 120000
Private _blnDisposed As Boolean = False
Private _OutputBuilder As StringBuilder
Private _ErrorBuilder As StringBuilder
Private _blnGetOutput As Boolean = True
Private _blnGetError As Boolean = True
Private _blnLaunchInThread As Boolean = False
Private _strWorkingDirectory As String
Private _StartTime As DateTime
Private _blnCancelRequested As Boolean = False
Private Const _intSleepMs As Integer = 200
Private _OutputThread As Thread
Private _ErrorThread As Thread
Private _blnProcessLaunched As Boolean = False
Public Event OutputLine(ByVal LineText As String)
Public Event ExecutionComplete(ByVal TimedOut As Boolean)
''' <summary>
''' The working directory to be used by the process that is launched.
''' If left blank, will default to the whatever the current path is.
''' </summary>
Public Property WorkingDirectory() As String
Get
Return _strWorkingDirectory
End Get
Set(ByVal Value As String)
_strWorkingDirectory = Value
End Set
End Property
''' <summary>
''' capture any returned output from the command into the .Output string
''' </summary>
Public Property CaptureOutput() As Boolean
Get
Return _blnGetOutput
End Get
Set(ByVal Value As Boolean)
_blnGetOutput = Value
End Set
End Property
''' <summary>
''' capture any returned errors from the command into the .Error string
''' </summary>
Public Property CaptureError() As Boolean
Get
Return _blnGetError
End Get
Set(ByVal Value As Boolean)
_blnGetError = Value
End Set
End Property
''' <summary>
''' Maximum number of seconds to wait for the process to finish running.
''' Use Integer.MaxValue to specify infinite wait.
''' If the process is not finished in this time, it will be automatically killed.
''' </summary>
Public Property MaximumWaitSeconds() As Integer
Get
Return Convert.ToInt32(_intMaxWaitMs / 1000)
End Get
Set(ByVal Value As Integer)
_intMaxWaitMs = Value * 1000
End Set
End Property
''' <summary>
''' execute the command in a seperate thread, synchronously; if not set, execution is asynchronous (blocking)
''' </summary>
Public Property UseNewThread() As Boolean
Get
Return _blnLaunchInThread
End Get
Set(ByVal Value As Boolean)
_blnLaunchInThread = Value
End Set
End Property
''' <summary>
''' any returned output from the command. Only provided if .CaptureOutput is True.
''' </summary>
Public ReadOnly Property Output() As String
Get
If _OutputBuilder Is Nothing Then
Return ""
Else
Return _OutputBuilder.ToString
End If
End Get
End Property
''' <summary>
''' any returned errors from the command. Only provided if .CaptureError is True.
''' </summary>
Public ReadOnly Property [Error]() As String
Get
If _ErrorBuilder Is Nothing Then
Return ""
Else
Return _ErrorBuilder.ToString
End If
End Get
End Property
''' <summary>
''' command execution time in milliseconds. Returns zero until execution is complete.
''' </summary>
Public ReadOnly Property ExecutionTime() As Integer
Get
If _p Is Nothing Then Return 0
If Not ProcessHasExited() Then Return 0
Return Convert.ToInt32(New TimeSpan(_p.ExitTime.Ticks - _StartTime.Ticks).TotalMilliseconds)
End Get
End Property
''' <summary>
''' exit code for the command. Returns -1 until execution is complete.
''' </summary>
''' <remarks>
''' Developers usually indicate a successful exit by an ExitCode value of zero, and designate errors by nonzero
''' values that the calling method can use to identify the cause of an abnormal process termination.
''' It is not necessary to follow these guidelines, but they are the convention.
''' </remarks>
Public ReadOnly Property ExitCode() As Integer
Get
If _p Is Nothing Then Return -1
If Not ProcessHasExited() Then Return -1
Return _p.ExitCode
End Get
End Property
''' <summary>
''' Executes a command line and waits for it to finish. Check .Error and .Output for results.
''' Set .WorkingDirectory if your command is not fully pathed, or not in the path on this machine.
''' </summary>
''' <param name="Command">valid command line string to execute</param>
Public Sub Execute(ByVal Command As String)
StartProcess("cmd.exe", "/c """ & Command & """")
End Sub
''' <summary>
''' Cancels execution of the command if it is still running
''' </summary>
Public Sub CancelExecution()
_blnCancelRequested = True
End Sub
Private Function ProcessHasExited() As Boolean
If _p Is Nothing Then
Return True
End If
Return _p.HasExited
End Function
Private Sub LaunchThreadHandler()
'-- launch process
_p.Start()
_blnProcessLaunched = True
WaitForExit()
End Sub
Private Sub OutputThreadHandler()
Dim strLine As String
'-- this will run forever until the thread is aborted or suspended; this is by design
Do While True
If _blnProcessLaunched Then
If _p Is Nothing Then Exit Do
If _blnCancelRequested Then Exit Do
strLine = _p.StandardOutput.ReadLine
If Not strLine Is Nothing Then
_OutputBuilder.Append(strLine)
_OutputBuilder.Append(Environment.NewLine)
RaiseEvent OutputLine(strLine)
Else
'-- suspend
Thread.Sleep(0)
End If
Else
Thread.Sleep(20)
End If
Loop
End Sub
Private Sub ErrorThreadHandler()
Dim strLine As String
'-- this will run forever until the thread is aborted or suspended; this is by design
Do While True
If _blnProcessLaunched Then
If _p Is Nothing Then Exit Do
If _blnCancelRequested Then Exit Do
strLine = _p.StandardError.ReadLine
If Not strLine Is Nothing Then
_ErrorBuilder.Append(strLine)
_ErrorBuilder.Append(Environment.NewLine)
Else
'-- suspend
Thread.Sleep(0)
End If
Else
Thread.Sleep(20)
End If
Loop
End Sub
Private Sub StartProcess(ByVal strFileName As String, Optional ByVal strArguments As String = "")
Dim LaunchThread As Thread
_p = New Process
With _p.StartInfo
If Not _strWorkingDirectory Is Nothing Then
.WorkingDirectory = _strWorkingDirectory
End If
.FileName = strFileName
.Arguments = strArguments
.UseShellExecute = False
.CreateNoWindow = True
.RedirectStandardOutput = _blnGetOutput
.RedirectStandardError = _blnGetError
End With
_StartTime = DateTime.Now
If _blnLaunchInThread Then
LaunchThread = New Thread(New ThreadStart(AddressOf LaunchThreadHandler))
LaunchThread.Name = "ShellLaunchThread"
LaunchThread.Start()
Else
_p.Start()
_blnProcessLaunched = True
End If
'-- spawn threads to read in output and error as they are created
If _blnGetOutput Then
_OutputBuilder = New StringBuilder
_OutputThread = New Thread(New ThreadStart(AddressOf OutputThreadHandler))
_OutputThread.Name = "ShellOutputThread"
_OutputThread.Start()
End If
If _blnGetError Then
_ErrorBuilder = New StringBuilder
_ErrorThread = New Thread(New ThreadStart(AddressOf ErrorThreadHandler))
_ErrorThread.Name = "ShellErrorThread"
_ErrorThread.Start()
End If
If LaunchThread Is Nothing Then
WaitForExit()
End If
End Sub
Private Sub WaitForExit()
'-- wait for process to exit, or else we time out
_blnCancelRequested = False
Dim intWaitedMs As Integer = 0
Do While (Not ProcessHasExited()) And (intWaitedMs < _intMaxWaitMs) And (Not _blnCancelRequested)
Thread.Sleep(_intSleepMs)
intWaitedMs += _intSleepMs
Loop
CloseThreads()
'-- if we timed out, kill the process
If (intWaitedMs >= _intMaxWaitMs) Or _blnCancelRequested Then
_p.Kill()
RaiseEvent ExecutionComplete(True)
Else
RaiseEvent ExecutionComplete(False)
End If
End Sub
Private Sub CloseThreads()
If Not _OutputThread Is Nothing Then
If _OutputThread.IsAlive() Then
_OutputThread.Abort()
End If
_OutputThread = Nothing
End If
If Not _ErrorThread Is Nothing Then
If _ErrorThread.IsAlive() Then
_ErrorThread.Abort()
End If
_ErrorThread = Nothing
End If
End Sub
#Region " Destructor"
Public Overloads Sub Dispose() Implements System.IDisposable.Dispose
Dispose(False)
GC.SuppressFinalize(Me)
End Sub
Protected Overridable Overloads Sub Dispose(ByVal IsFinalizer As Boolean)
If Not _blnDisposed Then
If IsFinalizer Then
End If
If Not _p Is Nothing Then
_p.Close()
_p = Nothing
End If
CloseThreads()
End If
_blnDisposed = True
End Sub
Protected Overrides Sub Finalize()
Dispose(True)
End Sub
#End Region
End Class
Hi,
I am in process of creating a script in vb.net that will backup our sharepoint sites/databases etc... I found your code on the internet and it seems to be very helpful...however i try to dim s as New Shell and Visual Studio underlines the object Shell as if the namespace is not imported or something. Do you think that is the problem? If so, what is the namespaces that i have to import? There are two commands that i will be running in the dos command console and i want to capture the output of the commands to write into a log file. So, it seems like some of the code that you have here would be quite useful and helpful for me to do this. Any help would be greatly appreciated.
Thanks,
dave
Dave on April 4, 2005 2:44 AMHello-- you need to add the second file (starting with "Public Class Shell") as a class file to your solution.
Jeff Atwood on April 4, 2005 12:02 PMBrilliant. Just what I was looking for.
Ed Manet on March 29, 2006 10:15 AMYeah, what Ed said. Exactly what I needed. You rock yet again Mr Atwood.
You appear above the fold when googling for "vb.net capture shell output" (sans quotes) too, nifty :)
Dan F on April 12, 2006 4:30 AMVery nice work. I needed a quick way to execute shell commands and capture the output, and this fitted the bill nicely.
Thanks!
James Shields on May 23, 2006 4:16 AMYou are the man. Exactly what I was looking for.
Mike Adkins on May 24, 2006 10:00 AMThat is some high-speed stuff right there. Thank you Sir.
PirateCodeMonkey on January 13, 2008 9:21 AMcan u tell me how to open a network file using impersonation
raj on August 28, 2008 5:04 AMCan u tell me how to Open the Network files using Imeprsonation in vb.net
raj on August 28, 2008 5:05 AMThe comments to this entry are closed.
|
|
Traffic Stats |