diff --git a/examples/ScratchPad.Fs/Program.fs b/examples/ScratchPad.Fs/Program.fs index e63e6d8..a434009 100644 --- a/examples/ScratchPad.Fs/Program.fs +++ b/examples/ScratchPad.Fs/Program.fs @@ -1,19 +1,48 @@ -open Proc.Fs +open System +open Proc.Fs -let uname = shell { +let _ = shell { exec "dotnet" "--version" - exec "uname" + exec "uname" } let dotnetVersion = exec { binary "dotnet" args "--help" + filter_output (fun l -> l.Line.Contains "clean") filter (fun l -> l.Line.Contains "clean") } +exec { + binary "dotnet" + args "--help" + env Map[("key", "value")] + workingDirectory "." + send_control_c false + timeout (TimeSpan.FromSeconds(10)) + thread_wrap false + validExitCode (fun i -> i <> 0) + run +} + +let helpStatus = exec { + binary "dotnet" + args "--help" + exit_code +} + +let helpOutput = exec { + binary "dotnet" + args "--help" + output +} + printfn "Found lines %i" dotnetVersion.Length -let dotnetRestoreHelp = exec { +exec { binary "dotnet" - invoke_args ["restore"; "--help"] + run_args ["restore"; "--help"] } + +exec { run "dotnet" " "} +let statusCode = exec { exit_code_of "dotnet" " "} diff --git a/src/Proc.Fs/Bindings.fs b/src/Proc.Fs/Bindings.fs index b19cfcf..367ce27 100644 --- a/src/Proc.Fs/Bindings.fs +++ b/src/Proc.Fs/Bindings.fs @@ -4,57 +4,6 @@ open System open ProcNet open ProcNet.Std -let execWithTimeout binary args timeout = - let opts = - ExecArguments(binary, args |> List.map (sprintf "\"%s\"") |> List.toArray) - let options = args |> String.concat " " - printfn ":: Running command: %s %s" binary options - let r = Proc.Exec(opts, timeout) - - match r.HasValue with - | true -> r.Value - | false -> failwithf "invocation of `%s` timed out" binary - -///executes -let exec2 (binary: string) (args: string list): int = - execWithTimeout binary args (TimeSpan.FromMinutes 10) - -let private redirected (binary: string) (args: string list) : ProcessCaptureResult = - Proc.Start(binary, args |> Array.ofList) - -type RunningStatus = { - LastExitCode: int - GrepOutput: Std.LineOut list option -} -type ShellBuilder() = - - member t.Yield _ = None - - [] - member inline this.ExecuteWithArguments(status, binary, [] args: string array) = - let exitCode = exec2 binary (args |> List.ofArray) - match status with - | None -> - Some { LastExitCode = exitCode; GrepOutput = None } - | Some s -> - Some { s with LastExitCode = exitCode } - - [] - member this.Grep(status, searchForRe, binary, [] args: string array) = - let r = Proc.Start(binary, args) - let o = - r.ConsoleOut - |> Seq.filter (_.Line.Contains(searchForRe)) - |> List.ofSeq - - match status with - | None -> - Some { LastExitCode = 0; GrepOutput = Some o } - | Some s -> - Some { LastExitCode = 0; GrepOutput = Some o } - -let shell = ShellBuilder() - type ExecOptions = { Binary: string Arguments: string list option @@ -71,27 +20,8 @@ type ExecOptions = { SendControlCFirst: bool option WaitForStreamReadersTimeout: TimeSpan option } - -type ExecBuilder() = - - let startArgs (opts: ExecOptions) = - let startArguments = StartArguments(opts.Binary, opts.Arguments |> Option.defaultValue []) - opts.LineOutFilter |> Option.iter(fun f -> startArguments.LineOutFilter <- f) - opts.Environment |> Option.iter(fun e -> startArguments.Environment <- e) - opts.WorkingDirectory |> Option.iter(fun d -> startArguments.WorkingDirectory <- d) - opts.NoWrapInThread |> Option.iter(fun b -> startArguments.NoWrapInThread <- b) - opts.SendControlCFirst |> Option.iter(fun b -> startArguments.SendControlCFirst <- b) - opts.WaitForStreamReadersTimeout |> Option.iter(fun t -> startArguments.WaitForStreamReadersTimeout <- t) - startArguments - - let execArgs (opts: ExecOptions) = - let execArguments = ExecArguments(opts.Binary, opts.Arguments |> Option.defaultValue []) - opts.Environment |> Option.iter(fun e -> execArguments.Environment <- e) - opts.WorkingDirectory |> Option.iter(fun d -> execArguments.WorkingDirectory <- d) - opts.ValidExitCodeClassifier |> Option.iter(fun f -> execArguments.ValidExitCodeClassifier <- f) - execArguments - - member t.Yield _ = +with + static member Empty = { Binary = ""; Arguments = None; Find = None; LineOutFilter = None; WorkingDirectory = None; Environment = None @@ -99,11 +29,77 @@ type ExecBuilder() = ValidExitCodeClassifier = None; NoWrapInThread = None; SendControlCFirst = None; WaitForStreamReadersTimeout = None; } + +let private startArgs (opts: ExecOptions) = + let startArguments = StartArguments(opts.Binary, opts.Arguments |> Option.defaultValue []) + opts.LineOutFilter |> Option.iter(fun f -> startArguments.LineOutFilter <- f) + opts.Environment |> Option.iter(fun e -> startArguments.Environment <- e) + opts.WorkingDirectory |> Option.iter(fun d -> startArguments.WorkingDirectory <- d) + opts.NoWrapInThread |> Option.iter(fun b -> startArguments.NoWrapInThread <- b) + opts.SendControlCFirst |> Option.iter(fun b -> startArguments.SendControlCFirst <- b) + opts.WaitForStreamReadersTimeout |> Option.iter(fun t -> startArguments.WaitForStreamReadersTimeout <- t) + startArguments + +let private execArgs (opts: ExecOptions) = + let execArguments = ExecArguments(opts.Binary, opts.Arguments |> Option.defaultValue []) + opts.Environment |> Option.iter(fun e -> execArguments.Environment <- e) + opts.WorkingDirectory |> Option.iter(fun d -> execArguments.WorkingDirectory <- d) + opts.ValidExitCodeClassifier |> Option.iter(fun f -> execArguments.ValidExitCodeClassifier <- f) + execArguments + + +type ShellBuilder() = + + member t.Yield _ = ExecOptions.Empty + + [] + member inline this.WorkingDirectory(opts, workingDirectory: string) = + { opts with WorkingDirectory = Some workingDirectory } + + [] + member inline this.EnvironmentVariables(opts, env: Map) = + { opts with Environment = Some env } + + [] + member inline this.Timeout(opts, timeout) = + { opts with Timeout = Some timeout } + + [] + member inline this.WaitForStreamReadersTimeout(opts, timeout) = + { opts with WaitForStreamReadersTimeout = Some timeout } + + [] + member inline this.SendControlCFirst(opts, sendControlCFirst) = + { opts with SendControlCFirst = Some sendControlCFirst } + + [] + member inline this.NoWrapInThread(opts, threadWrap) = + { opts with NoWrapInThread = Some (not threadWrap) } + + [] + member this.ExecuteWithArguments(opts, binary, [] args: string array) = + let opts = { opts with Binary = binary; Arguments = Some (args |> List.ofArray) } + let execArgs = execArgs opts + Proc.Exec(execArgs) |> ignore + opts + + [] + member this.ExecuteWithArguments(opts, binary, args: string list) = + let opts = { opts with Binary = binary; Arguments = Some args } + let execArgs = execArgs opts + Proc.Exec(execArgs) |> ignore + opts + +let shell = ShellBuilder() + +type ExecBuilder() = + + member t.Yield _ = ExecOptions.Empty [] - member inline this.Binary(opts, binary) = + member this.Binary(opts, binary) = { opts with Binary = binary } - + [] member inline this.Arguments(opts, [] args: string array) = { opts with Arguments = Some (args |> List.ofArray) } @@ -136,7 +132,7 @@ type ExecBuilder() = member inline this.NoWrapInThread(opts, threadWrap) = { opts with NoWrapInThread = Some (not threadWrap) } - [] + [] member inline this.FilterOutput(opts, find: LineOut -> bool) = { opts with LineOutFilter = Some find } @@ -161,22 +157,68 @@ type ExecBuilder() = |> Seq.filter find |> List.ofSeq - [] + [] + member this.Output(opts) = + let startArguments = startArgs opts + Proc.Start(startArguments) + + [] member this.InvokeArgs(opts, [] args: string array) = let opts = { opts with Arguments = Some (args |> List.ofArray) } let execArgs = execArgs opts - Proc.Exec(execArgs) + Proc.Exec(execArgs) |> ignore - [] + [] member this.InvokeArgs(opts, args: string list) = let opts = { opts with Arguments = Some args} let execArgs = execArgs opts - Proc.Exec(execArgs) + Proc.Exec(execArgs) |> ignore + + [] + member this.Execute(opts) = + let execArgs = execArgs opts + Proc.Exec(execArgs) |> ignore - [] - member this.Invoke(opts) = + [] + member this.Execute(opts, binary, args: string list) = + let opts = { opts with Binary = binary; Arguments = Some args} let execArgs = execArgs opts - Proc.Exec(execArgs) + Proc.Exec(execArgs) |> ignore + + [] + member this.Execute(opts, binary, [] args: string array) = + let opts = { opts with Binary = binary; Arguments = Some (args |> List.ofArray)} + let execArgs = execArgs opts + Proc.Exec(execArgs) |> ignore + + [] + member this.ReturnStatus(opts, binary, args: string list) = + let opts = { opts with Binary = binary; Arguments = Some args} + let execArgs = execArgs opts + Proc.Exec(execArgs).GetValueOrDefault 1 + + [] + member this.ReturnStatus(opts, binary, [] args: string array) = + let opts = { opts with Binary = binary; Arguments = Some (args |> List.ofArray)} + let execArgs = execArgs opts + Proc.Exec(execArgs).GetValueOrDefault 1 + + [] + member this.ReturnStatus(opts) = + let execArgs = execArgs opts + Proc.Exec(execArgs).GetValueOrDefault 1 + + [] + member this.ReturnOutput(opts, binary, [] args: string array) = + let opts = { opts with Binary = binary; Arguments = Some (args |> List.ofArray)} + let execArgs = startArgs opts + Proc.Start(execArgs) + + [] + member this.ReturnOutput(opts) = + let startArgs = startArgs opts + Proc.Start(startArgs) + let exec = ExecBuilder() diff --git a/src/Proc.Fs/Proc.Fs.fsproj b/src/Proc.Fs/Proc.Fs.fsproj index 636eb98..6532e1e 100644 --- a/src/Proc.Fs/Proc.Fs.fsproj +++ b/src/Proc.Fs/Proc.Fs.fsproj @@ -24,6 +24,7 @@ true true Latest + README.md @@ -31,6 +32,7 @@ + nuget-icon.png True diff --git a/src/Proc.Fs/README.md b/src/Proc.Fs/README.md new file mode 100644 index 0000000..fdf6f61 --- /dev/null +++ b/src/Proc.Fs/README.md @@ -0,0 +1,105 @@ +# Proc.Fs + +F# bindings for Proc. + +Proc is a library that turns `System.Diagnostics.Process` into a stream based `IObservable` capable of capturing a process's true output while still emitting line based events. + +The library ships with two computation expression builders: + +# Shell + +Under development but this allows you to easily chain several process invocations where execution will stop if any process yields an exit_code other than `0` + +```fsharp +let _ = shell { + exec "dotnet" "--version" + exec "uname" +} +``` + +# Exec + +A `CE` to make it **REAL** easy to execute processes. + +```fsharp +//executes dotnet --help +exec { run "dotnet" "--help" } + +//supports lists as args too +exec { run "dotnet" ["--help"] } +``` + +If you want more info about the invocation +you can use either `exit_code_of` or `output_of` +for the quick one liners. + + +```fsharp +let exitCode = exec { exit_code_of "dotnet" "--help" } +let output = exec { output_of "dotnet" "--help" } +``` +`output` will hold both the exit code and the console output + +If you need more control on how the process is started +you can supply the following options. + + +```fsharp +exec { + binary "dotnet" + args "--help" + env Map[("key", "value")] + workingDirectory "." + send_control_c false + timeout (TimeSpan.FromSeconds(10)) + thread_wrap false + filter_output (fun l -> l.Line.Contains "clean") + validExitCode (fun i -> i <> 0) + run +} +``` + +`run` will kick off the invocation of the process. + +However there are other ways to kick this off too. + +```fsharp +exec { + binary "dotnet" + run_args ["restore"; "--help"] +} +``` +Shortcut to supply arguments AND run + +```fsharp +let linesContainingClean = exec { + binary "dotnet" + args "--help" + filter (fun l -> l.Line.Contains "clean") +} +``` + +run the process returning only the console out matching the `filter` if you want to actually filter what gets written to the console use `filter_output`. + + +```fsharp + +let dotnetHelpExitCode = exec { + binary "dotnet" + args "--help" + exit_code +} +``` + +returns just the exit code + +```fsharp + +let helpOutput = exec { + binary "dotnet" + args "--help" + output +} +``` + +returns the exit code and the full console output. diff --git a/src/Proc/Proc.csproj b/src/Proc/Proc.csproj index b8817df..5d4a66a 100644 --- a/src/Proc/Proc.csproj +++ b/src/Proc/Proc.csproj @@ -25,9 +25,11 @@ $(ProcCurrentAssemblyFileVersion) true Latest + README.md + nuget-icon.png True diff --git a/src/Proc/README.md b/src/Proc/README.md new file mode 100644 index 0000000..8216660 --- /dev/null +++ b/src/Proc/README.md @@ -0,0 +1,156 @@ +# Proc + +A dependency free `System.Diagnostics.Process` supercharger. + +1. `Proc.Exec()` for the quick one-liners +2. `Proc.Start()` for the quick one-liners + - Use if you want to capture the console output as well as print these message in real time. + - Proc.Start() also allows you to script StandardIn and react to messages +3. Wraps `System.Diagnostics.Process` as an `IObservable` + * `ProcessObservable` stream based wrapper + * `EventBasedObservableProcess` event based wrapper +4. Built in support to send `SIGINT` to any process before doing a hard `SIGKILL` (`Process.Kill()`) + * Has to be set using `SendControlCFirst = true` on `StartArguments` + +## Proc.Exec + +Execute a process and blocks using a default timeout of 4 minutes. This method uses the same console session +as and as such will print the binaries console output. Throws a `ProcExecException` if the command fails to execute. +See also `ExecArguments` for more options + +```csharp +Proc.Exec("ipconfig", "/all"); +``` + +## Proc.Start + +start a process and block using the default timeout of 4 minutes +```csharp +var result = Proc.Start("ipconfig", "/all"); +``` + +Provide a custom timeout and an `IConsoleOutWriter` that can output to console +while this line is blocking. The following example writes `stderr` in red. + +```csharp +var result = Proc.Start("ipconfig", TimeSpan.FromSeconds(10), new ConsoleOutColorWriter()); +``` + +More options can be passed by passing `StartArguments` instead to control how the process should start. + +```csharp +var args = new StartArguments("ipconfig", "/all") +{ + WorkingDirectory = .. +} +Proc.Start(args, TimeSpan.FromSeconds(10)); +``` + +The static `Proc.Start` has a timeout of `4 minutes` if not specified. + +`result` has the following properties + +* `Completed` true if the program completed before the timeout +* `ConsoleOut` a list the console out message as `LineOut` + instances where `Error` on each indicating whether it was written on `stderr` or not +* `ExitCode` + +**NOTE** `ConsoleOut` will always be set regardless of whether an `IConsoleOutWriter` is provided + +## ObservableProcess + +The heart of it all this is an `IObservable`. It listens on the output buffers directly and does not wait on +newlines to emit. + +To create an observable process manually follow the following pattern: + +```csharp +using (var p = new ObservableProcess(args)) +{ + p.Subscribe(c => Console.Write(c.Characters)); + p.WaitForCompletion(TimeSpan.FromSeconds(2)); +} +``` + +The observable is `cold` untill subscribed and is not intended to be reused or subscribed to multiple times. If you need to +share a subscription look into RX's `Publish`. + +The `WaitForCompletion()` call blocks so that `p` is not disposed which would attempt to shutdown the started process. + +The default for doing a shutdown is through `Process.Kill` this is a hard `SIGKILL` on the process. + +The cool thing about `Proc` is that it supports `SIGINT` interoptions as well to allow for processes to be cleanly shutdown. + +```csharp +var args = new StartArguments("elasticsearch.bat") +{ + SendControlCFirst = true +}; +``` + +This will attempt to send a `Control+C` into the running process console on windows first before falling back to `Process.Kill`. +Linux and OSX support for this flag is still in the works so thats why this behaviour is opt in. + + +Dealing with `byte[]` characters might not be what you want to program against, so `ObservableProcess` allows the following as well. + + +```csharp +using (var p = new ObservableProcess(args)) +{ + p.SubscribeLines(c => Console.WriteLine(c.Line)); + p.WaitForCompletion(TimeSpan.FromSeconds(2)); +} +``` + +Instead of proxying `byte[]` as they are received on the socket this buffers and only emits on lines. + +In some cases it can be very useful to introduce your own word boundaries + +```csharp +public class MyProcObservable : ObservableProcess +{ + public MyProcObservable(string binary, params string[] arguments) : base(binary, arguments) { } + + public MyProcObservable(StartArguments startArguments) : base(startArguments) { } + + protected override bool BufferBoundary(char[] stdOut, char[] stdErr) + { + return base.BufferBoundary(stdOut, stdErr); + } +} +``` + +returning true inside `BufferBoundary` will yield the line to `SubscribeLine()`. This could be usefull e.g if your process +prompts without a new line: + +> Continue [Y/N]: + +A more concrete example of this is when you call a `bat` file on windows and send a `SIGINT` signal it will *always* prompt: + +> Terminate batch job (Y/N)? + +Which would not yield to `SubscribeLines` and block any waithandles unnecessary. `ObservableProcess` handles this edgecase +therefor OOTB and automatically replies with `Y` on `stdin` in this case. + +Also note that `ObservableProcess` will yield whatever is in the buffer before OnCompleted(). + + +# EventBasedObservable + +`ObservableProcess`'s sibbling that utilizes `OutputDataReceived` and `ErrorDataReceived` and can only emit lines. + + + + + + + + + + + + + + +