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