.netf#system.diagnosticsprocessstartinforedirectstandardoutput

How to read from Process.StandardOutput without redirecting it? (F#)


I've got this little function that saves me some headaches from dealing with the horrible System.Diagnostics.Process API:

let HiddenExec (command: string, arguments: string) =
    let startInfo = new System.Diagnostics.ProcessStartInfo(command)
    startInfo.Arguments <- arguments
    startInfo.UseShellExecute <- false

    startInfo.RedirectStandardError <- true
    startInfo.RedirectStandardOutput <- true

    use proc = System.Diagnostics.Process.Start(startInfo)
    proc.WaitForExit()
    (proc.ExitCode,proc.StandardOutput.ReadToEnd(),proc.StandardError.ReadToEnd())

This works great, because I get a tuple of three elements with the exitcode, the stdout and stderr results.

Now, suppose I don't want to "hide" the execution. That is, I want to write a hypothetical, simpler, Exec function. Then the solution is to not redirect stdout/stderr and we're done:

let Exec (command: string, arguments: string) =
    let startInfo = new System.Diagnostics.ProcessStartInfo(command)
    startInfo.Arguments <- arguments
    startInfo.UseShellExecute <- false

    let proc = System.Diagnostics.Process.Start(startInfo)
    proc.WaitForExit()
    proc.ExitCode

However, it would be nice if I could refactor this two functions to converge them into a single one, and just pass a "hidden" bool flag to it:

let NewExec (command: string, arguments: string, hidden: bool) =

This way, NewExec(_,_,false) would also return stdout,stderr (not only the exitCode, as before). The problem is that if I don't do the redirection dance (startInfo.RedirectStandardError <- true) then I cannot read from the output later via proc.StandardOutput.ReadToEnd() because I get the error StandardOut has not been redirected or the process hasn't started yet.

Another option to always redirect outputs, and if the hidden flag passed is not true, would be to call Console.WriteLine(eachOutput), but this is not very elegant because it would write the buffers in one go, without intercalating stderr between stdout lines in the screen in the proper order that they come. And for long running processes, it would hide incremental output until the process has finished.

So what's the alternative here? Do I need to resort to using the damned events from the Process class? :(

Cheers


Solution

  • @Groundoon solution is not exactly what I asked for :)

    In the end I ported this solution in C# to F#:

    let private procTimeout = TimeSpan.FromSeconds(float 10)
    
    let Execute (commandWithArguments: string, echo: bool, hidden: bool)
        : int * string * string =
    
        let outBuilder = new StringBuilder()
        let errBuilder = new StringBuilder()
    
        use outWaitHandle = new AutoResetEvent(false)
        use errWaitHandle = new AutoResetEvent(false)
    
        if (echo) then
            Console.WriteLine(commandWithArguments)
    
        let firstSpaceAt = commandWithArguments.IndexOf(" ")
        let (command, args) =
            if (firstSpaceAt >= 0) then
                (commandWithArguments.Substring(0, firstSpaceAt), commandWithArguments.Substring(firstSpaceAt + 1))
            else
                (commandWithArguments, String.Empty)
    
        let startInfo = new ProcessStartInfo(command, args)
        startInfo.UseShellExecute <- false
        startInfo.RedirectStandardOutput <- true
        startInfo.RedirectStandardError <- true
        use proc = new Process()
        proc.StartInfo <- startInfo
    
        let outReceived (e: DataReceivedEventArgs): unit =
            if (e.Data = null) then
                outWaitHandle.Set() |> ignore
            else
                if not (hidden) then
                    Console.WriteLine(e.Data)
                outBuilder.AppendLine(e.Data) |> ignore
    
        let errReceived (e: DataReceivedEventArgs): unit =
            if (e.Data = null) then
                errWaitHandle.Set() |> ignore
            else
                if not (hidden) then
                    Console.Error.WriteLine(e.Data)
                errBuilder.AppendLine(e.Data) |> ignore
    
        proc.OutputDataReceived.Add outReceived
        proc.ErrorDataReceived.Add errReceived
    
        let exitCode =
            try
                proc.Start() |> ignore
                proc.BeginOutputReadLine()
                proc.BeginErrorReadLine()
    
                if (proc.WaitForExit(int procTimeout.TotalMilliseconds)) then
                    proc.ExitCode
                else
                    failwith String.Format("Timeout expired for process '{0}'", commandWithArguments)
    
            finally
                outWaitHandle.WaitOne(procTimeout) |> ignore
                errWaitHandle.WaitOne(procTimeout) |> ignore
    
        exitCode,outBuilder.ToString(),errBuilder.ToString()