Reputation: 17939
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
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
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