System.Diagnostics.Process: redirect StandardInput, StandardOutput, StandardError

Sometimes you want to launch an external utility and send input to it and also capture its output. But it's easy to run into deadlock this way...

' BAD CODE

Using p As New System.Diagnostics.Process

    p.StartInfo.FileName = "cat"

    p.StartInfo.UseShellExecute = False

    p.StartInfo.RedirectStandardOutput = True

    p.StartInfo.RedirectStandardInput = True

    p.Start()

    p.StandardInput.Write("world" & vbCrLf & "hello")

    ' deadlock here if p needs to write more than 12k to StandardOutput

    p.StandardInput.Close()

    Dim op = p.StandardOutput.ReadToEnd()

    p.WaitForExit()

    p.Close()

    Console.WriteLine("OUTPUT:") : Console.WriteLine(op)

End Using

The deadlock in this case arises because "cat" (a standard unix utility) first reads from StandardInput, then writes to StandardOutput, then reads again, and so on until there's nothing left to read. But if its StandardOutput fills up with no one to read it, then it can't write any more, and blocks.

The number "12k" is arbitrary and I wouldn't rely on it...

' BAD CODE

Using p As New System.Diagnostics.Process

    p.StartInfo.FileName = "findstr"

    p.StartInfo.UseShellExecute = False

    p.StartInfo.RedirectStandardOutput = True

    p.StartInfo.RedirectStandardError = True

    p.Start()

    ' deadlock here if p needs to write more than 12k to StandardError

    Dim op = p.StandardOutput.ReadToEnd()

    Dim err = p.StandardError.ReadToEnd()

    p.WaitForExit()

    Console.WriteLine("OUTPUT:") : Console.WriteLine(op)

    Console.WriteLine("ERROR:") : Console.WriteLine(err)

End Using

The MSDN documentation says, "You can use asynchronous read operations to avoid these dependencies and their deadlock potential. Alternately, you can avoid the deadlock condition by creating two threads and reading the output of each stream on a separate thread. " So that's what we'll do...

Using threads to redirect without deadlock

' GOOD CODE: this will not deadlock.

Using p As New Diagnostics.Process

    p.StartInfo.FileName = "sort"

    p.StartInfo.UseShellExecute = False

    p.StartInfo.RedirectStandardOutput = True

    p.StartInfo.RedirectStandardInput = True

    p.Start()

    Dim op = ""

    ' do NOT WaitForExit yet since that would introduce deadlocks.

    p.InputAndOutputToEnd("world" & vbCrLf & "hello", op, Nothing)

    p.WaitForExit()

    p.Close()

    Console.WriteLine("OUTPUT:") : Console.WriteLine(op)

End Using

 

 

''' <summary>

''' InputAndOutputToEnd: a handy way to use redirected input/output/error on a p.

''' </summary>

''' <param name="p">The p to redirect. Must have UseShellExecute set to false.</param>

''' <param name="StandardInput">This string will be sent as input to the p. (must be Nothing if not StartInfo.RedirectStandardInput)</param>

''' <param name="StandardOutput">The p's output will be collected in this ByRef string. (must be Nothing if not StartInfo.RedirectStandardOutput)</param>

''' <param name="StandardError">The p's error will be collected in this ByRef string. (must be Nothing if not StartInfo.RedirectStandardError)</param>

''' <remarks>This function solves the deadlock problem mentioned at https://msdn.microsoft.com/en-us/library/system.diagnostics.p.standardoutput.aspx\</remarks>

<Runtime.CompilerServices.Extension()> Sub InputAndOutputToEnd(ByVal p As Diagnostics.Process, ByVal StandardInput As String, ByRef StandardOutput As String, ByRef StandardError As String)

    If p Is Nothing Then Throw New ArgumentException("p must be non-null")

    ' Assume p has started. Alas there's no way to check.

    If p.StartInfo.UseShellExecute Then Throw New ArgumentException("Set StartInfo.UseShellExecute to false")

    If (p.StartInfo.RedirectStandardInput <> (StandardInput IsNot Nothing)) Then Throw New ArgumentException("Provide a non-null Input only when StartInfo.RedirectStandardInput")

    If (p.StartInfo.RedirectStandardOutput <> (StandardOutput IsNot Nothing)) Then Throw New ArgumentException("Provide a non-null Output only when StartInfo.RedirectStandardOutput")

    If (p.StartInfo.RedirectStandardError <> (StandardError IsNot Nothing)) Then Throw New ArgumentException("Provide a non-null Error only when StartInfo.RedirectStandardError")

    '

    Dim outputData As New InputAndOutputToEndData

    Dim errorData As New InputAndOutputToEndData

    '

    If p.StartInfo.RedirectStandardOutput Then

        outputData.Stream = p.StandardOutput

        outputData.Thread = New Threading.Thread(AddressOf InputAndOutputToEndProc)

        outputData.Thread.Start(outputData)

    End If

    If p.StartInfo.RedirectStandardError Then

        errorData.Stream = p.StandardError

        errorData.Thread = New Threading.Thread(AddressOf InputAndOutputToEndProc)

        errorData.Thread.Start(errorData)

    End If

    '

    If p.StartInfo.RedirectStandardInput Then

        p.StandardInput.Write(StandardInput)

        p.StandardInput.Close()

    End If

    '

    If p.StartInfo.RedirectStandardOutput Then outputData.Thread.Join() : StandardOutput = outputData.Output

    If p.StartInfo.RedirectStandardError Then errorData.Thread.Join() : StandardError = errorData.Output

    If outputData.Exception IsNot Nothing Then Throw outputData.Exception

    If errorData.Exception IsNot Nothing Then Throw errorData.Exception

End Sub

Private Class InputAndOutputToEndData

    Public Thread As Threading.Thread

    Public Stream As IO.StreamReader

    Public Output As String

    Public Exception As Exception

End Class

Private Sub InputAndOutputToEndProc(ByVal data_ As Object)

    Dim data = DirectCast(data_, InputAndOutputToEndData)

    Try : data.Output = data.Stream.ReadToEnd : Catch e As Exception : data.Exception = e : End Try

End Sub