knocte
knocte

Reputation: 17939

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

Upvotes: 4

Views: 2541

Answers (2)

knocte
knocte

Reputation: 17939

@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()

Upvotes: 1

Grundoon
Grundoon

Reputation: 2764

I would follow the "parameterize all the things" principle.

In this case, it means finding the differences between HiddenExec and Exec and then parameterizing these differences with functions.

Here's what I end up when I do that:

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

    // parameterize this bit
    configureStartInfo startInfo

    use proc = System.Diagnostics.Process.Start(startInfo)
    proc.WaitForExit()

    // parameterize this bit too
    returnFromProc proc

Note that by passing in various returnFromProc functions, you can change the type of the return value, just as you want.

Now you can define HiddenExec to specify the redirect and the 3-tuple return value as you did originally:

/// Specialize ExecWith to redirect the output.
/// Return the exit code and the output and error.
/// Signature: string * string -> int * string * string
let HiddenExec =

    let configureStartInfo (startInfo: System.Diagnostics.ProcessStartInfo) =
        startInfo.RedirectStandardError <- true
        startInfo.RedirectStandardOutput <- true

    let returnFromProc (proc:System.Diagnostics.Process) =       
        (proc.ExitCode,proc.StandardOutput.ReadToEnd(),proc.StandardError.ReadToEnd())

    // partial application -- the command & arguments are passed later
    ExecWith configureStartInfo returnFromProc 

The signature shows that we have just what we want: you pass a command & arguments tuple and get the 3-tuple in return:

val HiddenExec : string * string -> int * string * string

Note that I'm using partial application here. I could have also defined HiddenExec with explicit parameters like this:

let HiddenExec (command, arguments) =  // (command, arguments) passed here

    let configureStartInfo ...
    let returnFromProc ...

    ExecWith configureStartInfo returnFromProc (command, arguments) // (command, arguments) passed here

Similarly you can define Exec to not use a redirect, like this:

/// Specialize ExecWith to not redirect the output.
/// Return the exit code.
/// Signature: string * string -> int
let Exec =

    let configureStartInfo _  =
        ()  // ignore the input

    let returnFromProc (proc:System.Diagnostics.Process) = 
        proc.ExitCode    

    ExecWith configureStartInfo returnFromProc

    // alternative version using `ignore` and lambda
    // ExecWith ignore (fun proc -> proc.ExitCode)    

Again, the signature shows that we have the simpler version that we wanted: you pass a command & arguments tuple and get just the ExitCode in return:

val Exec : string * string -> int 

Upvotes: 5

Related Questions