views:

108

answers:

1

I'm looking at trying to start a process from F#, wait till it's finished, but also read it's output progressively.

Is this the right/best way to do it? (In my case I'm trying to execute git commands, but that is tangential to the question)

let gitexecute (logger:string->unit) cmd = 
    let procStartInfo = new ProcessStartInfo(@"C:\Program Files\Git\bin\git.exe", cmd) 

    // Redirect to the Process.StandardOutput StreamReader.
    procStartInfo.RedirectStandardOutput <- true
    procStartInfo.UseShellExecute <- false;

    // Do not create the black window.
    procStartInfo.CreateNoWindow <- true;

    // Create a process, assign its ProcessStartInfo and start it
    let proc = new Process();
    proc.StartInfo <- procStartInfo;
    proc.Start() |> ignore

    // Get the output into a string
    while not proc.StandardOutput.EndOfStream do
        proc.StandardOutput.ReadLine() |> logger

What I don't understand is how the proc.Start() can return a boolean and also be asynchronous enough for me to get the output out of the while progressively.

Unfortunately, I don't currently have a large enough repository - or slow enough machine, to be able to tell what order things are happening in...

UPDATE

I tried Brian's suggestion, and it does work.

My question was a bit vague. My misunderstanding was that I assumed that Process.Start() returned the success of the process as a whole, and not just of the 'Start', and thus I couldn't see how it could work.

+2  A: 

The code you wrote in the form you wrote it is (almost) ok: process.Start start the process you specify in, well, another process, so your output stream reads will happen in parallel with your process execution. One issue though is that you should throw in a call to process.WaitForExit in the end - the fact that output stream is closed does not imply that process terminated.

However you will run into problems with synchronyous reading if you try to read both stdout and stderr of the process: there is no way of reading 2 streams synchronously and simultaneously - you will deadlock if you read stdout and process is writing to stderr and waits for you to consume its output or vice versa.

To mediate this, you can subscribe to OutputDataRecieved and ErrorDataRecieved, like this:

type ProcessResult = { exitCode : int; stdout : string; stderr : string }

let executeProcess (exe,cmdline) =
    let psi = new System.Diagnostics.ProcessStartInfo(exe,cmdline) 
    psi.UseShellExecute <- false
    psi.RedirectStandardOutput <- true
    psi.RedirectStandardError <- true
    psi.CreateNoWindow <- true        
    let p = System.Diagnostics.Process.Start(psi) 
    let output = new System.Text.StringBuilder()
    let error = new System.Text.StringBuilder()
    p.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore)
    p.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore)
    p.BeginErrorReadLine()
    p.BeginOutputReadLine()
    p.WaitForExit()
    { exitCode = p.ExitCode; stdout = output.ToString(); stderr = error.ToString() }

You can also write something along the lines of:

async {
    while true do
        let! args = Async.AwaitEvent p.OutputDataReceived
        ...
} |> Async.StartImmediate

for F#-style reactive event handling.

Mitya
+1 for answering the rest of the question that I didn't ask :) Just one error in your code is that Process.Start() doesn't return the process, it returns a boolean.
Benjol
I am calling a static Process.Start(ProcessStartInfo) that returns a Process (http://msdn.microsoft.com/en-us/library/0w4h05yb.aspx)
Mitya