├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Directory.Build.props ├── LICENSE ├── build.bat ├── build.sh ├── build ├── keys │ ├── keypair.snk │ └── public.snk └── scripts │ ├── CommandLine.fs │ ├── Paths.fs │ ├── Program.fs │ ├── Targets.fs │ └── scripts.fsproj ├── dotnet-tools.json ├── examples ├── ScratchPad.Fs.ArgumentPrinter │ ├── Program.fs │ └── ScratchPad.Fs.ArgumentPrinter.fsproj ├── ScratchPad.Fs │ ├── Program.fs │ └── ScratchPad.Fs.fsproj └── ScratchPad │ ├── Program.cs │ ├── ScratchPad.csproj │ └── TestBinary.cs ├── global.json ├── nuget-icon.png ├── proc.sln ├── readme.md ├── src ├── Proc.ControlC │ ├── ControlCDispatcher.cs │ ├── Proc.ControlC.csproj │ └── Program.cs ├── Proc.Fs │ ├── Bindings.fs │ ├── Proc.Fs.fsproj │ └── README.md └── Proc │ ├── BufferedObservableProcess.cs │ ├── CleanExitExceptionBase.cs │ ├── Embedded │ └── Proc.ControlC.exe │ ├── EventBasedObservableProcess.cs │ ├── ExecArguments.cs │ ├── Extensions │ ├── ArgumentExtensions.cs │ ├── CancellableStreamReader.cs │ └── ObserveOutputExtensions.cs │ ├── IObservableProcess.cs │ ├── LongRunningArguments.cs │ ├── ObservableProcess.cs │ ├── ObservableProcessBase.cs │ ├── Proc.Exec.cs │ ├── Proc.ExecAsync.cs │ ├── Proc.Start.cs │ ├── Proc.StartLongRunning.cs │ ├── Proc.StartRedirected.cs │ ├── Proc.csproj │ ├── ProcessArgumentsBase.cs │ ├── ProcessCaptureResult.cs │ ├── ProcessResult.cs │ ├── README.md │ ├── StartArguments.cs │ └── Std │ ├── CharactersOut.cs │ ├── ConsoleOut.cs │ ├── ConsoleOutColorWriter.cs │ ├── ConsoleOutWriter.cs │ ├── IConsoleLineHandler.cs │ ├── IConsoleOutWriter.cs │ └── LineOut.cs └── tests ├── Directory.Build.props ├── Proc.Tests.Binary ├── Proc.Tests.Binary.csproj └── Program.cs └── Proc.Tests ├── AsyncReadsStartStopTests.cs ├── ControlCTestCases.cs ├── DisposeTestCases.cs ├── EventBasedObservableProcessTestCases.cs ├── LineOutputTests.cs ├── LongRunningTests.cs ├── ObservableProcessTestCases.cs ├── PrintArgsTests.cs ├── Proc.Tests.csproj ├── ProcTestCases.cs ├── ReadInOrderTests.cs ├── ReadLineTestCases.cs ├── SkipOnNonWindowsFact.cs ├── TestConsoleOutWriter.cs ├── TestsBase.cs └── TrailingNewLinesTestCases.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*.cs] 4 | trim_trailing_whitespace=true 5 | insert_final_newline=true 6 | 7 | [*] 8 | indent_style = tab 9 | indent_size = 4 10 | 11 | [*.cshtml] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.{fs,fsx,yml}] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | [*.{md,markdown,json,js,csproj,fsproj,targets,targets,props}] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | # Dotnet code style settings: 24 | [*.{cs,vb}] 25 | 26 | # --- 27 | # naming conventions https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions 28 | # currently not supported in Rider/Resharper so not using these for now 29 | # --- 30 | 31 | # --- 32 | # language conventions https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#language-conventions 33 | 34 | # Sort using and Import directives with System.* appearing first 35 | dotnet_sort_system_directives_first = true 36 | 37 | dotnet_style_qualification_for_field = false:error 38 | dotnet_style_qualification_for_property = false:error 39 | dotnet_style_qualification_for_method = false:error 40 | dotnet_style_qualification_for_event = false:error 41 | 42 | # Use language keywords instead of framework type names for type references 43 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 44 | dotnet_style_predefined_type_for_member_access = true:error 45 | 46 | # Suggest more modern language features when available 47 | dotnet_style_object_initializer = true:error 48 | dotnet_style_collection_initializer = true:error 49 | dotnet_style_explicit_tuple_names = true:error 50 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:error 51 | dotnet_style_prefer_inferred_tuple_names = true:error 52 | dotnet_style_coalesce_expression = true:error 53 | dotnet_style_null_propagation = true:error 54 | 55 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:error 56 | dotnet_style_readonly_field = true:error 57 | 58 | # CSharp code style settings: 59 | [*.cs] 60 | # Prefer "var" everywhere 61 | csharp_style_var_for_built_in_types = true:error 62 | csharp_style_var_when_type_is_apparent = true:error 63 | csharp_style_var_elsewhere = true:error 64 | 65 | csharp_style_expression_bodied_methods = true:error 66 | csharp_style_expression_bodied_constructors = true:error 67 | csharp_style_expression_bodied_operators = true:error 68 | csharp_style_expression_bodied_properties = true:error 69 | csharp_style_expression_bodied_indexers = true:error 70 | csharp_style_expression_bodied_accessors = true:error 71 | 72 | # Suggest more modern language features when available 73 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 74 | csharp_style_pattern_matching_over_as_with_null_check = true:error 75 | csharp_style_inlined_variable_declaration = true:error 76 | csharp_style_deconstructed_variable_declaration = true:error 77 | csharp_style_pattern_local_over_anonymous_function = true:error 78 | csharp_style_throw_expression = true:error 79 | csharp_style_conditional_delegate_call = true:error 80 | 81 | csharp_prefer_braces = false:warning 82 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error 83 | 84 | # --- 85 | # formatting conventions https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions 86 | 87 | # Newline settings (Allman yo!) 88 | csharp_new_line_before_open_brace = all:error 89 | csharp_new_line_before_else = true:error 90 | csharp_new_line_before_catch = true:error 91 | csharp_new_line_before_finally = true:error 92 | csharp_new_line_before_members_in_object_initializers = true 93 | # just a suggestion do to our JSON tests that use anonymous types to 94 | # represent json quite a bit (makes copy paste easier). 95 | csharp_new_line_before_members_in_anonymous_types = true:suggestion 96 | csharp_new_line_between_query_expression_clauses = true:error 97 | 98 | # Indent 99 | csharp_indent_case_contents = true:error 100 | csharp_indent_switch_labels = true:error 101 | csharp_space_after_cast = false:error 102 | csharp_space_after_keywords_in_control_flow_statements = true:error 103 | csharp_space_between_method_declaration_parameter_list_parentheses = false:error 104 | csharp_space_between_method_call_parameter_list_parentheses = false:error 105 | 106 | #Wrap 107 | csharp_preserve_single_line_statements = false:error 108 | csharp_preserve_single_line_blocks = true:error 109 | 110 | # Resharper 111 | resharper_csharp_braces_for_lock=required_for_complex 112 | resharper_csharp_braces_for_using=required_for_complex 113 | resharper_csharp_braces_for_while=required_for_complex 114 | resharper_csharp_braces_for_foreach=required_for_complex 115 | resharper_csharp_braces_for_for=required_for_complex 116 | resharper_csharp_braces_for_fixed=required_for_complex 117 | resharper_csharp_braces_for_ifelse=required_for_complex 118 | 119 | resharper_csharp_accessor_owner_body=expression_body 120 | 121 | 122 | resharper_redundant_case_label_highlighting=do_not_show 123 | 124 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Always be deploying 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - 'README.md' 7 | - '.editorconfig' 8 | push: 9 | paths-ignore: 10 | - 'README.md' 11 | - '.editorconfig' 12 | branches: 13 | - master 14 | tags: 15 | - "*.*.*" 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 1 24 | - run: | 25 | git fetch --prune --unshallow --tags 26 | echo exit code $? 27 | git tag --list 28 | - uses: actions/setup-dotnet@v1 29 | with: 30 | dotnet-version: | 31 | 8.0.x 32 | 6.0.x 33 | source-url: https://nuget.pkg.github.com/nullean/index.json 34 | env: 35 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 36 | 37 | - run: ./build.sh build -s true 38 | name: Build 39 | - run: ./build.sh test -s true 40 | name: Test 41 | - run: ./build.sh generatepackages -s true 42 | name: Generate local nuget packages 43 | - run: ./build.sh validatepackages -s true 44 | name: "validate *.npkg files that were created" 45 | - run: ./build.sh generateapichanges -s true 46 | name: "Inspect public API changes" 47 | 48 | - name: publish to github package repository 49 | if: github.event_name == 'push' && startswith(github.ref, 'refs/heads') 50 | shell: bash 51 | run: | 52 | until dotnet nuget push 'build/output/*.nupkg' -k ${{secrets.GITHUB_TOKEN}} --skip-duplicate --no-symbols; do echo "Retrying"; sleep 1; done; 53 | 54 | - run: ./build.sh generatereleasenotes -s true 55 | name: Generate release notes for tag 56 | if: github.event_name == 'push' && startswith(github.ref, 'refs/tags') 57 | - run: ./build.sh createreleaseongithub -s true --token ${{secrets.GITHUB_TOKEN}} 58 | if: github.event_name == 'push' && startswith(github.ref, 'refs/tags') 59 | name: Create or update release for tag on github 60 | 61 | - run: dotnet nuget push 'build/output/*.nupkg' -k ${{secrets.NUGET_ORG_API_KEY}} -s https://api.nuget.org/v3/index.json --skip-duplicate --no-symbols 62 | name: release to nuget.org 63 | if: github.event_name == 'push' && startswith(github.ref, 'refs/tags') 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.userprefs 2 | *.local.xml 3 | *.sln.docstates 4 | *.obj 5 | *.swp 6 | *.exe 7 | *.pdb 8 | *.user 9 | *.aps 10 | *.pch 11 | *.tss 12 | *.vspscc 13 | *_i.c 14 | *_p.c 15 | *.ncb 16 | *.suo 17 | *.tlb 18 | *.tlh 19 | *.bak 20 | *.cache 21 | *.ilk 22 | *.log 23 | *.nupkg 24 | *.ncrunchsolution 25 | [Bb]in 26 | [Dd]ebug/ 27 | test-results 28 | test-results/* 29 | *.lib 30 | *.sbr 31 | *.DotSettings.user 32 | obj/ 33 | _ReSharper*/ 34 | _NCrunch*/ 35 | [Tt]est[Rr]esult* 36 | 37 | .fake/* 38 | .fake 39 | packages/* 40 | paket.exe 41 | paket-files/*.cached 42 | 43 | BenchmarkDotNet.Artifacts 44 | build/* 45 | !build/tools 46 | !build/keys 47 | build/tools/* 48 | !build/tools/sn 49 | !build/tools/sn/* 50 | !build/tools/ilmerge 51 | !build/*.fsx 52 | !build/*.fsx 53 | !build/*.ps1 54 | !build/*.nuspec 55 | !build/*.png 56 | !build/*.targets 57 | !build/scripts 58 | 59 | 60 | /dep/Newtonsoft.Json.4.0.2 61 | !docs/build 62 | docs/node_modules 63 | doc/Help 64 | 65 | /src/Nest.Tests.Unit/*.ncrunchproject 66 | *.ncrunchproject 67 | Cache 68 | YamlCache 69 | tests.yaml 70 | 71 | *.DS_Store 72 | *.sln.ide 73 | 74 | launchSettings.json 75 | # https://github.com/elastic/elasticsearch-net/pull/1822#issuecomment-183722698 76 | *.project.lock.json 77 | project.lock.json 78 | .vs 79 | .vs/* 80 | 81 | .idea/ 82 | *.sln.iml 83 | /src/.vs/restore.dg 84 | # temporary location for doc generation 85 | docs-temp 86 | *.binlog 87 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | canary.0 5 | 0.2 6 | 7 | 8 | 9 | all 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nullean - A collection of .NET tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | dotnet run --project build/scripts -- %* -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | dotnet run --project build/scripts -- "$@" 4 | -------------------------------------------------------------------------------- /build/keys/keypair.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullean/proc/767c37244459892f88a6f2514292c249c2518272/build/keys/keypair.snk -------------------------------------------------------------------------------- /build/keys/public.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullean/proc/767c37244459892f88a6f2514292c249c2518272/build/keys/public.snk -------------------------------------------------------------------------------- /build/scripts/CommandLine.fs: -------------------------------------------------------------------------------- 1 | module CommandLine 2 | 3 | open Argu 4 | open Microsoft.FSharp.Reflection 5 | 6 | type Arguments = 7 | | [] Clean 8 | | [] Build 9 | | [] Test 10 | 11 | | [] PristineCheck 12 | | [] GeneratePackages 13 | | [] ValidatePackages 14 | | [] GenerateReleaseNotes 15 | | [] GenerateApiChanges 16 | | [] Release 17 | 18 | | [] CreateReleaseOnGithub 19 | | [] Publish 20 | 21 | | [] SingleTarget of bool 22 | | [] Token of string 23 | | [] CleanCheckout of bool 24 | with 25 | interface IArgParserTemplate with 26 | member this.Usage = 27 | match this with 28 | | Clean -> "clean known output locations" 29 | | Build -> "Run build" 30 | | Test -> "Runs build then tests" 31 | | Release -> "runs build, tests, and create and validates the packages shy of publishing them" 32 | | Publish -> "Runs the full release" 33 | 34 | | SingleTarget _ -> "Runs the provided sub command without running their dependencies" 35 | | Token _ -> "Token to be used to authenticate with github" 36 | | CleanCheckout _ -> "Skip the clean checkout check that guards the release/publish targets" 37 | 38 | | PristineCheck 39 | | GeneratePackages 40 | | ValidatePackages 41 | | GenerateReleaseNotes 42 | | GenerateApiChanges 43 | | CreateReleaseOnGithub 44 | -> "Undocumented, dependent target" 45 | member this.Name = 46 | match FSharpValue.GetUnionFields(this, typeof) with 47 | | case, _ -> case.Name.ToLowerInvariant() 48 | -------------------------------------------------------------------------------- /build/scripts/Paths.fs: -------------------------------------------------------------------------------- 1 | module Paths 2 | 3 | open System 4 | open System.IO 5 | 6 | let ToolName = "proc" 7 | let Repository = sprintf "nullean/%s" ToolName 8 | let MainTFM = "netstandard2.0" 9 | let SignKey = "96c599bbe3e70f5d" 10 | 11 | let ValidateAssemblyName = true 12 | let IncludeGitHashInInformational = true 13 | let GenerateApiChanges = true 14 | 15 | let Root = 16 | let mutable dir = DirectoryInfo(".") 17 | while dir.GetFiles("*.sln").Length = 0 do dir <- dir.Parent 18 | Environment.CurrentDirectory <- dir.FullName 19 | dir 20 | 21 | let RootRelative path = Path.GetRelativePath(Root.FullName, path) 22 | 23 | let Output = DirectoryInfo(Path.Combine(Root.FullName, "build", "output")) 24 | 25 | let ToolProject = DirectoryInfo(Path.Combine(Root.FullName, "src", ToolName)) 26 | -------------------------------------------------------------------------------- /build/scripts/Program.fs: -------------------------------------------------------------------------------- 1 | module Program 2 | 3 | open Argu 4 | open Bullseye 5 | open ProcNet 6 | open CommandLine 7 | 8 | [] 9 | let main argv = 10 | let parser = ArgumentParser.Create(programName = "./build.sh") 11 | let parsed = 12 | try 13 | let parsed = parser.ParseCommandLine(inputs = argv, raiseOnUsage = true) 14 | let arguments = parsed.GetSubCommand() 15 | Some (parsed, arguments) 16 | with e -> 17 | printfn "%s" e.Message 18 | None 19 | 20 | match parsed with 21 | | None -> 2 22 | | Some (parsed, arguments) -> 23 | 24 | let target = arguments.Name 25 | 26 | Targets.Setup parsed arguments 27 | let swallowTypes = [typeof; typeof] 28 | 29 | Targets.RunTargetsAndExit 30 | ([target], (fun e -> swallowTypes |> List.contains (e.GetType()) ), ":") 31 | 0 32 | 33 | -------------------------------------------------------------------------------- /build/scripts/Targets.fs: -------------------------------------------------------------------------------- 1 | module Targets 2 | 3 | open Argu 4 | open System 5 | open System.IO 6 | open Bullseye 7 | open CommandLine 8 | open Fake.Tools.Git 9 | open ProcNet 10 | 11 | 12 | let exec binary args = 13 | let r = Proc.Exec (binary, args |> List.map (fun a -> sprintf "\"%s\"" a) |> List.toArray) 14 | match r.HasValue with | true -> r.Value | false -> failwithf "invocation of `%s` timed out" binary 15 | 16 | let private restoreTools = lazy(exec "dotnet" ["tool"; "restore"]) 17 | let private currentVersion = 18 | lazy( 19 | restoreTools.Value |> ignore 20 | let r = Proc.Start("dotnet", "minver", "-p", "canary.0", "-m", "0.1") 21 | let o = r.ConsoleOut |> Seq.find (fun l -> not(l.Line.StartsWith("MinVer:"))) 22 | o.Line 23 | ) 24 | let private currentVersionInformational = 25 | lazy( 26 | match Paths.IncludeGitHashInInformational with 27 | | false -> currentVersion.Value 28 | | true -> sprintf "%s+%s" currentVersion.Value (Information.getCurrentSHA1( ".")) 29 | ) 30 | 31 | let private clean (arguments:ParseResults) = 32 | if (Paths.Output.Exists) then Paths.Output.Delete (true) 33 | exec "dotnet" ["clean"] |> ignore 34 | 35 | let private build (arguments:ParseResults) = exec "dotnet" ["build"; "-c"; "Release"] |> ignore 36 | 37 | let private pristineCheck (arguments:ParseResults) = 38 | let doCheck = arguments.TryGetResult CleanCheckout |> Option.defaultValue true 39 | match doCheck, Information.isCleanWorkingCopy "." with 40 | | _, true -> printfn "The checkout folder does not have pending changes, proceeding" 41 | | false, _ -> printf "Checkout is dirty but -c was specified to ignore this" 42 | | _ -> failwithf "The checkout folder has pending changes, aborting" 43 | 44 | let private test (arguments:ParseResults) = 45 | let junitOutput = Path.Combine(Paths.Output.FullName, "junit-{assembly}-{framework}-test-results.xml") 46 | let loggerPathArgs = sprintf "LogFilePath=%s" junitOutput 47 | let loggerArg = sprintf "--logger:\"junit;%s\"" loggerPathArgs 48 | exec "dotnet" ["test"; "-c"; "RELEASE"; loggerArg; "--logger:pretty"] |> ignore 49 | 50 | let private generatePackages (arguments:ParseResults) = 51 | let output = Paths.RootRelative Paths.Output.FullName 52 | exec "dotnet" ["pack"; "-c"; "Release"; "-o"; output] |> ignore 53 | 54 | let private validatePackages (arguments:ParseResults) = 55 | let output = Paths.RootRelative <| Paths.Output.FullName 56 | let nugetPackages = 57 | Paths.Output.GetFiles("*.nupkg") |> Seq.sortByDescending(fun f -> f.CreationTimeUtc) 58 | |> Seq.map (fun p -> Paths.RootRelative p.FullName) 59 | 60 | let jenkinsOnWindowsArgs = 61 | if Fake.Core.Environment.hasEnvironVar "JENKINS_URL" && Fake.Core.Environment.isWindows then ["-r"; "true"] else [] 62 | 63 | let args = ["-v"; currentVersionInformational.Value; "-k"; Paths.SignKey; "-t"; output] @ jenkinsOnWindowsArgs 64 | nugetPackages |> Seq.iter (fun p -> exec "dotnet" (["nupkg-validator"; p] @ args) |> ignore) 65 | 66 | 67 | let private generateApiChanges (arguments:ParseResults) = 68 | let output = Paths.RootRelative <| Paths.Output.FullName 69 | let currentVersion = currentVersion.Value 70 | let nugetPackages = 71 | Paths.Output.GetFiles("*.nupkg") |> Seq.sortByDescending(fun f -> f.CreationTimeUtc) 72 | |> Seq.map (fun p -> Path.GetFileNameWithoutExtension(Paths.RootRelative p.FullName).Replace("." + currentVersion, "")) 73 | nugetPackages 74 | |> Seq.iter(fun p -> 75 | let outputFile = 76 | let f = sprintf "breaking-changes-%s.md" p 77 | Path.Combine(output, f) 78 | let args = 79 | [ 80 | "assembly-differ" 81 | (sprintf "previous-nuget|%s|%s|%s" p currentVersion Paths.MainTFM); 82 | (sprintf "directory|src/%s/bin/Release/%s" p Paths.MainTFM); 83 | "-a"; "true"; "--target"; p; "-f"; "github-comment"; "--output"; outputFile 84 | ] 85 | 86 | exec "dotnet" args |> ignore 87 | ) 88 | 89 | let private generateReleaseNotes (arguments:ParseResults) = 90 | let currentVersion = currentVersion.Value 91 | let output = 92 | Paths.RootRelative <| Path.Combine(Paths.Output.FullName, sprintf "release-notes-%s.md" currentVersion) 93 | let tokenArgs = 94 | match arguments.TryGetResult Token with 95 | | None -> [] 96 | | Some token -> ["--token"; token;] 97 | let releaseNotesArgs = 98 | (Paths.Repository.Split("/") |> Seq.toList) 99 | @ ["--version"; currentVersion 100 | "--label"; "enhancement"; "New Features" 101 | "--label"; "bug"; "Bug Fixes" 102 | "--label"; "documentation"; "Docs Improvements" 103 | ] @ tokenArgs 104 | @ ["--output"; output] 105 | 106 | exec "dotnet" (["release-notes"] @ releaseNotesArgs) |> ignore 107 | 108 | let private createReleaseOnGithub (arguments:ParseResults) = 109 | let currentVersion = currentVersion.Value 110 | let tokenArgs = 111 | match arguments.TryGetResult Token with 112 | | None -> [] 113 | | Some token -> ["--token"; token;] 114 | let releaseNotes = Paths.RootRelative <| Path.Combine(Paths.Output.FullName, sprintf "release-notes-%s.md" currentVersion) 115 | let breakingChanges = 116 | let breakingChangesDocs = Paths.Output.GetFiles("breaking-changes-*.md") 117 | breakingChangesDocs 118 | |> Seq.map(fun f -> ["--body"; Paths.RootRelative f.FullName]) 119 | |> Seq.collect id 120 | |> Seq.toList 121 | let releaseArgs = 122 | (Paths.Repository.Split("/") |> Seq.toList) 123 | @ ["create-release" 124 | "--version"; currentVersion 125 | "--body"; releaseNotes; 126 | ] @ breakingChanges @ tokenArgs 127 | 128 | exec "dotnet" (["release-notes"] @ releaseArgs) |> ignore 129 | 130 | let private release (arguments:ParseResults) = printfn "release" 131 | 132 | let private publish (arguments:ParseResults) = printfn "publish" 133 | 134 | let Setup (parsed:ParseResults) (subCommand:Arguments) = 135 | let step (name:string) action = Targets.Target(name, new Action(fun _ -> action(parsed))) 136 | 137 | let cmd (name:string) commandsBefore steps action = 138 | let singleTarget = (parsed.TryGetResult SingleTarget |> Option.defaultValue false) 139 | let deps = 140 | match (singleTarget, commandsBefore) with 141 | | (true, _) -> [] 142 | | (_, Some d) -> d 143 | | _ -> [] 144 | let steps = steps |> Option.defaultValue [] 145 | Targets.Target(name, deps @ steps, Action(action)) 146 | 147 | step Clean.Name clean 148 | cmd Build.Name None (Some [Clean.Name]) <| fun _ -> build parsed 149 | 150 | cmd Test.Name (Some [Build.Name;]) None <| fun _ -> test parsed 151 | 152 | step PristineCheck.Name pristineCheck 153 | step GeneratePackages.Name generatePackages 154 | step ValidatePackages.Name validatePackages 155 | step GenerateReleaseNotes.Name generateReleaseNotes 156 | step GenerateApiChanges.Name generateApiChanges 157 | cmd Release.Name 158 | (Some [PristineCheck.Name; Test.Name;]) 159 | (Some [GeneratePackages.Name; ValidatePackages.Name; GenerateReleaseNotes.Name; GenerateApiChanges.Name]) 160 | <| fun _ -> release parsed 161 | 162 | step CreateReleaseOnGithub.Name createReleaseOnGithub 163 | cmd Publish.Name 164 | (Some [Release.Name]) 165 | (Some [CreateReleaseOnGithub.Name; ]) 166 | <| fun _ -> publish parsed 167 | -------------------------------------------------------------------------------- /build/scripts/scripts.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "minver-cli": { 6 | "version": "4.3.0", 7 | "commands": [ 8 | "minver" 9 | ] 10 | }, 11 | "release-notes": { 12 | "version": "0.6.0", 13 | "commands": [ 14 | "release-notes" 15 | ] 16 | }, 17 | "nupkg-validator": { 18 | "version": "0.6.0", 19 | "commands": [ 20 | "nupkg-validator" 21 | ] 22 | }, 23 | "assembly-differ": { 24 | "version": "0.15.0", 25 | "commands": [ 26 | "assembly-differ" 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /examples/ScratchPad.Fs.ArgumentPrinter/Program.fs: -------------------------------------------------------------------------------- 1 | // For more information see https://aka.ms/fsharp-console-apps 2 | 3 | open System 4 | 5 | let args = Environment.GetCommandLineArgs() 6 | printfn "%i" args.Length 7 | for a in args do 8 | printfn "%s" a 9 | -------------------------------------------------------------------------------- /examples/ScratchPad.Fs.ArgumentPrinter/ScratchPad.Fs.ArgumentPrinter.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/ScratchPad.Fs/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open Proc.Fs 3 | 4 | (* 5 | let _ = shell { 6 | exec "dotnet" "--version" 7 | exec "uname" 8 | } 9 | 10 | exec { run "dotnet" "--help"} 11 | 12 | exec { 13 | binary "dotnet" 14 | arguments "--help" 15 | env Map[("key", "value")] 16 | working_dir "." 17 | send_control_c false 18 | timeout (TimeSpan.FromSeconds(10)) 19 | thread_wrap false 20 | validExitCode (fun i -> i = 0) 21 | run 22 | } 23 | 24 | let helpStatus = exec { 25 | binary "dotnet" 26 | arguments "--help" 27 | exit_code 28 | } 29 | 30 | let helpOutput = exec { 31 | binary "dotnet" 32 | arguments "--help" 33 | output 34 | } 35 | let dotnetVersion = exec { 36 | binary "dotnet" 37 | arguments "--help" 38 | filter_output (fun l -> l.Line.Contains "clean") 39 | filter (fun l -> l.Line.Contains "clean") 40 | } 41 | 42 | printfn $"Found lines %i{dotnetVersion.Length}" 43 | 44 | 45 | let dotnetOptions = exec { binary "dotnet" } 46 | exec { 47 | options dotnetOptions 48 | run_args ["restore"; "--help"] 49 | } 50 | 51 | let args: string list = ["--help"] 52 | exec { run "dotnet" "--help"} 53 | exec { run "dotnet" args } 54 | 55 | let _ = shell { exec "dotnet" args } 56 | let statusCode = exec { exit_code_of "dotnet" "--help"} 57 | 58 | exec { run "dotnet" "run" "--project" "examples/ScratchPad.Fs.ArgumentPrinter" "--" "With Space" } 59 | *) 60 | 61 | let runningProcess = exec { 62 | binary "dotnet" 63 | arguments "run" "--project" "tests/Proc.Tests.Binary" "--" "TrulyLongRunning" 64 | //wait_until (fun l -> l.Line = "Started!") 65 | wait_until_and_disconnect (fun l -> l.Line = "Started!") 66 | } 67 | 68 | 69 | printfn "That's all folks!" 70 | -------------------------------------------------------------------------------- /examples/ScratchPad.Fs/ScratchPad.Fs.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/ScratchPad/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ProcNet; 3 | 4 | namespace ScratchPad 5 | { 6 | public static class Program 7 | { 8 | public static int Main() 9 | { 10 | var tessCase = TestBinary.TestCaseArguments("ControlC"); 11 | var process = new ObservableProcess(tessCase); 12 | process.SubscribeLines(c => 13 | { 14 | //if (c.Line.EndsWith("4")) 15 | { 16 | process.SendControlC(); 17 | } 18 | Console.WriteLine(c.Line); 19 | }, e=> Console.Error.WriteLine(e)); 20 | 21 | process.WaitForCompletion(TimeSpan.FromSeconds(20)); 22 | return 0; 23 | } 24 | 25 | } 26 | 27 | public class MyProcObservable : ObservableProcess 28 | { 29 | public MyProcObservable(string binary, params string[] arguments) : base(binary, arguments) { } 30 | 31 | public MyProcObservable(StartArguments startArguments) : base(startArguments) { } 32 | 33 | protected override bool BufferBoundary(char[] stdOut, char[] stdErr) => base.BufferBoundary(stdOut, stdErr); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/ScratchPad/ScratchPad.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net8.0 5 | false 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/ScratchPad/TestBinary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using ProcNet; 5 | 6 | namespace ScratchPad 7 | { 8 | public static class TestBinary 9 | { 10 | private static string _procTestBinary = "Proc.Tests.Binary"; 11 | 12 | //increase this if you are using the debugger 13 | private static TimeSpan WaitTimeout { get; } = TimeSpan.FromSeconds(5); 14 | 15 | private static string GetWorkingDir() 16 | { 17 | var directoryInfo = new DirectoryInfo(Directory.GetCurrentDirectory()); 18 | 19 | var root = (directoryInfo.Name == "ScratchPad" 20 | && directoryInfo.Parent != null 21 | && directoryInfo.Parent.Name == "src") 22 | ? "./.." 23 | : @"../../../.."; 24 | 25 | var binaryFolder = Path.Combine(Path.GetFullPath(root), _procTestBinary); 26 | return binaryFolder; 27 | } 28 | 29 | public static StartArguments TestCaseArguments(string testcase) => 30 | new StartArguments("dotnet", GetDll(), testcase) 31 | { 32 | WorkingDirectory = GetWorkingDir() 33 | }; 34 | 35 | private static string GetDll() 36 | { 37 | var dll = Path.Combine("bin", GetRunningConfiguration(), "net8.0", _procTestBinary + ".dll"); 38 | var fullPath = Path.Combine(GetWorkingDir(), dll); 39 | if (!File.Exists(fullPath)) throw new Exception($"Can not find {fullPath}"); 40 | 41 | return dll; 42 | } 43 | 44 | private static string GetRunningConfiguration() 45 | { 46 | var l = typeof(TestBinary).GetTypeInfo().Assembly.Location; 47 | return new DirectoryInfo(l).Parent?.Parent?.Name; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /nuget-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullean/proc/767c37244459892f88a6f2514292c249c2518272/nuget-icon.png -------------------------------------------------------------------------------- /proc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.0.0 5 | MinimumVisualStudioVersion = 10.0.0.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proc.Tests.Binary", "tests\Proc.Tests.Binary\Proc.Tests.Binary.csproj", "{D3AFFBE0-8F40-42C7-8A6A-BCCD2EDF4A3E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proc.Tests", "tests\Proc.Tests\Proc.Tests.csproj", "{BE04AF1C-20CB-497B-9725-518C0C8109ED}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proc", "src\Proc\Proc.csproj", "{4BE05F38-DAA3-45E3-A2F5-B7E6408941AD}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScratchPad", "examples\ScratchPad\ScratchPad.csproj", "{E7AD2461-309A-4BA7-A6EB-5C1F4F882BC2}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "scripts", "build\scripts\scripts.fsproj", "{D6997ADC-E933-418E-831C-DE1A78897493}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Proc.ControlC", "src\Proc.ControlC\Proc.ControlC.csproj", "{733EB608-B8B4-47FE-AB63-A4C7856C4209}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9C336E9A-3FC8-4F77-A5B4-1D07E4E54924}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0F267D58-B5AA-4D04-B346-12B283E37B68}" 21 | ProjectSection(SolutionItems) = preProject 22 | tests\Directory.Build.props = tests\Directory.Build.props 23 | EndProjectSection 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{E89606EC-111B-4151-997C-8006627F1926}" 26 | ProjectSection(SolutionItems) = preProject 27 | dotnet-tools.json = dotnet-tools.json 28 | readme.md = readme.md 29 | build.sh = build.sh 30 | build.bat = build.bat 31 | Directory.Build.props = Directory.Build.props 32 | global.json = global.json 33 | .editorconfig = .editorconfig 34 | .gitignore = .gitignore 35 | .github\workflows\ci.yml = .github\workflows\ci.yml 36 | EndProjectSection 37 | EndProject 38 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{E4B3DD3A-E36C-46D6-B35E-D824EB9B3C06}" 39 | EndProject 40 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Proc.Fs", "src\Proc.Fs\Proc.Fs.fsproj", "{5EA4E26F-F623-473D-9CD7-E590A3E54239}" 41 | EndProject 42 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ScratchPad.Fs", "examples\ScratchPad.Fs\ScratchPad.Fs.fsproj", "{C33E3F7C-0C2A-4DD2-91E4-328866195997}" 43 | EndProject 44 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ScratchPad.Fs.ArgumentPrinter", "examples\ScratchPad.Fs.ArgumentPrinter\ScratchPad.Fs.ArgumentPrinter.fsproj", "{50A40BEB-1C22-41CF-908F-F24FB34B1699}" 45 | EndProject 46 | Global 47 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 48 | Debug|Any CPU = Debug|Any CPU 49 | Release|Any CPU = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 52 | {4BE05F38-DAA3-45E3-A2F5-B7E6408941AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {4BE05F38-DAA3-45E3-A2F5-B7E6408941AD}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {4BE05F38-DAA3-45E3-A2F5-B7E6408941AD}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {4BE05F38-DAA3-45E3-A2F5-B7E6408941AD}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {E7AD2461-309A-4BA7-A6EB-5C1F4F882BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {E7AD2461-309A-4BA7-A6EB-5C1F4F882BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {E7AD2461-309A-4BA7-A6EB-5C1F4F882BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {E7AD2461-309A-4BA7-A6EB-5C1F4F882BC2}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {D3AFFBE0-8F40-42C7-8A6A-BCCD2EDF4A3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {D3AFFBE0-8F40-42C7-8A6A-BCCD2EDF4A3E}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {D3AFFBE0-8F40-42C7-8A6A-BCCD2EDF4A3E}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {D3AFFBE0-8F40-42C7-8A6A-BCCD2EDF4A3E}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {BE04AF1C-20CB-497B-9725-518C0C8109ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {BE04AF1C-20CB-497B-9725-518C0C8109ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {BE04AF1C-20CB-497B-9725-518C0C8109ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {BE04AF1C-20CB-497B-9725-518C0C8109ED}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {733EB608-B8B4-47FE-AB63-A4C7856C4209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {733EB608-B8B4-47FE-AB63-A4C7856C4209}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {733EB608-B8B4-47FE-AB63-A4C7856C4209}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {733EB608-B8B4-47FE-AB63-A4C7856C4209}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {D6997ADC-E933-418E-831C-DE1A78897493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {D6997ADC-E933-418E-831C-DE1A78897493}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {D6997ADC-E933-418E-831C-DE1A78897493}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {D6997ADC-E933-418E-831C-DE1A78897493}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {5EA4E26F-F623-473D-9CD7-E590A3E54239}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {5EA4E26F-F623-473D-9CD7-E590A3E54239}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {5EA4E26F-F623-473D-9CD7-E590A3E54239}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {5EA4E26F-F623-473D-9CD7-E590A3E54239}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {C33E3F7C-0C2A-4DD2-91E4-328866195997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 81 | {C33E3F7C-0C2A-4DD2-91E4-328866195997}.Debug|Any CPU.Build.0 = Debug|Any CPU 82 | {C33E3F7C-0C2A-4DD2-91E4-328866195997}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {C33E3F7C-0C2A-4DD2-91E4-328866195997}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {50A40BEB-1C22-41CF-908F-F24FB34B1699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 85 | {50A40BEB-1C22-41CF-908F-F24FB34B1699}.Debug|Any CPU.Build.0 = Debug|Any CPU 86 | {50A40BEB-1C22-41CF-908F-F24FB34B1699}.Release|Any CPU.ActiveCfg = Release|Any CPU 87 | {50A40BEB-1C22-41CF-908F-F24FB34B1699}.Release|Any CPU.Build.0 = Release|Any CPU 88 | EndGlobalSection 89 | GlobalSection(SolutionProperties) = preSolution 90 | HideSolutionNode = FALSE 91 | EndGlobalSection 92 | GlobalSection(NestedProjects) = preSolution 93 | {4BE05F38-DAA3-45E3-A2F5-B7E6408941AD} = {9C336E9A-3FC8-4F77-A5B4-1D07E4E54924} 94 | {733EB608-B8B4-47FE-AB63-A4C7856C4209} = {9C336E9A-3FC8-4F77-A5B4-1D07E4E54924} 95 | {BE04AF1C-20CB-497B-9725-518C0C8109ED} = {0F267D58-B5AA-4D04-B346-12B283E37B68} 96 | {D3AFFBE0-8F40-42C7-8A6A-BCCD2EDF4A3E} = {0F267D58-B5AA-4D04-B346-12B283E37B68} 97 | {E7AD2461-309A-4BA7-A6EB-5C1F4F882BC2} = {E4B3DD3A-E36C-46D6-B35E-D824EB9B3C06} 98 | {D6997ADC-E933-418E-831C-DE1A78897493} = {E89606EC-111B-4151-997C-8006627F1926} 99 | {5EA4E26F-F623-473D-9CD7-E590A3E54239} = {9C336E9A-3FC8-4F77-A5B4-1D07E4E54924} 100 | {C33E3F7C-0C2A-4DD2-91E4-328866195997} = {E4B3DD3A-E36C-46D6-B35E-D824EB9B3C06} 101 | {50A40BEB-1C22-41CF-908F-F24FB34B1699} = {E4B3DD3A-E36C-46D6-B35E-D824EB9B3C06} 102 | EndGlobalSection 103 | EndGlobal 104 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Proc 2 | 3 | 4 | 5 | 7 | 8 | A dependency free `System.Diagnostics.Process` supercharger. 9 | 10 | 1. `Proc.Exec()` for the quick one-liners 11 | 2. `Proc.Start()` for the quick one-liners 12 | - Use if you want to capture the console output as well as print these message in real time. 13 | - Proc.Start() also allows you to script StandardIn and react to messages 14 | 3. Wraps `System.Diagnostics.Process` as an `IObservable` 15 | * `ProcessObservable` stream based wrapper 16 | * `EventBasedObservableProcess` event based wrapper 17 | 4. Built in support to send `SIGINT` to any process before doing a hard `SIGKILL` (`Process.Kill()`) 18 | * Has to be set using `SendControlCFirst = true` on `StartArguments` 19 | 20 | ## Proc.Exec 21 | 22 | Execute a process and blocks using a default timeout of 4 minutes. This method uses the same console session 23 | as and as such will print the binaries console output. Throws a `ProcExecException` if the command fails to execute. 24 | See also `ExecArguments` for more options 25 | 26 | ```csharp 27 | Proc.Exec("ipconfig", "/all"); 28 | ``` 29 | 30 | ## Proc.Start 31 | 32 | start a process and block using the default timeout of 4 minutes 33 | ```csharp 34 | var result = Proc.Start("ipconfig", "/all"); 35 | ``` 36 | 37 | Provide a custom timeout and an `IConsoleOutWriter` that can output to console 38 | while this line is blocking. The following example writes `stderr` in red. 39 | 40 | ```csharp 41 | var result = Proc.Start("ipconfig", TimeSpan.FromSeconds(10), new ConsoleOutColorWriter()); 42 | ``` 43 | 44 | More options can be passed by passing `StartArguments` instead to control how the process should start. 45 | 46 | ```csharp 47 | var args = new StartArguments("ipconfig", "/all") 48 | { 49 | WorkingDirectory = .. 50 | } 51 | Proc.Start(args, TimeSpan.FromSeconds(10)); 52 | ``` 53 | 54 | The static `Proc.Start` has a timeout of `4 minutes` if not specified. 55 | 56 | `result` has the following properties 57 | 58 | * `Completed` true if the program completed before the timeout 59 | * `ConsoleOut` a list the console out message as `LineOut` 60 | instances where `Error` on each indicating whether it was written on `stderr` or not 61 | * `ExitCode` 62 | 63 | **NOTE** `ConsoleOut` will always be set regardless of whether an `IConsoleOutWriter` is provided 64 | 65 | ## ObservableProcess 66 | 67 | The heart of it all this is an `IObservable`. It listens on the output buffers directly and does not wait on 68 | newlines to emit. 69 | 70 | To create an observable process manually follow the following pattern: 71 | 72 | ```csharp 73 | using (var p = new ObservableProcess(args)) 74 | { 75 | p.Subscribe(c => Console.Write(c.Characters)); 76 | p.WaitForCompletion(TimeSpan.FromSeconds(2)); 77 | } 78 | ``` 79 | 80 | The observable is `cold` untill subscribed and is not intended to be reused or subscribed to multiple times. If you need to 81 | share a subscription look into RX's `Publish`. 82 | 83 | The `WaitForCompletion()` call blocks so that `p` is not disposed which would attempt to shutdown the started process. 84 | 85 | The default for doing a shutdown is through `Process.Kill` this is a hard `SIGKILL` on the process. 86 | 87 | The cool thing about `Proc` is that it supports `SIGINT` interoptions as well to allow for processes to be cleanly shutdown. 88 | 89 | ```csharp 90 | var args = new StartArguments("elasticsearch.bat") 91 | { 92 | SendControlCFirst = true 93 | }; 94 | ``` 95 | 96 | This will attempt to send a `Control+C` into the running process console on windows first before falling back to `Process.Kill`. 97 | Linux and OSX support for this flag is still in the works so thats why this behaviour is opt in. 98 | 99 | 100 | Dealing with `byte[]` characters might not be what you want to program against, so `ObservableProcess` allows the following as well. 101 | 102 | 103 | ```csharp 104 | using (var p = new ObservableProcess(args)) 105 | { 106 | p.SubscribeLines(c => Console.WriteLine(c.Line)); 107 | p.WaitForCompletion(TimeSpan.FromSeconds(2)); 108 | } 109 | ``` 110 | 111 | Instead of proxying `byte[]` as they are received on the socket this buffers and only emits on lines. 112 | 113 | In some cases it can be very useful to introduce your own word boundaries 114 | 115 | ```csharp 116 | public class MyProcObservable : ObservableProcess 117 | { 118 | public MyProcObservable(string binary, params string[] arguments) : base(binary, arguments) { } 119 | 120 | public MyProcObservable(StartArguments startArguments) : base(startArguments) { } 121 | 122 | protected override bool BufferBoundary(char[] stdOut, char[] stdErr) 123 | { 124 | return base.BufferBoundary(stdOut, stdErr); 125 | } 126 | } 127 | ``` 128 | 129 | returning true inside `BufferBoundary` will yield the line to `SubscribeLine()`. This could be usefull e.g if your process 130 | prompts without a new line: 131 | 132 | > Continue [Y/N]: 133 | 134 | A more concrete example of this is when you call a `bat` file on windows and send a `SIGINT` signal it will *always* prompt: 135 | 136 | > Terminate batch job (Y/N)? 137 | 138 | Which would not yield to `SubscribeLines` and block any waithandles unnecessary. `ObservableProcess` handles this edgecase 139 | therefor OOTB and automatically replies with `Y` on `stdin` in this case. 140 | 141 | Also note that `ObservableProcess` will yield whatever is in the buffer before OnCompleted(). 142 | 143 | 144 | # EventBasedObservable 145 | 146 | `ObservableProcess`'s sibbling that utilizes `OutputDataReceived` and `ErrorDataReceived` and can only emit lines. 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/Proc.ControlC/ControlCDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.InteropServices; 4 | using System.Security.Permissions; 5 | using static System.Diagnostics.Process; 6 | 7 | namespace Proc.ControlC 8 | { 9 | [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)] 10 | internal static class ControlCDispatcher 11 | { 12 | private const int CTRL_C_EVENT = 0; 13 | 14 | [DllImport("kernel32.dll", SetLastError = true)] 15 | private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId); 16 | 17 | [DllImport("kernel32.dll", SetLastError = true)] 18 | private static extern bool AttachConsole(uint dwProcessId); 19 | 20 | [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] 21 | private static extern bool FreeConsole(); 22 | 23 | [DllImport("kernel32.dll", SetLastError = true)] 24 | private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add); 25 | 26 | private delegate bool ConsoleCtrlDelegate(uint CtrlType); 27 | 28 | public static bool Send(int processId) 29 | { 30 | if (!IsAProcessAndIsRunning(processId)) return false; 31 | 32 | if (!FreeConsole()) throw new Win32Exception(); 33 | if (!AttachConsole((uint) processId)) throw new Win32Exception(); 34 | 35 | if (!SetConsoleCtrlHandler(null, true)) throw new Win32Exception(); 36 | try 37 | { 38 | if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) throw new Win32Exception(); 39 | } 40 | finally 41 | { 42 | FreeConsole(); 43 | SetConsoleCtrlHandler(null, false); 44 | } 45 | return true; 46 | } 47 | 48 | private static bool IsAProcessAndIsRunning(int processId) 49 | { 50 | try 51 | { 52 | var p = GetProcessById(processId); 53 | Console.WriteLine(p.ProcessName); 54 | return true; 55 | } 56 | catch 57 | { 58 | return false; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Proc.ControlC/Proc.ControlC.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net40 5 | false 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Proc.ControlC/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | 4 | namespace Proc.ControlC 5 | { 6 | public static class Program 7 | { 8 | public static int Main(string[] args) 9 | { 10 | Console.WriteLine("Calling" + string.Join(" ", args)); 11 | if (args.Length == 0) return 1; 12 | if (!int.TryParse(args[0], out var processId)) return 2; 13 | 14 | try 15 | { 16 | var success = ControlCDispatcher.Send(processId) ? 0 : 1; 17 | Console.WriteLine("success: " + success); 18 | return success; 19 | } 20 | catch (Win32Exception e) 21 | { 22 | Console.WriteLine(e.Message); 23 | return 3; 24 | } 25 | catch (Exception e) 26 | { 27 | Console.WriteLine(e.Message); 28 | return 4; 29 | } 30 | 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Proc.Fs/Bindings.fs: -------------------------------------------------------------------------------- 1 | module Proc.Fs 2 | 3 | open System 4 | open ProcNet 5 | open ProcNet.Std 6 | 7 | type ExecOptions = { 8 | Binary: string 9 | Arguments: string list option 10 | LineOutFilter: (LineOut -> bool) option 11 | Find: (LineOut -> bool) option 12 | WorkingDirectory: string option 13 | Environment: Map option 14 | 15 | Timeout: TimeSpan 16 | ValidExitCodeClassifier: (int -> bool) option 17 | 18 | StartedConfirmationHandler: (LineOut -> bool) option 19 | StopBufferingAfterStarted: bool option 20 | 21 | NoWrapInThread: bool option 22 | SendControlCFirst: bool option 23 | WaitForStreamReadersTimeout: TimeSpan option 24 | } 25 | with 26 | static member Empty = 27 | { 28 | Binary = ""; Arguments = None; Find = None; 29 | LineOutFilter = None; WorkingDirectory = None; Environment = None 30 | Timeout = TimeSpan(0, 0, 0, 0, -1) 31 | ValidExitCodeClassifier = None; 32 | NoWrapInThread = None; SendControlCFirst = None; WaitForStreamReadersTimeout = None 33 | StartedConfirmationHandler = None; StopBufferingAfterStarted = None 34 | } 35 | 36 | let private startArgs (opts: ExecOptions) = 37 | let args = opts.Arguments |> Option.defaultValue [] 38 | let startArguments = StartArguments(opts.Binary, args) 39 | opts.LineOutFilter |> Option.iter(fun f -> startArguments.LineOutFilter <- f) 40 | opts.Environment |> Option.iter(fun e -> startArguments.Environment <- e) 41 | opts.WorkingDirectory |> Option.iter(fun d -> startArguments.WorkingDirectory <- d) 42 | opts.NoWrapInThread |> Option.iter(fun b -> startArguments.NoWrapInThread <- b) 43 | opts.SendControlCFirst |> Option.iter(fun b -> startArguments.SendControlCFirst <- b) 44 | opts.WaitForStreamReadersTimeout |> Option.iter(fun t -> startArguments.WaitForStreamReadersTimeout <- t) 45 | startArguments.Timeout <- opts.Timeout 46 | startArguments 47 | 48 | let private execArgs (opts: ExecOptions) = 49 | let args = opts.Arguments |> Option.defaultValue [] 50 | let execArguments = ExecArguments(opts.Binary, args) 51 | opts.Environment |> Option.iter(fun e -> execArguments.Environment <- e) 52 | opts.WorkingDirectory |> Option.iter(fun d -> execArguments.WorkingDirectory <- d) 53 | opts.ValidExitCodeClassifier |> Option.iter(fun f -> execArguments.ValidExitCodeClassifier <- f) 54 | execArguments.Timeout <- opts.Timeout 55 | execArguments 56 | 57 | let private longRunningArguments (opts: ExecOptions) = 58 | let args = opts.Arguments |> Option.defaultValue [] 59 | let longRunningArguments = LongRunningArguments(opts.Binary, args) 60 | opts.LineOutFilter |> Option.iter(fun f -> longRunningArguments.LineOutFilter <- f) 61 | opts.Environment |> Option.iter(fun e -> longRunningArguments.Environment <- e) 62 | opts.WorkingDirectory |> Option.iter(fun d -> longRunningArguments.WorkingDirectory <- d) 63 | opts.NoWrapInThread |> Option.iter(fun b -> longRunningArguments.NoWrapInThread <- b) 64 | opts.SendControlCFirst |> Option.iter(fun b -> longRunningArguments.SendControlCFirst <- b) 65 | opts.WaitForStreamReadersTimeout |> Option.iter(fun t -> longRunningArguments.WaitForStreamReadersTimeout <- t) 66 | 67 | opts.StartedConfirmationHandler |> Option.iter(fun t -> longRunningArguments.StartedConfirmationHandler <- t) 68 | opts.StopBufferingAfterStarted |> Option.iter(fun t -> longRunningArguments.StopBufferingAfterStarted <- t) 69 | 70 | longRunningArguments 71 | 72 | 73 | type ShellBuilder() = 74 | 75 | member t.Yield _ = ExecOptions.Empty 76 | 77 | [] 78 | member inline this.WorkingDirectory(opts, workingDirectory: string) = 79 | { opts with WorkingDirectory = Some workingDirectory } 80 | 81 | [] 82 | member inline this.EnvironmentVariables(opts, env: Map) = 83 | { opts with Environment = Some env } 84 | 85 | [] 86 | member inline this.Timeout(opts, timeout) = 87 | { opts with Timeout = timeout } 88 | 89 | [] 90 | member inline this.WaitForStreamReadersTimeout(opts, timeout) = 91 | { opts with WaitForStreamReadersTimeout = Some timeout } 92 | 93 | [] 94 | member inline this.SendControlCFirst(opts, sendControlCFirst) = 95 | { opts with SendControlCFirst = Some sendControlCFirst } 96 | 97 | [] 98 | member inline this.NoWrapInThread(opts, threadWrap) = 99 | { opts with NoWrapInThread = Some (not threadWrap) } 100 | 101 | [] 102 | member this.ExecuteWithArguments(opts, binary, [] args: string array) = 103 | let opts = { opts with Binary = binary; Arguments = Some (args |> List.ofArray) } 104 | let execArgs = execArgs opts 105 | Proc.Exec(execArgs) |> ignore 106 | opts 107 | 108 | [] 109 | member this.ExecuteWithArguments(opts, binary, args: string list) = 110 | let opts = { opts with Binary = binary; Arguments = Some args } 111 | let execArgs = execArgs opts 112 | Proc.Exec(execArgs) |> ignore 113 | opts 114 | 115 | 116 | let shell = ShellBuilder() 117 | 118 | type ExecBuilder() = 119 | 120 | member t.Yield _ = ExecOptions.Empty 121 | 122 | ///Runs using immediately 123 | /// the computation build thus far, not specified directly 124 | /// The binary to execute 125 | /// the arguments to pass on to the binary being executed 126 | [] 127 | member this.Execute(opts, binary, [] arguments: string array) = 128 | let opts = { opts with Binary = binary; Arguments = Some (arguments |> List.ofArray)} 129 | let execArgs = execArgs opts 130 | Proc.Exec(execArgs) |> ignore 131 | 132 | ///Runs using immediately 133 | /// the computation build thus far, not specified directly 134 | /// The binary to execute 135 | /// the arguments to pass on to the binary being executed 136 | [] 137 | member this.Execute(opts, binary, arguments: string list) = 138 | let opts = { opts with Binary = binary; Arguments = Some arguments} 139 | let execArgs = execArgs opts 140 | Proc.Exec(execArgs) |> ignore 141 | 142 | /// 143 | /// Runs the the computation build thus far. 144 | /// Needs at least `binary` to be specified 145 | /// 146 | [] 147 | member this.Execute(opts) = 148 | if opts.Binary = "" then failwithf "No binary specified to exec computation expression" 149 | let execArgs = execArgs opts 150 | Proc.Exec(execArgs) |> ignore 151 | 152 | ///Supply external to bootstrap 153 | [] 154 | member this.Options(_, reuseOptions: ExecOptions) = 155 | reuseOptions 156 | 157 | ///The binary to execute 158 | /// the computation build thus far, not specified directly 159 | /// The binary to execute 160 | [] 161 | member this.Binary(opts, binary) = 162 | { opts with Binary = binary } 163 | 164 | ///The arguments to call the binary with 165 | /// the computation build thus far, not specified directly 166 | /// The arguments to call the binary with 167 | [] 168 | member this.Arguments(opts, [] arguments: string array) = 169 | { opts with Arguments = Some (arguments |> List.ofArray) } 170 | 171 | ///The arguments to call the binary with 172 | /// the computation build thus far, not specified directly 173 | /// The arguments to call the binary with 174 | [] 175 | member this.Arguments(opts, arguments: string list) = 176 | { opts with Arguments = Some arguments} 177 | 178 | ///Specify a working directory to start the execution of the binary in 179 | /// the computation build thus far, not specified directly 180 | /// Specify a working directory to start the execution of the binary in 181 | [] 182 | member this.WorkingDirectory(opts, workingDirectory: string) = 183 | { opts with WorkingDirectory = Some workingDirectory } 184 | 185 | [] 186 | member this.EnvironmentVariables(opts, env: Map) = 187 | { opts with Environment = Some env } 188 | 189 | [] 190 | member this.Timeout(opts, timeout) = 191 | { opts with Timeout = timeout } 192 | 193 | [] 194 | member this.WaitForStreamReadersTimeout(opts, timeout) = 195 | { opts with WaitForStreamReadersTimeout = Some timeout } 196 | 197 | [] 198 | member this.SendControlCFirst(opts, sendControlCFirst) = 199 | { opts with SendControlCFirst = Some sendControlCFirst } 200 | 201 | [] 202 | member this.NoWrapInThread(opts, threadWrap) = 203 | { opts with NoWrapInThread = Some (not threadWrap) } 204 | 205 | [] 206 | member this.FilterOutput(opts, find: LineOut -> bool) = 207 | { opts with LineOutFilter = Some find } 208 | 209 | [] 210 | member this.ValidExitCode(opts, exitCodeClassifier: int -> bool) = 211 | { opts with ValidExitCodeClassifier = Some exitCodeClassifier } 212 | 213 | [] 214 | member this.Find(opts, find: LineOut -> bool) = 215 | let opts = { opts with Find = Some find } 216 | let startArguments = startArgs opts 217 | let result = Proc.Start(startArguments) 218 | result.ConsoleOut 219 | |> Seq.find find 220 | 221 | [] 222 | member this.Filter(opts, find: LineOut -> bool) = 223 | let opts = { opts with Find = Some find } 224 | let startArguments = startArgs opts 225 | let result = Proc.Start(startArguments) 226 | result.ConsoleOut 227 | |> Seq.filter find 228 | |> List.ofSeq 229 | 230 | [] 231 | member this.Output(opts) = 232 | let startArguments = startArgs opts 233 | Proc.Start(startArguments) 234 | 235 | [] 236 | member this.InvokeArgs(opts, [] arguments: string array) = 237 | let opts = { opts with Arguments = Some (arguments |> List.ofArray) } 238 | let execArgs = execArgs opts 239 | Proc.Exec(execArgs) |> ignore 240 | 241 | [] 242 | member this.InvokeArgs(opts, arguments: string list) = 243 | let opts = { opts with Arguments = Some arguments} 244 | let execArgs = execArgs opts 245 | Proc.Exec(execArgs) |> ignore 246 | 247 | [] 248 | member this.ReturnStatus(opts, binary, arguments: string list) = 249 | let opts = { opts with Binary = binary; Arguments = Some arguments} 250 | let execArgs = execArgs opts 251 | Proc.Exec(execArgs) 252 | 253 | [] 254 | member this.ReturnStatus(opts, binary, [] arguments: string array) = 255 | let opts = { opts with Binary = binary; Arguments = Some (arguments |> List.ofArray)} 256 | let execArgs = execArgs opts 257 | Proc.Exec(execArgs) 258 | 259 | [] 260 | member this.ReturnStatus(opts) = 261 | let execArgs = execArgs opts 262 | Proc.Exec(execArgs) 263 | 264 | [] 265 | member this.ReturnOutput(opts, binary, [] arguments: string array) = 266 | let opts = { opts with Binary = binary; Arguments = Some (arguments |> List.ofArray)} 267 | let execArgs = startArgs opts 268 | Proc.Start(execArgs) 269 | 270 | [] 271 | member this.ReturnOutput(opts) = 272 | let startArgs = startArgs opts 273 | Proc.Start(startArgs) 274 | 275 | [] 276 | member this.WaitUntil(opts, startedConfirmation: LineOut -> bool) = 277 | let opts = { opts with StartedConfirmationHandler = Some startedConfirmation } 278 | let longRunningArguments = longRunningArguments opts 279 | Proc.StartLongRunning(longRunningArguments, opts.Timeout) 280 | 281 | [] 282 | member this.WaitUntilQuietAfter(opts, startedConfirmation: LineOut -> bool) = 283 | let opts = 284 | { 285 | opts with 286 | StartedConfirmationHandler = Some startedConfirmation 287 | StopBufferingAfterStarted = Some true 288 | } 289 | let longRunningArguments = longRunningArguments opts 290 | Proc.StartLongRunning(longRunningArguments, opts.Timeout) 291 | 292 | 293 | 294 | let exec = ExecBuilder() 295 | 296 | 297 | -------------------------------------------------------------------------------- /src/Proc.Fs/Proc.Fs.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | netstandard2.0 6 | 7 | true 8 | ..\..\build\keys\keypair.snk 9 | 10 | nuget-icon.png 11 | MIT 12 | https://github.com/nullean/proc 13 | https://github.com/nullean/proc 14 | https://github.com/nullean/proc/releases 15 | 16 | Proc.Fs 17 | Proc.Fs - F# bindings for the easiest and full featured way to execute Processes in .NET 18 | Dependency free reactive abstraction around Process, exposes handy static methods for the quick one-liners 19 | 20 | $(ProcCurrentVersion) 21 | $(ProcCurrentVersion) 22 | $(ProcCurrentAssemblyVersion) 23 | $(ProcCurrentAssemblyFileVersion) 24 | true 25 | true 26 | Latest 27 | README.md 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | nuget-icon.png 38 | True 39 | nuget-icon.png 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Proc.Fs/README.md: -------------------------------------------------------------------------------- 1 | # Proc.Fs 2 | 3 | F# bindings for Proc. 4 | 5 | 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. 6 | 7 | The library ships with two computation expression builders: 8 | 9 | # Shell 10 | 11 | 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` 12 | 13 | ```fsharp 14 | let _ = shell { 15 | exec "dotnet" "--version" 16 | exec "uname" 17 | } 18 | ``` 19 | 20 | # Exec 21 | 22 | A `CE` to make it **REAL** easy to execute processes. 23 | 24 | ```fsharp 25 | //executes dotnet --help 26 | exec { run "dotnet" "--help" } 27 | 28 | //supports lists as args too 29 | exec { run "dotnet" ["--help"] } 30 | ``` 31 | 32 | If you want more info about the invocation 33 | you can use either `exit_code_of` or `output_of` 34 | for the quick one liners. 35 | 36 | 37 | ```fsharp 38 | let exitCode = exec { exit_code_of "dotnet" "--help" } 39 | let output = exec { output_of "dotnet" "--help" } 40 | ``` 41 | `output` will hold both the exit code and the console output 42 | 43 | If you need more control on how the process is started 44 | you can supply the following options. 45 | 46 | 47 | ```fsharp 48 | exec { 49 | binary "dotnet" 50 | arguments "--help" 51 | env Map[("key", "value")] 52 | workingDirectory "." 53 | send_control_c false 54 | timeout (TimeSpan.FromSeconds(10)) 55 | thread_wrap false 56 | filter_output (fun l -> l.Line.Contains "clean") 57 | validExitCode (fun i -> i <> 0) 58 | run 59 | } 60 | ``` 61 | 62 | `run` will kick off the invocation of the process. 63 | 64 | However there are other ways to kick this off too. 65 | 66 | ```fsharp 67 | exec { 68 | binary "dotnet" 69 | run_args ["restore"; "--help"] 70 | } 71 | ``` 72 | Shortcut to supply arguments AND run 73 | 74 | ```fsharp 75 | let linesContainingClean = exec { 76 | binary "dotnet" 77 | arguments "--help" 78 | filter (fun l -> l.Line.Contains "clean") 79 | } 80 | ``` 81 | 82 | 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`. 83 | 84 | 85 | ```fsharp 86 | 87 | let dotnetHelpExitCode = exec { 88 | binary "dotnet" 89 | arguments "--help" 90 | exit_code 91 | } 92 | ``` 93 | 94 | returns just the exit code 95 | 96 | ```fsharp 97 | 98 | let helpOutput = exec { 99 | binary "dotnet" 100 | arguments "--help" 101 | output 102 | } 103 | ``` 104 | 105 | returns the exit code and the full console output. 106 | 107 | ```fsharp 108 | 109 | let process = exec { 110 | binary "dotnet" 111 | arguments "--help" 112 | wait_until (fun l -> l.Line.Contains "clean") 113 | } 114 | ``` 115 | 116 | returns an already running process but only after it confirms a line was printed 117 | ```fsharp 118 | 119 | let process = exec { 120 | binary "dotnet" 121 | arguments "--help" 122 | wait_until_and_disconnect (fun l -> l.Line.Contains "clean") 123 | } 124 | ``` 125 | 126 | returns an already running process but only after it confirms a line was printed. This version will stop the yielding standard/out lines which may utilize memory consumption which is no longer needed. 127 | -------------------------------------------------------------------------------- /src/Proc/BufferedObservableProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reactive.Disposables; 4 | using System.Reactive.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using ProcNet.Extensions; 8 | using ProcNet.Std; 9 | 10 | namespace ProcNet 11 | { 12 | /// 13 | /// This reads and using . 14 | /// 15 | /// When the process exits it waits for these stream readers to finish up to whatever 16 | /// is configured to. 17 | /// 18 | /// This catches all cases where would fall short to capture all the process output. 19 | /// 20 | /// contains the current char[] buffer which could be fed to directly. 21 | /// 22 | /// Note that there is a subclass that allows you to subscribe console output per line 23 | /// instead whilst still reading the actual process output using asynchronous stream readers. 24 | /// 25 | public class BufferedObservableProcess : ObservableProcessBase 26 | { 27 | public BufferedObservableProcess(string binary, params string[] arguments) : base(binary, arguments) { } 28 | 29 | public BufferedObservableProcess(StartArguments startArguments) : base(startArguments) { } 30 | 31 | /// 32 | /// How long we should wait for the output stream readers to finish when the process exits before we call 33 | /// is called. By default waits for 10 seconds. 34 | /// 35 | private TimeSpan? WaitForStreamReadersTimeout => StartArguments.WaitForStreamReadersTimeout; 36 | 37 | /// 38 | /// Expert level setting: the maximum number of characters to read per iteration. Defaults to 1024 39 | /// 40 | public int BufferSize { get; set; } = 1024; 41 | 42 | private CancellationTokenSource _ctx = new(); 43 | private Task _stdOutSubscription; 44 | private Task _stdErrSubscription; 45 | private IObserver _observer; 46 | 47 | protected override IObservable CreateConsoleOutObservable() 48 | { 49 | if (NoWrapInThread) 50 | return Observable.Create(observer => 51 | { 52 | var disposable = KickOff(observer); 53 | return disposable; 54 | }); 55 | 56 | return Observable.Create(async observer => 57 | { 58 | var disposable = await Task.Run(() => KickOff(observer)); 59 | return disposable; 60 | }); 61 | } 62 | 63 | /// 64 | /// Expert setting, subclasses can return true if a certain condition is met to break out of the async readers on StandardOut and StandardError 65 | /// 66 | /// 67 | protected virtual bool ContinueReadingFromProcessReaders() => true; 68 | 69 | private IDisposable KickOff(IObserver observer) 70 | { 71 | if (!StartProcess(observer)) return Disposable.Empty; 72 | 73 | Started = true; 74 | _observer = observer; 75 | 76 | if (Process.HasExited) 77 | { 78 | Process.ReadStandardErrBlocking(observer, BufferSize, () => ContinueReadingFromProcessReaders()); 79 | Process.ReadStandardOutBlocking(observer, BufferSize, () => ContinueReadingFromProcessReaders()); 80 | OnExit(observer); 81 | return Disposable.Empty; 82 | } 83 | 84 | StartAsyncReads(); 85 | 86 | Process.Exited += (o, s) => 87 | { 88 | WaitForEndOfStreams(observer, _stdOutSubscription, _stdErrSubscription); 89 | OnExit(observer); 90 | }; 91 | 92 | var disposable = Disposable.Create(() => 93 | { 94 | }); 95 | 96 | return disposable; 97 | } 98 | 99 | private readonly object _lock = new object(); 100 | private bool _reading = false; 101 | 102 | /// 103 | /// Allows you to stop reading the console output after subscribing on the observable while leaving the underlying 104 | /// process running. 105 | /// 106 | public void CancelAsyncReads() 107 | { 108 | if (!_reading) return; 109 | lock (_lock) 110 | { 111 | if (!_reading) return; 112 | try 113 | { 114 | Process.StandardOutput.BaseStream.Flush(); 115 | Process.StandardError.BaseStream.Flush(); 116 | _ctx.Cancel(); 117 | } 118 | finally 119 | { 120 | _ctx = new CancellationTokenSource(); 121 | _reading = false; 122 | } 123 | } 124 | } 125 | 126 | public bool IsActivelyReading => IsNotCompletedTask(_stdOutSubscription) && IsNotCompletedTask(_stdErrSubscription); 127 | 128 | private static bool IsNotCompletedTask(Task t) => t.Status != TaskStatus.Canceled && t.Status != TaskStatus.RanToCompletion && t.Status != TaskStatus.Faulted; 129 | 130 | /// 131 | /// Start reading the console output again after calling 132 | /// 133 | public void StartAsyncReads() 134 | { 135 | if (_reading) return; 136 | lock (_lock) 137 | { 138 | if (_reading) return; 139 | 140 | Process.StandardOutput.BaseStream.Flush(); 141 | Process.StandardError.BaseStream.Flush(); 142 | 143 | _stdOutSubscription = Process.ObserveStandardOutBuffered(_observer, BufferSize, () => ContinueReadingFromProcessReaders(), _ctx.Token); 144 | _stdErrSubscription = Process.ObserveErrorOutBuffered(_observer, BufferSize, () => ContinueReadingFromProcessReaders(), _ctx.Token); 145 | _reading = true; 146 | } 147 | } 148 | 149 | protected virtual void OnBeforeWaitForEndOfStreamsError(TimeSpan waited) { } 150 | 151 | private void WaitForEndOfStreams(IObserver observer, Task stdOutSubscription, Task stdErrSubscription) 152 | { 153 | if (!_reading) return; 154 | SendYesForBatPrompt(); 155 | if (!WaitForStreamReadersTimeout.HasValue) 156 | { 157 | CancelAsyncReads(); 158 | } 159 | else if (!Task.WaitAll(new[] {stdOutSubscription, stdErrSubscription}, WaitForStreamReadersTimeout.Value)) 160 | { 161 | CancelAsyncReads(); 162 | OnBeforeWaitForEndOfStreamsError(WaitForStreamReadersTimeout.Value); 163 | OnError(observer, new WaitForEndOfStreamsTimeoutException(WaitForStreamReadersTimeout.Value)); 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Proc/CleanExitExceptionBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProcNet 4 | { 5 | 6 | public class WaitForEndOfStreamsTimeoutException : ObservableProcessException 7 | { 8 | public WaitForEndOfStreamsTimeoutException(TimeSpan wait) 9 | : base($"Waited {wait} unsuccesfully for stdout/err subscriptions to complete after the the process exited") { } 10 | } 11 | 12 | public class ObservableProcessException : CleanExitExceptionBase 13 | { 14 | public ObservableProcessException(string message) : base(message) { } 15 | public ObservableProcessException(string message, string helpText, Exception innerException) : base(message, helpText, innerException) { } 16 | } 17 | 18 | public class CleanExitExceptionBase : Exception 19 | { 20 | public CleanExitExceptionBase(string message) : base(message) { } 21 | public CleanExitExceptionBase(string message, string helpText, Exception innerException) 22 | : base(message, innerException) => HelpText = helpText; 23 | 24 | public string HelpText { get; private set; } 25 | } 26 | 27 | public class ProcExecException : CleanExitExceptionBase 28 | { 29 | public ProcExecException(string message) : base(message) { } 30 | 31 | public ProcExecException(string message, string helpText, Exception innerException) 32 | : base(message, helpText, innerException) { } 33 | 34 | public int? ExitCode { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Proc/Embedded/Proc.ControlC.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nullean/proc/767c37244459892f88a6f2514292c249c2518272/src/Proc/Embedded/Proc.ControlC.exe -------------------------------------------------------------------------------- /src/Proc/EventBasedObservableProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Reactive; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using System.Threading.Tasks; 7 | using ProcNet.Extensions; 8 | using ProcNet.Std; 9 | 10 | namespace ProcNet 11 | { 12 | /// 13 | /// This implementation wraps over and 14 | /// it utilizes a double call to once with timeout and once without to ensure all events are 15 | /// received. 16 | /// 17 | public class EventBasedObservableProcess: ObservableProcessBase, ISubscribeLines 18 | { 19 | public EventBasedObservableProcess(string binary, params string[] arguments) : base(binary, arguments) { } 20 | 21 | public EventBasedObservableProcess(StartArguments startArguments) : base(startArguments) { } 22 | 23 | protected override IObservable CreateConsoleOutObservable() 24 | { 25 | if (NoWrapInThread) 26 | return Observable.Create(observer => KickOff(observer)); 27 | 28 | return Observable.Create(async observer => 29 | { 30 | var disposable = await Task.Run(() => KickOff(observer)); 31 | return disposable; 32 | }); 33 | } 34 | 35 | private CompositeDisposable KickOff(IObserver observer) 36 | { 37 | var stdOut = Process.ObserveStandardOutLineByLine(); 38 | var stdErr = Process.ObserveErrorOutLineByLine(); 39 | 40 | var stdOutSubscription = stdOut.Subscribe(observer); 41 | var stdErrSubscription = stdErr.Subscribe(observer); 42 | 43 | var processExited = Observable.FromEventPattern(h => Process.Exited += h, h => Process.Exited -= h); 44 | var processError = CreateProcessExitSubscription(processExited, observer); 45 | 46 | if (!StartProcess(observer)) 47 | return new CompositeDisposable(processError); 48 | 49 | Process.BeginOutputReadLine(); 50 | Process.BeginErrorReadLine(); 51 | 52 | Started = true; 53 | return new CompositeDisposable(stdOutSubscription, stdErrSubscription, processError); 54 | } 55 | 56 | private IDisposable CreateProcessExitSubscription(IObservable> processExited, IObserver observer) => 57 | processExited.Subscribe(args => { OnExit(observer); }, e => OnError(observer, e), ()=> OnCompleted(observer)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Proc/ExecArguments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ProcNet 5 | { 6 | public class ExecArguments : ProcessArgumentsBase 7 | { 8 | private Func _validExitCodeClassifier; 9 | public ExecArguments(string binary, IEnumerable args) : base(binary, args) { } 10 | 11 | public ExecArguments(string binary, params string[] args) : base(binary, args) { } 12 | 13 | public Func ValidExitCodeClassifier 14 | { 15 | get => _validExitCodeClassifier ?? (c => c == 0); 16 | set => _validExitCodeClassifier = value; 17 | } 18 | 19 | public TimeSpan? Timeout { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Proc/Extensions/ArgumentExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace ProcNet.Extensions; 5 | 6 | internal static class ArgumentExtensions 7 | { 8 | public static string NaivelyQuoteArguments(this IEnumerable arguments) 9 | { 10 | if (arguments == null) return string.Empty; 11 | var args = arguments.ToList(); 12 | if (args.Count == 0) return string.Empty; 13 | 14 | var quotedArgs = args 15 | .Select(a => 16 | { 17 | if (!a.Contains(" ")) return a; 18 | if (a.StartsWith("\"")) return a; 19 | return $"\"{a}\""; 20 | }) 21 | .ToList(); 22 | return string.Join(" ", quotedArgs); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Proc/Extensions/CancellableStreamReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace ProcNet.Extensions 8 | { 9 | /// 10 | /// A streamreader implementation that supports CancellationToken inside it's ReadAsync method allowing it to not block indefintely. 11 | /// 12 | internal class CancellableStreamReader : StreamReader 13 | { 14 | public CancellableStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize, bool leaveOpen, CancellationToken token) 15 | : base(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, leaveOpen) 16 | { 17 | _stream = stream; 18 | _encoding = encoding; 19 | _decoder = encoding.GetDecoder(); 20 | if (bufferSize < MinBufferSize) bufferSize = MinBufferSize; 21 | _byteBuffer = new byte[bufferSize]; 22 | _maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize); 23 | _charBuffer = new char[_maxCharsPerBuffer]; 24 | _byteLen = 0; 25 | _bytePos = 0; 26 | _detectEncoding = detectEncodingFromByteOrderMarks; 27 | _token = token; 28 | _preamble = encoding.GetPreamble(); 29 | _checkPreamble = (_preamble.Length > 0); 30 | _isBlocked = false; 31 | _closable = !leaveOpen; 32 | } 33 | private const int MinBufferSize = 128; 34 | 35 | private Stream _stream; 36 | private Encoding _encoding; 37 | private Decoder _decoder; 38 | private byte[] _byteBuffer; 39 | private char[] _charBuffer; 40 | private byte[] _preamble; // Encoding's preamble, which identifies this encoding. 41 | private int _charPos; 42 | private int _charLen; 43 | // Record the number of valid bytes in the byteBuffer, for a few checks. 44 | private int _byteLen; 45 | // This is used only for preamble detection 46 | private int _bytePos; 47 | 48 | // This is the maximum number of chars we can get from one call to 49 | // ReadBuffer. Used so ReadBuffer can tell when to copy data into 50 | // a user's char[] directly, instead of our internal char[]. 51 | private int _maxCharsPerBuffer; 52 | 53 | // We will support looking for byte order marks in the stream and trying 54 | // to decide what the encoding might be from the byte order marks, IF they 55 | // exist. But that's all we'll do. 56 | private bool _detectEncoding; 57 | private readonly CancellationToken _token; 58 | 59 | // Whether we must still check for the encoding's given preamble at the 60 | // beginning of this file. 61 | private bool _checkPreamble; 62 | 63 | // Whether the stream is most likely not going to give us back as much 64 | // data as we want the next time we call it. We must do the computation 65 | // before we do any byte order mark handling and save the result. Note 66 | // that we need this to allow users to handle streams used for an 67 | // interactive protocol, where they block waiting for the remote end 68 | // to send a response, like logging in on a Unix machine. 69 | private bool _isBlocked; 70 | 71 | // The intent of this field is to leave open the underlying stream when 72 | // disposing of this StreamReader. A name like _leaveOpen is better, 73 | // but this type is serializable, and this field's name was _closable. 74 | private bool _closable; // Whether to close the underlying stream. 75 | 76 | public override Task ReadAsync(char[] buffer, int index, int count) 77 | { 78 | if (buffer==null) throw new ArgumentNullException(nameof(buffer)); 79 | if (index < 0 || count < 0) throw new ArgumentOutOfRangeException((index < 0 ? "index" : "count")); 80 | if (buffer.Length - index < count) throw new ArgumentException(nameof(buffer)); 81 | 82 | return ReadAsyncInternal(buffer, index, count); 83 | } 84 | 85 | public async Task EndOfStreamAsync() 86 | { 87 | if (_stream == null) 88 | throw new ArgumentNullException(nameof(_stream)); 89 | 90 | if (_charPos < _charLen) return false; 91 | 92 | // This may block on pipes! 93 | var numRead = await ReadBufferAsync(); 94 | return numRead == 0; 95 | } 96 | 97 | private async Task ReadBufferAsync() 98 | { 99 | _charLen = 0; 100 | _charPos = 0; 101 | var tmpByteBuffer = _byteBuffer; 102 | var tmpStream = _stream; 103 | 104 | if (!_checkPreamble) _byteLen = 0; 105 | do 106 | { 107 | if (_checkPreamble) 108 | { 109 | var tmpBytePos = _bytePos; 110 | var len = await tmpStream.ReadAsync(tmpByteBuffer, tmpBytePos, tmpByteBuffer.Length - tmpBytePos, _token).ConfigureAwait(false); 111 | 112 | if (len == 0) 113 | { 114 | // EOF but we might have buffered bytes from previous 115 | // attempt to detect preamble that needs to be decoded now 116 | if (_byteLen <= 0) return _charLen; 117 | _charLen += _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, _charLen); 118 | // Need to zero out the _byteLen after we consume these bytes so that we don't keep infinitely hitting this code path 119 | _bytePos = 0; 120 | _byteLen = 0; 121 | 122 | return _charLen; 123 | } 124 | 125 | _byteLen += len; 126 | } 127 | else 128 | { 129 | _byteLen = await tmpStream.ReadAsync(tmpByteBuffer, 0, tmpByteBuffer.Length, _token).ConfigureAwait(false); 130 | 131 | if (_byteLen == 0) // We're at EOF 132 | return _charLen; 133 | } 134 | 135 | // _isBlocked == whether we read fewer bytes than we asked for. 136 | // Note we must check it here because CompressBuffer or 137 | // DetectEncoding will change _byteLen. 138 | _isBlocked = (_byteLen < tmpByteBuffer.Length); 139 | 140 | // Check for preamble before detect encoding. This is not to override the 141 | // user suppplied Encoding for the one we implicitly detect. The user could 142 | // customize the encoding which we will loose, such as ThrowOnError on UTF8 143 | if (IsPreamble()) 144 | continue; 145 | 146 | // If we're supposed to detect the encoding and haven't done so yet, 147 | // do it. Note this may need to be called more than once. 148 | if (_detectEncoding && _byteLen >= 2) 149 | DetectEncoding(); 150 | 151 | _charLen += _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, _charLen); 152 | } while (_charLen == 0); 153 | 154 | return _charLen; 155 | } 156 | 157 | 158 | internal async Task ReadAsyncInternal(char[] buffer, int index, int count) 159 | { 160 | if (_charPos == _charLen && (await ReadBufferAsync().ConfigureAwait(false)) == 0) 161 | return 0; 162 | 163 | var charsRead = 0; 164 | 165 | // As a perf optimization, if we had exactly one buffer's worth of 166 | // data read in, let's try writing directly to the user's buffer. 167 | var readToUserBuffer = false; 168 | 169 | var tmpByteBuffer = _byteBuffer; 170 | var tmpStream = _stream; 171 | 172 | while (count > 0) 173 | { 174 | // n is the cha----ters avaialbe in _charBuffer 175 | var n = _charLen - _charPos; 176 | 177 | // charBuffer is empty, let's read from the stream 178 | if (n == 0) 179 | { 180 | _charLen = 0; 181 | _charPos = 0; 182 | 183 | if (!_checkPreamble) 184 | _byteLen = 0; 185 | 186 | readToUserBuffer = count >= _maxCharsPerBuffer; 187 | 188 | // We loop here so that we read in enough bytes to yield at least 1 char. 189 | // We break out of the loop if the stream is blocked (EOF is reached). 190 | do 191 | { 192 | if (_checkPreamble) 193 | { 194 | var tmpBytePos = _bytePos; 195 | var len = await tmpStream.ReadAsync(tmpByteBuffer, tmpBytePos, tmpByteBuffer.Length - tmpBytePos, _token).ConfigureAwait(false); 196 | 197 | if (len == 0) 198 | { 199 | // EOF but we might have buffered bytes from previous 200 | // attempts to detect preamble that needs to be decoded now 201 | if (_byteLen > 0) 202 | { 203 | if (readToUserBuffer) 204 | { 205 | n = _decoder.GetChars(tmpByteBuffer, 0, _byteLen, buffer, index + charsRead); 206 | _charLen = 0; // StreamReader's buffer is empty. 207 | } 208 | else 209 | { 210 | n = _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, 0); 211 | _charLen += n; // Number of chars in StreamReader's buffer. 212 | } 213 | } 214 | 215 | _isBlocked = true; 216 | break; 217 | } 218 | else 219 | { 220 | _byteLen += len; 221 | } 222 | } 223 | else 224 | { 225 | _byteLen = await tmpStream.ReadAsync(tmpByteBuffer, 0, tmpByteBuffer.Length, _token).ConfigureAwait(false); 226 | 227 | if (_byteLen == 0) // EOF 228 | { 229 | _isBlocked = true; 230 | break; 231 | } 232 | } 233 | 234 | // _isBlocked == whether we read fewer bytes than we asked for. 235 | // Note we must check it here because CompressBuffer or 236 | // DetectEncoding will change _byteLen. 237 | _isBlocked = (_byteLen < tmpByteBuffer.Length); 238 | 239 | // Check for preamble before detect encoding. This is not to override the 240 | // user suppplied Encoding for the one we implicitly detect. The user could 241 | // customize the encoding which we will loose, such as ThrowOnError on UTF8 242 | // Note: we don't need to recompute readToUserBuffer optimization as IsPreamble 243 | // doesn't change the encoding or affect _maxCharsPerBuffer 244 | if (IsPreamble()) 245 | continue; 246 | 247 | // On the first call to ReadBuffer, if we're supposed to detect the encoding, do it. 248 | if (_detectEncoding && _byteLen >= 2) 249 | { 250 | DetectEncoding(); 251 | // DetectEncoding changes some buffer state. Recompute this. 252 | readToUserBuffer = count >= _maxCharsPerBuffer; 253 | } 254 | 255 | 256 | _charPos = 0; 257 | if (readToUserBuffer) 258 | { 259 | n += _decoder.GetChars(tmpByteBuffer, 0, _byteLen, buffer, index + charsRead); 260 | 261 | _charLen = 0; // StreamReader's buffer is empty. 262 | } 263 | else 264 | { 265 | n = _decoder.GetChars(tmpByteBuffer, 0, _byteLen, _charBuffer, 0); 266 | 267 | 268 | _charLen += n; // Number of chars in StreamReader's buffer. 269 | } 270 | } while (n == 0); 271 | 272 | if (n == 0) break; // We're at EOF 273 | } // if (n == 0) 274 | 275 | // Got more chars in charBuffer than the user requested 276 | if (n > count) 277 | n = count; 278 | 279 | if (!readToUserBuffer) 280 | { 281 | Buffer.BlockCopy(_charBuffer, _charPos * 2, buffer, (index + charsRead) * 2, n * 2); 282 | _charPos += n; 283 | } 284 | 285 | charsRead += n; 286 | count -= n; 287 | 288 | // This function shouldn't block for an indefinite amount of time, 289 | // or reading from a network stream won't work right. If we got 290 | // fewer bytes than we requested, then we want to break right here. 291 | if (_isBlocked) 292 | break; 293 | } // while (count > 0) 294 | 295 | return charsRead; 296 | } 297 | private void CompressBuffer(int n) 298 | { 299 | Buffer.BlockCopy(_byteBuffer, n, _byteBuffer, 0, _byteLen - n); 300 | _byteLen -= n; 301 | } 302 | 303 | private void DetectEncoding() 304 | { 305 | if (_byteLen < 2) return; 306 | _detectEncoding = false; 307 | var changedEncoding = false; 308 | if (_byteBuffer[0] == 0xFE && _byteBuffer[1] == 0xFF) 309 | { 310 | // Big Endian Unicode 311 | _encoding = new UnicodeEncoding(true, true); 312 | CompressBuffer(2); 313 | changedEncoding = true; 314 | } 315 | else if (_byteBuffer[0] == 0xFF && _byteBuffer[1] == 0xFE) 316 | { 317 | // Little Endian Unicode, or possibly little endian UTF32 318 | if (_byteLen < 4 || _byteBuffer[2] != 0 || _byteBuffer[3] != 0) 319 | { 320 | _encoding = new UnicodeEncoding(false, true); 321 | CompressBuffer(2); 322 | changedEncoding = true; 323 | } 324 | else 325 | { 326 | _encoding = new UTF32Encoding(false, true); 327 | CompressBuffer(4); 328 | changedEncoding = true; 329 | } 330 | } 331 | 332 | else if (_byteLen >= 3 && _byteBuffer[0] == 0xEF && _byteBuffer[1] == 0xBB && _byteBuffer[2] == 0xBF) 333 | { 334 | // UTF-8 335 | _encoding = Encoding.UTF8; 336 | CompressBuffer(3); 337 | changedEncoding = true; 338 | } 339 | else if (_byteLen >= 4 && _byteBuffer[0] == 0 && _byteBuffer[1] == 0 && _byteBuffer[2] == 0xFE && _byteBuffer[3] == 0xFF) 340 | { 341 | // Big Endian UTF32 342 | _encoding = new UTF32Encoding(true, true); 343 | CompressBuffer(4); 344 | changedEncoding = true; 345 | } 346 | else if (_byteLen == 2) 347 | _detectEncoding = true; 348 | // Note: in the future, if we change this algorithm significantly, 349 | // we can support checking for the preamble of the given encoding. 350 | 351 | if (!changedEncoding) return; 352 | _decoder = _encoding.GetDecoder(); 353 | _maxCharsPerBuffer = _encoding.GetMaxCharCount(_byteBuffer.Length); 354 | _charBuffer = new char[_maxCharsPerBuffer]; 355 | } 356 | 357 | // Trims the preamble bytes from the byteBuffer. This routine can be called multiple times 358 | // and we will buffer the bytes read until the preamble is matched or we determine that 359 | // there is no match. If there is no match, every byte read previously will be available 360 | // for further consumption. If there is a match, we will compress the buffer for the 361 | // leading preamble bytes 362 | private bool IsPreamble() 363 | { 364 | if (!_checkPreamble) 365 | return _checkPreamble; 366 | 367 | var len = (_byteLen >= (_preamble.Length)) ? (_preamble.Length - _bytePos) : (_byteLen - _bytePos); 368 | 369 | for (var i = 0; i < len; i++, _bytePos++) 370 | { 371 | if (_byteBuffer[_bytePos] == _preamble[_bytePos]) continue; 372 | _bytePos = 0; 373 | _checkPreamble = false; 374 | break; 375 | } 376 | 377 | if (!_checkPreamble) return _checkPreamble; 378 | if (_bytePos != _preamble.Length) return _checkPreamble; 379 | // We have a match 380 | CompressBuffer(_preamble.Length); 381 | _bytePos = 0; 382 | _checkPreamble = false; 383 | _detectEncoding = false; 384 | 385 | return _checkPreamble; 386 | } 387 | 388 | protected override void Dispose(bool disposing) 389 | { 390 | try { 391 | if (_closable && disposing) _stream?.Dispose(); 392 | } 393 | finally { 394 | if (_closable && (_stream != null)) { 395 | _stream = null; 396 | _encoding = null; 397 | _decoder = null; 398 | _byteBuffer = null; 399 | _charBuffer = null; 400 | _charPos = 0; 401 | _charLen = 0; 402 | base.Dispose(disposing); 403 | } 404 | } 405 | } 406 | 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/Proc/Extensions/ObserveOutputExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using ProcNet.Std; 10 | 11 | namespace ProcNet.Extensions 12 | { 13 | internal static class ObserveOutputExtensions 14 | { 15 | public static IObservable ObserveErrorOutLineByLine(this Process process) 16 | { 17 | var receivedStdErr = 18 | Observable.FromEventPattern 19 | (h => process.ErrorDataReceived += h, h => process.ErrorDataReceived -= h) 20 | .Where(e => e.EventArgs.Data != null) 21 | .Select(e => ConsoleOut.ErrorOut(e.EventArgs.Data)); 22 | 23 | return Observable.Create(observer => 24 | { 25 | var cancel = Disposable.Create(()=>{ try { process.CancelErrorRead(); } catch(InvalidOperationException) { } }); 26 | return new CompositeDisposable(cancel, receivedStdErr.Subscribe(observer)); 27 | }); 28 | } 29 | 30 | public static IObservable ObserveStandardOutLineByLine(this Process process) 31 | { 32 | var receivedStdOut = 33 | Observable.FromEventPattern 34 | (h => process.OutputDataReceived += h, h => process.OutputDataReceived -= h) 35 | .Where(e => e.EventArgs.Data != null) 36 | .Select(e => ConsoleOut.Out(e.EventArgs.Data)); 37 | 38 | return Observable.Create(observer => 39 | { 40 | var cancel = Disposable.Create(()=>{ try { process.CancelOutputRead(); } catch(InvalidOperationException) { } }); 41 | return new CompositeDisposable(cancel, receivedStdOut.Subscribe(observer)); 42 | }); 43 | } 44 | 45 | public static Task ObserveErrorOutBuffered(this Process process, IObserver observer, int bufferSize, Func keepBuffering, CancellationToken token) => 46 | BufferedRead(process, process.StandardError, observer, bufferSize, ConsoleOut.ErrorOut, keepBuffering, token); 47 | 48 | public static Task ObserveStandardOutBuffered(this Process process, IObserver observer, int bufferSize, Func keepBuffering, CancellationToken token) => 49 | BufferedRead(process, process.StandardOutput, observer, bufferSize, ConsoleOut.Out, keepBuffering, token); 50 | 51 | private static async Task BufferedRead(Process p, StreamReader r, IObserver o, int b, Func m, Func keepBuffering, CancellationToken token) 52 | { 53 | using (var sr = new CancellableStreamReader(r.BaseStream, Encoding.UTF8, true, b, true, token)) 54 | while (keepBuffering()) 55 | { 56 | var buffer = new char[b]; 57 | var read = await sr.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(true); 58 | if (token.IsCancellationRequested) break; 59 | 60 | token.ThrowIfCancellationRequested(); 61 | if (read > 0) 62 | o.OnNext(m(buffer)); 63 | else 64 | { 65 | if (await sr.EndOfStreamAsync()) break; 66 | await Task.Delay(1, token).ConfigureAwait(false); 67 | } 68 | } 69 | 70 | token.ThrowIfCancellationRequested(); 71 | } 72 | 73 | public static void ReadStandardErrBlocking(this Process process, IObserver observer, int bufferSize, Func keepBuffering) => 74 | BufferedReadBlocking(process, process.StandardError, observer, bufferSize, ConsoleOut.ErrorOut, keepBuffering); 75 | 76 | public static void ReadStandardOutBlocking(this Process process, IObserver observer, int bufferSize, Func keepBuffering) => 77 | BufferedReadBlocking(process, process.StandardOutput, observer, bufferSize, ConsoleOut.Out, keepBuffering); 78 | 79 | private static void BufferedReadBlocking(this Process p, StreamReader r, IObserver o, int b, Func m, Func keepBuffering) 80 | { 81 | using var sr = new StreamReader(r.BaseStream, Encoding.UTF8, true, b, true); 82 | while (keepBuffering()) 83 | { 84 | var buffer = new char[b]; 85 | var read = sr.Read(buffer, 0, buffer.Length); 86 | 87 | if (read > 0) 88 | o.OnNext(m(buffer)); 89 | else 90 | if (sr.EndOfStream) break; 91 | } 92 | } 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Proc/IObservableProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ProcNet.Std; 3 | 4 | namespace ProcNet 5 | { 6 | public interface ISubscribeLines 7 | { 8 | IDisposable Subscribe(IObserver observer); 9 | } 10 | 11 | 12 | public interface IObservableProcess : IDisposable, IObservable 13 | where TConsoleOut : ConsoleOut 14 | { 15 | /// 16 | /// is a handy abstraction if all you want to do is redirect output to the console. 17 | /// This library ships with and . 18 | /// 19 | IDisposable Subscribe(IConsoleOutWriter writer); 20 | 21 | /// 22 | /// When the process exits this will indicate its status code. 23 | /// 24 | int? ExitCode { get; } 25 | 26 | /// 27 | /// The process id of the started process 28 | /// 29 | int? ProcessId { get; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Proc/LongRunningArguments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ProcNet.Std; 4 | 5 | namespace ProcNet; 6 | 7 | public class LongRunningArguments : StartArguments 8 | { 9 | public LongRunningArguments(string binary, IEnumerable args) : base(binary, args) { } 10 | 11 | public LongRunningArguments(string binary, params string[] args) : base(binary, args) { } 12 | 13 | /// 14 | /// A handler that will delay return of the process until startup is confirmed over 15 | /// standard out/error. 16 | /// 17 | public Func StartedConfirmationHandler { get; set; } 18 | 19 | /// 20 | /// A helper that sets and stops immediately after 21 | /// indicates the process has started. 22 | /// 23 | public bool StopBufferingAfterStarted { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/Proc/ObservableProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reactive; 7 | using System.Reactive.Disposables; 8 | using System.Reactive.Linq; 9 | using ProcNet.Std; 10 | 11 | namespace ProcNet 12 | { 13 | #pragma warning disable 1574 14 | /// 15 | /// Wraps around and turns and 16 | /// into an observable sequence using 17 | /// 18 | /// This reads bytes at a time and returns those as 19 | /// 20 | /// You can also quite easily subscribe to whole lines instead using which buffers the characters arrays 21 | /// and calls OnNext for every line exposing them as . 22 | /// 23 | /// If all you want to do is redirect output to console consider subscribing to taking an 24 | /// instead. 25 | /// 26 | /// When the process exits it waits for these stream readers to finish up to whatever 27 | /// is configured to. This defaults to 5 seconds. 28 | /// 29 | /// This catches all cases where would fall short to capture all the process output. 30 | /// 31 | /// 32 | #pragma warning restore 1574 33 | public class ObservableProcess : BufferedObservableProcess, ISubscribeLines 34 | { 35 | private char[] _bufferStdOut = { }; 36 | private char[] _bufferStdOutRemainder = { }; 37 | private char[] _bufferStdErr = { }; 38 | private char[] _bufferStdErrRemainder = { }; 39 | private readonly object _copyLock = new object(); 40 | 41 | public ObservableProcess(string binary, params string[] arguments) : base(binary, arguments) { } 42 | 43 | public ObservableProcess(StartArguments startArguments) : base(startArguments) { } 44 | 45 | protected override IObservable CreateConsoleOutObservable() => 46 | Observable.Create(observer => 47 | { 48 | base.CreateConsoleOutObservable() 49 | .Subscribe(c => OnNextConsoleOut(c, observer), observer.OnError, observer.OnCompleted); 50 | return Disposable.Empty; 51 | }); 52 | 53 | public override IDisposable Subscribe(IObserver observer) => OutStream.Subscribe(observer); 54 | 55 | private static readonly char[] NewlineChars = Environment.NewLine.ToCharArray(); 56 | 57 | /// 58 | /// Subclasses can implement this and return true to stop buffering lines. 59 | /// This is great for long running processes to only buffer console output until 60 | /// all information is parsed. 61 | /// 62 | /// True to end the buffering of char[] to lines of text 63 | protected virtual bool KeepBufferingLines(LineOut l) => true; 64 | 65 | /// 66 | /// Create an buffer boundary by returning true. Useful if you want to force a line to be returned. 67 | /// 68 | protected virtual bool BufferBoundary(char[] stdOut, char[] stdErr) 69 | { 70 | if (!StopRequested) return false; 71 | var s = new string(stdOut); 72 | //bat files prompt to confirm which blocks the output, we auto confirm here 73 | if (ProcessName == "cmd" && s.EndsWith(" (Y/N)? ")) 74 | { 75 | SendYesForBatPrompt(); 76 | return true; 77 | } 78 | return false; 79 | } 80 | 81 | public IDisposable Subscribe(IObserver observerLines) => Subscribe(observerLines, null); 82 | public IDisposable Subscribe(IObserver observerLines, IObserver observerCharacters) 83 | { 84 | var published = OutStream.Publish(); 85 | var observeLinesFilter = StartArguments.LineOutFilter ?? (l => true); 86 | 87 | if (observerCharacters != null) published.Subscribe(observerCharacters); 88 | 89 | var boundaries = published 90 | .Where(o => o.EndsWithNewLine || o.StartsWithCarriage || BufferBoundary(_bufferStdOutRemainder, _bufferStdErrRemainder)); 91 | var buffered = published.Buffer(boundaries); 92 | var newlines = buffered 93 | .SelectMany(c => 94 | { 95 | if (c == null || c.Count == 0) 96 | return Array.Empty(); 97 | if (c.Count == 1) 98 | { 99 | var cOut = c.First(); 100 | var chars = new string(cOut.Characters).TrimEnd(NewlineChars); 101 | return new[] { new LineOut(cOut.Error, chars) }; 102 | } 103 | 104 | return c 105 | .GroupBy(l => l.Error) 106 | .Select(g => (g.Key, new string(g.SelectMany(o => o.Characters).ToArray()))) 107 | .SelectMany(kv => 108 | kv.Item2.TrimEnd(NewlineChars).Split(NewlineChars) 109 | .Select(line => new LineOut(kv.Key, line.TrimEnd(NewlineChars))) 110 | ) 111 | .ToArray(); 112 | }) 113 | .TakeWhile(l => 114 | { 115 | var keepBuffering = StartArguments.KeepBufferingLines ?? KeepBufferingLines; 116 | var keep = keepBuffering?.Invoke(l); 117 | return keep.GetValueOrDefault(true); 118 | }) 119 | .Where(l => l != null) 120 | .Where(observeLinesFilter) 121 | .Subscribe( 122 | observerLines.OnNext, 123 | e => 124 | { 125 | observerLines.OnError(e); 126 | SetCompletedHandle(); 127 | }, 128 | () => 129 | { 130 | observerLines.OnCompleted(); 131 | SetCompletedHandle(); 132 | }); 133 | var connected = published.Connect(); 134 | return new CompositeDisposable(newlines, connected); 135 | } 136 | 137 | public virtual IDisposable SubscribeLines(Action onNext, Action onError, Action onCompleted) => 138 | Subscribe(Observer.Create(onNext, onError, onCompleted)); 139 | 140 | public virtual IDisposable SubscribeLines(Action onNext, Action onError) => 141 | Subscribe(Observer.Create(onNext, onError, delegate { })); 142 | 143 | public virtual IDisposable SubscribeLinesAndCharacters( 144 | Action onNext, 145 | Action onError, 146 | Action onNextCharacters, 147 | Action onExceptionCharacters, 148 | Action onCompleted = null 149 | ) => 150 | Subscribe( 151 | Observer.Create(onNext, onError, onCompleted ?? delegate { }), 152 | Observer.Create(onNextCharacters, onExceptionCharacters, onCompleted ?? delegate { }) 153 | ); 154 | 155 | public virtual IDisposable SubscribeLines(Action onNext) => 156 | Subscribe(Observer.Create(onNext, delegate { }, delegate { })); 157 | 158 | private void OnNextConsoleOut(CharactersOut c, IObserver observer) 159 | { 160 | lock (_copyLock) 161 | { 162 | if (Flushing) OnNextFlush(c, observer); 163 | else OnNextSource(c, observer); 164 | } 165 | } 166 | 167 | private void OnNextSource(ConsoleOut c, IObserver observer) 168 | { 169 | c.OutOrErrrorCharacters(OutCharacters, ErrorCharacters); 170 | if (c.Error) 171 | { 172 | YieldNewLinesToOnNext(ref _bufferStdErr, b => OnNext(b, observer, ConsoleOut.ErrorOut)); 173 | FlushRemainder(ref _bufferStdErr, ref _bufferStdErrRemainder, b => OnNext(b, observer, ConsoleOut.ErrorOut)); 174 | } 175 | else 176 | { 177 | YieldNewLinesToOnNext(ref _bufferStdOut, b => OnNext(b, observer, ConsoleOut.Out)); 178 | FlushRemainder(ref _bufferStdOut, ref _bufferStdOutRemainder, b => OnNext(b, observer, ConsoleOut.Out)); 179 | } 180 | } 181 | 182 | private static void OnNext(char[] b, IObserver o, Func c) => o.OnNext(c(b)); 183 | 184 | private static void OnNextFlush(CharactersOut c, IObserver observer) => 185 | observer.OnNext(c.Error ? ConsoleOut.ErrorOut(c.Characters) : ConsoleOut.Out(c.Characters)); 186 | 187 | protected override void OnCompleted(IObserver observer) 188 | { 189 | Flush(observer); //make sure we flush our buffers before calling OnCompleted 190 | base.OnCompleted(observer); 191 | } 192 | 193 | protected override void OnError(IObserver observer, Exception e) 194 | { 195 | Flush(observer); //make sure we flush our buffers before erroring 196 | base.OnError(observer, e); 197 | } 198 | 199 | private void Flush(IObserver observer) 200 | { 201 | YieldNewLinesToOnNext(ref _bufferStdErr, b => OnNext(b, observer, ConsoleOut.ErrorOut)); 202 | FlushRemainder(ref _bufferStdErr, ref _bufferStdErrRemainder, b => OnNext(b, observer, ConsoleOut.ErrorOut)); 203 | 204 | YieldNewLinesToOnNext(ref _bufferStdOut, b => OnNext(b, observer, ConsoleOut.Out)); 205 | FlushRemainder(ref _bufferStdOut, ref _bufferStdOutRemainder, b => OnNext(b, observer, ConsoleOut.Out)); 206 | } 207 | 208 | private bool Flushing { get; set; } 209 | private void FlushRemainder(ref char[] buffer, ref char[] remainder, Action onNext) 210 | { 211 | if (buffer.Length <= 0) return; 212 | var endOffSet = FindEndOffSet(buffer, 0); 213 | if (endOffSet <= 0) return; 214 | var ret = new char[endOffSet]; 215 | Array.Copy(buffer, 0, ret, 0, endOffSet); 216 | buffer = new char[] {}; 217 | remainder = ret; 218 | Flushing = true; 219 | onNext(ret); 220 | Flushing = false; 221 | } 222 | 223 | private static void YieldNewLinesToOnNext(ref char[] buffer, Action onNext) 224 | { 225 | var newLineOffset = ReadLinesFromBuffer(buffer, onNext); 226 | var endOfArrayOffset = FindEndOffSet(buffer, newLineOffset); 227 | CopyRemainderToGlobalBuffer(ref buffer, endOfArrayOffset, newLineOffset); 228 | } 229 | 230 | /// 231 | /// Copies the remainder of the local buffer to the global buffer to consider in the next observable push 232 | /// 233 | private static void CopyRemainderToGlobalBuffer(ref char[] buffer, int endOfArrayOffset, int newLineOffset) 234 | { 235 | var remainder = endOfArrayOffset - newLineOffset; 236 | if (remainder < 1) 237 | { 238 | buffer = new char[0]; 239 | return; 240 | } 241 | var newBuffer = new char[remainder]; 242 | Array.Copy(buffer, newLineOffset, newBuffer, 0, remainder); 243 | //prevent an array of a single '\0'. 244 | if (newBuffer.Length == 1 && newBuffer[0] == '\0') 245 | { 246 | buffer = new char[0]; 247 | return; 248 | } 249 | buffer = newBuffer; 250 | } 251 | 252 | /// 253 | /// Finds the offset of the first null byte character or the 's 254 | /// 255 | /// Offset of the first null byte character or the 's 256 | private static int FindEndOffSet(char[] buffer, int from) 257 | { 258 | var zeroByteOffset = buffer.Length; 259 | for (var i = from; i < buffer.Length; i++) 260 | { 261 | var ch = buffer[i]; 262 | if (ch != '\0') continue; 263 | zeroByteOffset = i; 264 | break; 265 | } 266 | return zeroByteOffset; 267 | } 268 | 269 | /// 270 | /// Reads all the new lines inside 271 | /// 272 | /// the last new line character offset 273 | private static int ReadLinesFromBuffer(char[] buffer, Action onNext) 274 | { 275 | var newLineOffset = 0; 276 | for (var i = 0; i < buffer.Length; i++) 277 | { 278 | var ch = buffer[i]; 279 | if (ch != '\n') continue; 280 | 281 | var count = i - newLineOffset + 1; 282 | var ret = new char[count]; 283 | Array.Copy(buffer, newLineOffset, ret, 0, count); 284 | onNext(ret); 285 | 286 | newLineOffset = i + 1; 287 | } 288 | return newLineOffset; 289 | } 290 | 291 | private void OutCharacters(char[] data) => Combine(ref _bufferStdOut, data); 292 | private void ErrorCharacters(char[] data) => Combine(ref _bufferStdErr, data); 293 | 294 | private static void Combine(ref char[] first, char[] second) 295 | { 296 | var ret = new char[first.Length + second.Length]; 297 | Array.Copy(first, 0, ret, 0, first.Length); 298 | Array.Copy(second, 0, ret, first.Length, second.Length); 299 | first = ret; 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Proc/ObservableProcessBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Reactive.Linq; 7 | using System.Reflection; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using ProcNet.Extensions; 11 | using ProcNet.Std; 12 | 13 | namespace ProcNet 14 | { 15 | public delegate void StandardInputHandler(StreamWriter standardInput); 16 | 17 | public abstract class ObservableProcessBase : IObservableProcess 18 | where TConsoleOut : ConsoleOut 19 | { 20 | protected ObservableProcessBase(string binary, params string[] arguments) 21 | : this(new StartArguments(binary, arguments)) { } 22 | 23 | protected ObservableProcessBase(StartArguments startArguments) 24 | { 25 | StartArguments = startArguments ?? throw new ArgumentNullException(nameof(startArguments)); 26 | Process = CreateProcess(); 27 | if (startArguments.StandardInputHandler != null) 28 | StandardInputReady += startArguments.StandardInputHandler; 29 | CreateObservable(); 30 | } 31 | 32 | public virtual IDisposable Subscribe(IObserver observer) => OutStream.Subscribe(observer); 33 | 34 | public IDisposable Subscribe(IConsoleOutWriter writer) => OutStream.Subscribe(writer.Write, writer.Write, delegate { }); 35 | 36 | private readonly ManualResetEvent _completedHandle = new(false); 37 | 38 | public StreamWriter StandardInput => Process.StandardInput; 39 | public string Binary => StartArguments.Binary; 40 | public int? ExitCode { get; private set; } 41 | 42 | protected StartArguments StartArguments { get; } 43 | protected Process Process { get; } 44 | protected bool Started { get; set; } 45 | protected string ProcessName { get; private set; } 46 | 47 | protected bool NoWrapInThread => StartArguments.NoWrapInThread; 48 | private int? _processId; 49 | public virtual int? ProcessId => _processId; 50 | 51 | protected IObservable OutStream { get; private set; } = Observable.Empty(); 52 | 53 | private void CreateObservable() 54 | { 55 | if (Started) return; 56 | _completedHandle.Reset(); 57 | OutStream = CreateConsoleOutObservable(); 58 | } 59 | 60 | protected abstract IObservable CreateConsoleOutObservable(); 61 | 62 | public event StandardInputHandler StandardInputReady = (s) => { }; 63 | 64 | protected bool StartProcess(IObserver observer) 65 | { 66 | var started = false; 67 | try 68 | { 69 | started = Process.Start(); 70 | if (started) 71 | { 72 | try 73 | { 74 | _processId = Process.Id; 75 | ProcessName = Process.ProcessName; 76 | } 77 | catch (InvalidOperationException) 78 | { 79 | // best effort, Process could have finished before even attempting to read .Id and .ProcessName 80 | // which can throw if the process exits in between 81 | } 82 | StandardInputReady(Process.StandardInput); 83 | return true; 84 | } 85 | 86 | OnError(observer, new ObservableProcessException($"Failed to start observable process: {Binary}")); 87 | return false; 88 | } 89 | catch (Exception e) 90 | { 91 | OnError(observer, new ObservableProcessException($"Exception while starting observable process: {Binary}", e.Message, e)); 92 | } 93 | finally 94 | { 95 | if (!started) SetCompletedHandle(); 96 | } 97 | 98 | return false; 99 | } 100 | 101 | protected virtual void OnError(IObserver observer, Exception e) 102 | { 103 | HardKill(); 104 | observer.OnError(e); 105 | } 106 | 107 | protected virtual void OnCompleted(IObserver observer) => observer.OnCompleted(); 108 | 109 | private readonly object _exitLock = new object(); 110 | 111 | protected void OnExit(IObserver observer) 112 | { 113 | if (!Started) return; 114 | int? exitCode = null; 115 | try 116 | { 117 | exitCode = Process.ExitCode; 118 | } 119 | //ExitCode and HasExited are all trigger happy. We are aware the process may or may not have an exit code. 120 | catch (InvalidOperationException) { } 121 | finally 122 | { 123 | ExitStop(observer, exitCode); 124 | } 125 | } 126 | 127 | private void ExitStop(IObserver observer, int? exitCode) 128 | { 129 | if (!Started) return; 130 | if (_isDisposing) return; 131 | lock (_exitLock) 132 | { 133 | if (!Started) return; 134 | 135 | Stop(exitCode, observer); 136 | } 137 | } 138 | 139 | private Process CreateProcess() 140 | { 141 | var s = StartArguments; 142 | var args = s.Args; 143 | var processStartInfo = new ProcessStartInfo 144 | { 145 | FileName = s.Binary, 146 | #if STRING_ARGS 147 | Arguments = args != null ? string.Join(" ", args) : string.Empty, 148 | #endif 149 | CreateNoWindow = true, 150 | UseShellExecute = false, 151 | RedirectStandardOutput = true, 152 | RedirectStandardError = true, 153 | RedirectStandardInput = true 154 | }; 155 | #if !STRING_ARGS 156 | foreach (var arg in s.Args) 157 | processStartInfo.ArgumentList.Add(arg); 158 | #endif 159 | if (s.Environment != null) 160 | { 161 | foreach (var kv in s.Environment) 162 | { 163 | processStartInfo.Environment[kv.Key] = kv.Value; 164 | } 165 | } 166 | 167 | if (!string.IsNullOrWhiteSpace(s.WorkingDirectory)) processStartInfo.WorkingDirectory = s.WorkingDirectory; 168 | 169 | var p = new Process 170 | { 171 | EnableRaisingEvents = true, 172 | StartInfo = processStartInfo 173 | }; 174 | return p; 175 | } 176 | 177 | /// 178 | /// Block until the process completes. 179 | /// 180 | /// The maximum time span we are willing to wait 181 | /// an exception that indicates a problem early in the pipeline 182 | public bool WaitForCompletion(TimeSpan? timeout) 183 | { 184 | if (_completedHandle.WaitOne(timeout ?? TimeSpan.FromMilliseconds(-1))) return true; 185 | 186 | Stop(); 187 | return false; 188 | } 189 | 190 | private readonly object _unpackLock = new(); 191 | private readonly object _sendLock = new(); 192 | private bool _sentControlC; 193 | 194 | 195 | public bool SendControlC(int processId) 196 | { 197 | var platform = (int)Environment.OSVersion.Platform; 198 | var isWindows = platform != 4 && platform != 6 && platform != 128; 199 | if (isWindows) 200 | { 201 | var path = Path.Combine(Path.GetTempPath(), "proc-c.exe"); 202 | UnpackTempOutOfProcessSignalSender(path); 203 | lock (_sendLock) 204 | { 205 | var args = new StartArguments(path, processId.ToString(CultureInfo.InvariantCulture)) 206 | { 207 | WaitForExit = null, 208 | Timeout = TimeSpan.FromSeconds(5) 209 | }; 210 | var result = Proc.Start(args); 211 | SendYesForBatPrompt(); 212 | return result.ExitCode == 0; 213 | } 214 | } 215 | else 216 | { 217 | lock (_sendLock) 218 | { 219 | // I wish .NET Core had signals baked in but looking at the corefx repos tickets this is not happening any time soon. 220 | var args = new StartArguments("kill", "-SIGINT", processId.ToString(CultureInfo.InvariantCulture)) 221 | { 222 | WaitForExit = null, 223 | Timeout = TimeSpan.FromSeconds(5) 224 | }; 225 | var result = Proc.Start(args); 226 | return result.ExitCode == 0; 227 | } 228 | } 229 | } 230 | 231 | public void SendControlC() 232 | { 233 | if (_sentControlC) return; 234 | if (!ProcessId.HasValue) return; 235 | 236 | var success = SendControlC(ProcessId.Value); 237 | _sentControlC = true; 238 | } 239 | 240 | protected void SendYesForBatPrompt() 241 | { 242 | if (!StopRequested) return; 243 | if (ProcessName == "cmd") 244 | { 245 | try 246 | { 247 | StandardInput.WriteLine("Y"); 248 | } 249 | //best effort 250 | catch (InvalidOperationException) { } 251 | catch (IOException) { } 252 | } 253 | } 254 | 255 | private void UnpackTempOutOfProcessSignalSender(string path) 256 | { 257 | if (File.Exists(path)) return; 258 | var assembly = typeof(Proc).GetTypeInfo().Assembly; 259 | try 260 | { 261 | lock (_unpackLock) 262 | { 263 | if (File.Exists(path)) return; 264 | using (var stream = assembly.GetManifestResourceStream("ProcNet.Embedded.Proc.ControlC.exe")) 265 | using (var fs = File.OpenWrite(path)) 266 | stream.CopyTo(fs); 267 | } 268 | } 269 | catch (Exception e) 270 | { 271 | Console.WriteLine(e); 272 | throw; 273 | } 274 | } 275 | 276 | protected bool StopRequested => _stopRequested || _sentControlC; 277 | private bool _stopRequested; 278 | private void Stop(int? exitCode = null, IObserver observer = null) 279 | { 280 | try 281 | { 282 | _stopRequested = true; 283 | if (Process == null) return; 284 | 285 | var wait = StartArguments.WaitForExit; 286 | try 287 | { 288 | if (Started && wait.HasValue) 289 | { 290 | bool exitted; 291 | if (StartArguments.SendControlCFirst) 292 | { 293 | SendControlC(); 294 | exitted = Process?.WaitForExit((int) wait.Value.TotalMilliseconds) ?? false; 295 | //still attempt to kill to process if control c failed 296 | if (!exitted) Process?.Kill(); 297 | } 298 | else 299 | { 300 | Process?.Kill(); 301 | exitted = Process?.WaitForExit((int) wait.Value.TotalMilliseconds) ?? false; 302 | } 303 | 304 | //if we haven't exited do a hard wait for exit by using the overload that does not timeout. 305 | if (Process != null && !exitted) HardWaitForExit(TimeSpan.FromSeconds(10)); 306 | } 307 | else if (Started) 308 | { 309 | Process?.Kill(); 310 | } 311 | } 312 | //Access denied usually means the program is already terminating. 313 | catch (Win32Exception) { } 314 | //This usually indiciates the process is already terminated 315 | catch (InvalidOperationException) { } 316 | try 317 | { 318 | Process?.Dispose(); 319 | } 320 | //the underlying call to .Close() can throw an NRE if you dispose too fast after starting 321 | catch (NullReferenceException) { } 322 | } 323 | finally 324 | { 325 | if (Started && exitCode.HasValue) 326 | ExitCode = exitCode.Value; 327 | 328 | Started = false; 329 | if (observer != null) OnCompleted(observer); 330 | SetCompletedHandle(); 331 | } 332 | } 333 | 334 | private void HardKill() 335 | { 336 | try 337 | { 338 | Process?.Kill(); 339 | } 340 | catch (Exception) 341 | { 342 | // ignored 343 | } 344 | finally 345 | { 346 | try 347 | { 348 | Process?.Dispose(); 349 | } 350 | catch (Exception) 351 | { 352 | // ignored 353 | } 354 | } 355 | } 356 | 357 | protected void SetCompletedHandle() 358 | { 359 | OnBeforeSetCompletedHandle(); 360 | _completedHandle.Set(); 361 | } 362 | 363 | protected virtual void OnBeforeSetCompletedHandle() { } 364 | 365 | private bool HardWaitForExit(TimeSpan timeSpan) 366 | { 367 | var task = Task.Run(() => Process.WaitForExit()); 368 | return (Task.WaitAny(task, Task.Delay(timeSpan)) == 0); 369 | } 370 | 371 | private bool _isDisposing; 372 | 373 | public void Dispose() 374 | { 375 | _isDisposing = true; 376 | Stop(); 377 | _isDisposing = false; 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/Proc/Proc.Exec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using ProcNet.Extensions; 6 | 7 | namespace ProcNet 8 | { 9 | public static partial class Proc 10 | { 11 | 12 | /// 13 | /// This simply executes and returns the exit code or throws if the binary failed to start 14 | /// This method shares the same console and does not capture the output 15 | /// Use or overloads if you want to capture output and write to console in realtime 16 | /// 17 | /// If the application fails to start 18 | /// The exit code of the binary being run 19 | public static int Exec(string binary, params string[] arguments) => Exec(new ExecArguments(binary, arguments)); 20 | 21 | /// 22 | /// This simply executes a binary and returns the exit code or throws if the binary failed to start 23 | /// This method shares the same console and does not capture the output 24 | /// Use or overloads if you want to capture output and write to console in realtime 25 | /// 26 | /// If the application fails to start 27 | /// The exit code of the binary being run 28 | public static int Exec(ExecArguments arguments) 29 | { 30 | var args = arguments.Args?.ToArray() ?? []; 31 | var info = new ProcessStartInfo(arguments.Binary) 32 | { 33 | UseShellExecute = false 34 | }; 35 | #if !STRING_ARGS 36 | foreach (var arg in args) 37 | info.ArgumentList.Add(arg); 38 | #else 39 | info.Arguments = args != null ? string.Join(" ", args) : string.Empty; 40 | #endif 41 | 42 | var pwd = arguments.WorkingDirectory; 43 | if (!string.IsNullOrWhiteSpace(pwd)) info.WorkingDirectory = pwd; 44 | if (arguments.Environment != null) 45 | foreach (var kv in arguments.Environment) 46 | info.Environment[kv.Key] = kv.Value; 47 | 48 | var printBinary = arguments.OnlyPrintBinaryInExceptionMessage 49 | ? $"\"{arguments.Binary}\"" 50 | : $"\"{arguments.Binary} {args.NaivelyQuoteArguments()}\"{(pwd == null ? string.Empty : $" pwd: {pwd}")}"; 51 | 52 | using var process = new Process { StartInfo = info }; 53 | if (!process.Start()) 54 | throw new ProcExecException($"Failed to start {printBinary}"); 55 | 56 | if (arguments.Timeout.HasValue) 57 | { 58 | var t = arguments.Timeout.Value; 59 | var completedBeforeTimeout =process.WaitForExit((int)t.TotalMilliseconds); 60 | if (!completedBeforeTimeout) 61 | { 62 | HardWaitForExit(process, TimeSpan.FromSeconds(1)); 63 | throw new ProcExecException($"Timeout {t} occured while running {printBinary}"); 64 | } 65 | } 66 | else 67 | process.WaitForExit(); 68 | 69 | var exitCode = process.ExitCode; 70 | if (!arguments.ValidExitCodeClassifier(exitCode)) 71 | throw new ProcExecException($"Process exited with '{exitCode}' {printBinary}") 72 | { 73 | ExitCode = exitCode 74 | }; 75 | 76 | return exitCode; 77 | } 78 | 79 | private static void HardWaitForExit(Process process, TimeSpan timeSpan) 80 | { 81 | using var task = Task.Run(() => process.WaitForExit()); 82 | Task.WaitAny(task, Task.Delay(timeSpan)); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Proc/Proc.ExecAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using ProcNet.Extensions; 7 | 8 | #if NET6_0_OR_GREATER 9 | namespace ProcNet 10 | { 11 | public static partial class Proc 12 | { 13 | /// 14 | /// This simply executes a binary and returns the exit code or throws if the binary failed to start 15 | /// This method shares the same console and does not capture the output 16 | /// Use or overloads if you want to capture output and write to console in realtime 17 | /// 18 | /// If the application fails to start 19 | /// The exit code of the binary being run 20 | public static async Task ExecAsync(ExecArguments arguments, CancellationToken ctx = default) 21 | { 22 | var args = arguments.Args?.ToArray() ?? []; 23 | var info = new ProcessStartInfo(arguments.Binary) 24 | { 25 | UseShellExecute = false 26 | }; 27 | foreach (var arg in args) 28 | info.ArgumentList.Add(arg); 29 | 30 | var pwd = arguments.WorkingDirectory; 31 | if (!string.IsNullOrWhiteSpace(pwd)) info.WorkingDirectory = pwd; 32 | if (arguments.Environment != null) 33 | foreach (var kv in arguments.Environment) 34 | info.Environment[kv.Key] = kv.Value; 35 | 36 | var printBinary = arguments.OnlyPrintBinaryInExceptionMessage 37 | ? $"\"{arguments.Binary}\"" 38 | : $"\"{arguments.Binary} {args.NaivelyQuoteArguments()}\"{(pwd == null ? string.Empty : $" pwd: {pwd}")}"; 39 | 40 | using var process = new Process { StartInfo = info }; 41 | if (!process.Start()) throw new ProcExecException($"Failed to start {printBinary}"); 42 | 43 | if (arguments.Timeout.HasValue) 44 | { 45 | var t = arguments.Timeout.Value; 46 | var completedBeforeTimeout =process.WaitForExit((int)t.TotalMilliseconds); 47 | if (!completedBeforeTimeout) 48 | { 49 | await HardWaitForExitAsync(process, TimeSpan.FromSeconds(1)); 50 | throw new ProcExecException($"Timeout {t} occured while running {printBinary}"); 51 | } 52 | } 53 | else 54 | await process.WaitForExitAsync(ctx); 55 | 56 | var exitCode = process.ExitCode; 57 | if (!arguments.ValidExitCodeClassifier(exitCode)) 58 | throw new ProcExecException($"Process exited with '{exitCode}' {printBinary}") 59 | { 60 | ExitCode = exitCode 61 | }; 62 | 63 | return exitCode; 64 | } 65 | 66 | private static async Task HardWaitForExitAsync(Process process, TimeSpan timeSpan) 67 | { 68 | using var task = Task.Run(() => process.WaitForExitAsync()); 69 | await Task.WhenAny(task, Task.Delay(timeSpan)); 70 | } 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /src/Proc/Proc.Start.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive.Disposables; 4 | using System.Runtime.ExceptionServices; 5 | using ProcNet.Std; 6 | 7 | namespace ProcNet 8 | { 9 | public static partial class Proc 10 | { 11 | /// Starts a program and captures the output while writing to the console in realtime during execution 12 | /// The binary to execute 13 | /// the commandline arguments to add to the invocation of 14 | /// An object holding a list of console out lines, the exit code and whether the process completed 15 | public static ProcessCaptureResult Start(string bin, params string[] arguments) => 16 | Start(new StartArguments(bin, arguments)); 17 | 18 | /// Starts a program and captures the output while writing to the console in realtime during execution 19 | /// Encompasses all the options you can specify to start Proc processes 20 | /// 21 | /// An implementation of that takes care of writing to the console 22 | /// defaults to which writes standard error messages in red 23 | /// 24 | /// An object holding a list of console out lines, the exit code and whether the process completed 25 | public static ProcessCaptureResult Start(StartArguments arguments) 26 | { 27 | using var composite = new CompositeDisposable(); 28 | var process = new ObservableProcess(arguments); 29 | var consoleOutWriter = arguments.ConsoleOutWriter ?? new ConsoleOutColorWriter(); 30 | 31 | Exception seenException = null; 32 | var consoleOut = new List(); 33 | composite.Add(process); 34 | composite.Add(process.SubscribeLinesAndCharacters( 35 | l => 36 | { 37 | consoleOut.Add(l); 38 | }, 39 | e => seenException = e, 40 | l => consoleOutWriter.Write(l), 41 | e => 42 | { 43 | seenException = e; 44 | consoleOutWriter.Write(e); 45 | } 46 | ) 47 | ); 48 | 49 | var completed = process.WaitForCompletion(arguments.Timeout); 50 | if (seenException != null) ExceptionDispatchInfo.Capture(seenException).Throw(); 51 | return new ProcessCaptureResult(completed, consoleOut, process.ExitCode); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Proc/Proc.StartLongRunning.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Disposables; 3 | using System.Runtime.ExceptionServices; 4 | using System.Threading; 5 | using ProcNet.Extensions; 6 | using ProcNet.Std; 7 | 8 | namespace ProcNet 9 | { 10 | public class LongRunningApplicationSubscription : IDisposable 11 | { 12 | internal LongRunningApplicationSubscription(ObservableProcess process, CompositeDisposable subscription) 13 | { 14 | Process = process; 15 | Subscription = subscription; 16 | } 17 | 18 | private IDisposable Subscription { get; } 19 | 20 | private ObservableProcess Process { get; } 21 | 22 | public bool Running { get; internal set; } 23 | 24 | internal ManualResetEvent WaitHandle { get; } = new(false); 25 | 26 | /// > 27 | public bool SendControlC(int processId) => Process.SendControlC(processId); 28 | 29 | /// > 30 | public void SendControlC() => Process.SendControlC(); 31 | 32 | public void Dispose() 33 | { 34 | Subscription?.Dispose(); 35 | Process?.Dispose(); 36 | } 37 | } 38 | 39 | public static partial class Proc 40 | { 41 | 42 | /// 43 | /// Allows you to start long running processes and dispose them when needed. 44 | /// It also optionally allows you to wait before returning the disposable 45 | /// by inspecting the console out of the process to validate the 'true' starting confirmation of the process using 46 | /// 47 | /// 48 | /// 49 | /// A usecase for this could be starting a webserver or database and wait before it prints it startup confirmation 50 | /// before returning. 51 | /// 52 | /// 53 | /// Encompasses all the options you can specify to start Proc processes 54 | /// Waits returning the before confirms the process started 55 | /// 56 | /// An implementation of that takes care of writing to the console 57 | /// defaults to which writes standard error messages in red 58 | /// 59 | /// The exit code and whether the process completed 60 | public static LongRunningApplicationSubscription StartLongRunning(LongRunningArguments arguments, TimeSpan waitForStartedConfirmation, IConsoleOutWriter consoleOutWriter = null) 61 | { 62 | var composite = new CompositeDisposable(); 63 | var process = new ObservableProcess(arguments); 64 | var subscription = new LongRunningApplicationSubscription(process, composite); 65 | consoleOutWriter ??= new ConsoleOutColorWriter(); 66 | 67 | var startedConfirmation = arguments.StartedConfirmationHandler ?? (_ => true); 68 | 69 | if (arguments.StartedConfirmationHandler != null && arguments.StopBufferingAfterStarted) 70 | arguments.KeepBufferingLines = _ => !subscription.Running; 71 | 72 | Exception seenException = null; 73 | composite.Add(process); 74 | composite.Add(process.SubscribeLinesAndCharacters( 75 | l => 76 | { 77 | if (!startedConfirmation(l)) return; 78 | subscription.Running = true; 79 | subscription.WaitHandle.Set(); 80 | }, 81 | e => 82 | { 83 | seenException = e; 84 | subscription.Running = false; 85 | subscription.WaitHandle.Set(); 86 | }, 87 | l => consoleOutWriter.Write(l), 88 | l => consoleOutWriter.Write(l), 89 | onCompleted: () => 90 | { 91 | subscription.Running = false; 92 | subscription.WaitHandle.Set(); 93 | }) 94 | ); 95 | 96 | if (seenException != null) ExceptionDispatchInfo.Capture(seenException).Throw(); 97 | if (arguments.StartedConfirmationHandler == null) 98 | { 99 | subscription.Running = true; 100 | subscription.WaitHandle.Set(); 101 | } 102 | else 103 | { 104 | var completed = subscription.WaitHandle.WaitOne(waitForStartedConfirmation); 105 | if (completed) return subscription; 106 | var pwd = arguments.WorkingDirectory; 107 | var args = arguments.Args; 108 | var printBinary = arguments.OnlyPrintBinaryInExceptionMessage 109 | ? $"\"{arguments.Binary}\"" 110 | : $"\"{arguments.Binary} {args.NaivelyQuoteArguments()}\"{(pwd == null ? string.Empty : $" pwd: {pwd}")}"; 111 | throw new ProcExecException($"Could not yield started confirmation after {waitForStartedConfirmation} while running {printBinary}"); 112 | } 113 | 114 | return subscription; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Proc/Proc.StartRedirected.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Disposables; 3 | using System.Runtime.ExceptionServices; 4 | using ProcNet.Std; 5 | 6 | namespace ProcNet 7 | { 8 | public static partial class Proc 9 | { 10 | /// 11 | /// Start a program and get notified of lines in realtime through unlike 12 | /// This won't capture all lines on the returned object and won't default to writing to the Console. 13 | /// 14 | /// 15 | /// An implementation of that receives every line as or the that occurs while running 16 | /// 17 | /// The exit code and whether the process completed 18 | public static ProcessResult StartRedirected(IConsoleLineHandler lineHandler, string bin, params string[] arguments) => 19 | StartRedirected(lineHandler, bin, arguments); 20 | 21 | /// 22 | /// Start a program and get notified of lines in realtime through unlike 23 | /// This won't capture all lines on the returned object and won't default to writing to the Console. 24 | /// 25 | /// Encompasses all the options you can specify to start Proc processes 26 | /// 27 | /// An implementation of that receives every line as or the that occurs while running 28 | /// 29 | /// The exit code and whether the process completed 30 | public static ProcessResult StartRedirected(StartArguments arguments, IConsoleLineHandler lineHandler = null) => 31 | StartRedirected(arguments, standardInput: null, lineHandler: lineHandler); 32 | 33 | /// 34 | /// Start a program and get notified of lines in realtime through unlike 35 | /// This won't capture all lines on the returned object and won't default to writing to the Console. 36 | /// 37 | /// Encompasses all the options you can specify to start Proc processes 38 | /// The maximum runtime of the started program 39 | /// A callback when the process is ready to receive standard in writes 40 | /// 41 | /// An implementation of that receives every line as or the that occurs while running 42 | /// 43 | /// The exit code and whether the process completed 44 | public static ProcessResult StartRedirected(StartArguments arguments, StandardInputHandler standardInput, IConsoleLineHandler lineHandler = null) 45 | { 46 | using (var composite = new CompositeDisposable()) 47 | { 48 | var process = new ObservableProcess(arguments); 49 | if (standardInput != null) process.StandardInputReady += standardInput; 50 | 51 | Exception seenException = null; 52 | composite.Add(process); 53 | composite.Add(process.SubscribeLines( 54 | l => lineHandler?.Handle(l), 55 | e => 56 | { 57 | seenException = e; 58 | lineHandler?.Handle(e); 59 | }) 60 | ); 61 | 62 | var completed = process.WaitForCompletion(arguments.Timeout); 63 | if (seenException != null) ExceptionDispatchInfo.Capture(seenException).Throw(); 64 | return new ProcessResult(completed, process.ExitCode); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Proc/Proc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | proc 5 | netstandard2.0;netstandard2.1;net461;net8.0 6 | ProcNet 7 | 8 | 9 | true 10 | ..\..\build\keys\keypair.snk 11 | 12 | nuget-icon.png 13 | MIT 14 | https://github.com/nullean/proc 15 | https://github.com/nullean/proc 16 | https://github.com/nullean/proc/releases 17 | 18 | Proc 19 | Proc - The easiest and full featured way to execute Processes in .NET 20 | Dependency free reactive abstraction around Process, exposes handy static methods for the quick one-liners 21 | 22 | $(ProcCurrentVersion) 23 | $(ProcCurrentVersion) 24 | $(ProcCurrentAssemblyVersion) 25 | $(ProcCurrentAssemblyFileVersion) 26 | true 27 | Latest 28 | README.md 29 | STRING_ARGS 30 | 31 | 32 | 33 | 34 | 35 | nuget-icon.png 36 | True 37 | nuget-icon.png 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Proc/ProcessArgumentsBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace ProcNet 5 | { 6 | public abstract class ProcessArgumentsBase 7 | { 8 | public ProcessArgumentsBase(string binary, IEnumerable args) : this(binary, args?.ToArray()) { } 9 | 10 | public ProcessArgumentsBase(string binary, params string[] args) 11 | { 12 | Binary = binary; 13 | Args = args; 14 | } 15 | 16 | public string Binary { get; } 17 | public IEnumerable Args { get; } 18 | 19 | /// Provide environment variable scoped to the process being executed 20 | public IDictionary Environment { get; set; } 21 | 22 | /// Set the current working directory 23 | public string WorkingDirectory { get; set; } 24 | 25 | /// Force arguments and the current working director NOT to be part of the exception message 26 | public bool OnlyPrintBinaryInExceptionMessage { get; set; } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Proc/ProcessCaptureResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using ProcNet.Std; 4 | 5 | namespace ProcNet 6 | { 7 | /// 8 | /// A version of that captures all the console out and returns it on 9 | /// This is handy for when calling small utility binaries but if you are calling a binary with very verbose output you might not 10 | /// want to capture and store this information. 11 | /// and overloads will return a capturing result like this 12 | /// and overloads will not return the console out 13 | /// 14 | public class ProcessCaptureResult : ProcessResult 15 | { 16 | public IList ConsoleOut { get; } 17 | 18 | public ProcessCaptureResult(bool completed, IList consoleOut, int? exitCode) : base(completed, exitCode) => 19 | ConsoleOut = consoleOut ?? new List(); 20 | 21 | public override string ToString() 22 | { 23 | var sb = new StringBuilder(); 24 | sb.AppendLine(base.ToString()); 25 | sb.AppendLine(new string('-', 8)); 26 | foreach (var o in ConsoleOut) 27 | { 28 | sb.AppendLine($"[std{(o.Error ? "err" : "out")}]: {o.Line}"); 29 | } 30 | return sb.ToString(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Proc/ProcessResult.cs: -------------------------------------------------------------------------------- 1 | namespace ProcNet 2 | { 3 | public class ProcessResult 4 | { 5 | public bool Completed { get; } 6 | public int? ExitCode { get; } 7 | 8 | public ProcessResult(bool completed, int? exitCode) 9 | { 10 | Completed = completed; 11 | ExitCode = exitCode; 12 | } 13 | 14 | public override string ToString() => $"Process completed: {Completed}, ExitCode: {ExitCode}"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Proc/README.md: -------------------------------------------------------------------------------- 1 | # Proc 2 | 3 | A dependency free `System.Diagnostics.Process` supercharger. 4 | 5 | 1. `Proc.Exec()` for the quick one-liners 6 | 2. `Proc.Start()` for the quick one-liners 7 | - Use if you want to capture the console output as well as print these message in real time. 8 | - Proc.Start() also allows you to script StandardIn and react to messages 9 | 3. Wraps `System.Diagnostics.Process` as an `IObservable` 10 | * `ProcessObservable` stream based wrapper 11 | * `EventBasedObservableProcess` event based wrapper 12 | 4. Built in support to send `SIGINT` to any process before doing a hard `SIGKILL` (`Process.Kill()`) 13 | * Has to be set using `SendControlCFirst = true` on `StartArguments` 14 | 15 | ## Proc.Exec 16 | 17 | Execute a process and blocks using a default timeout of 4 minutes. This method uses the same console session 18 | as and as such will print the binaries console output. Throws a `ProcExecException` if the command fails to execute. 19 | See also `ExecArguments` for more options 20 | 21 | ```csharp 22 | Proc.Exec("ipconfig", "/all"); 23 | ``` 24 | 25 | ## Proc.Start 26 | 27 | start a process and block using the default timeout of 4 minutes 28 | ```csharp 29 | var result = Proc.Start("ipconfig", "/all"); 30 | ``` 31 | 32 | Provide a custom timeout and an `IConsoleOutWriter` that can output to console 33 | while this line is blocking. The following example writes `stderr` in red. 34 | 35 | ```csharp 36 | var result = Proc.Start("ipconfig", TimeSpan.FromSeconds(10), new ConsoleOutColorWriter()); 37 | ``` 38 | 39 | More options can be passed by passing `StartArguments` instead to control how the process should start. 40 | 41 | ```csharp 42 | var args = new StartArguments("ipconfig", "/all") 43 | { 44 | WorkingDirectory = .. 45 | } 46 | Proc.Start(args, TimeSpan.FromSeconds(10)); 47 | ``` 48 | 49 | The static `Proc.Start` has a timeout of `4 minutes` if not specified. 50 | 51 | `result` has the following properties 52 | 53 | * `Completed` true if the program completed before the timeout 54 | * `ConsoleOut` a list the console out message as `LineOut` 55 | instances where `Error` on each indicating whether it was written on `stderr` or not 56 | * `ExitCode` 57 | 58 | **NOTE** `ConsoleOut` will always be set regardless of whether an `IConsoleOutWriter` is provided 59 | 60 | ## ObservableProcess 61 | 62 | The heart of it all this is an `IObservable`. It listens on the output buffers directly and does not wait on 63 | newlines to emit. 64 | 65 | To create an observable process manually follow the following pattern: 66 | 67 | ```csharp 68 | using (var p = new ObservableProcess(args)) 69 | { 70 | p.Subscribe(c => Console.Write(c.Characters)); 71 | p.WaitForCompletion(TimeSpan.FromSeconds(2)); 72 | } 73 | ``` 74 | 75 | The observable is `cold` untill subscribed and is not intended to be reused or subscribed to multiple times. If you need to 76 | share a subscription look into RX's `Publish`. 77 | 78 | The `WaitForCompletion()` call blocks so that `p` is not disposed which would attempt to shutdown the started process. 79 | 80 | The default for doing a shutdown is through `Process.Kill` this is a hard `SIGKILL` on the process. 81 | 82 | The cool thing about `Proc` is that it supports `SIGINT` interoptions as well to allow for processes to be cleanly shutdown. 83 | 84 | ```csharp 85 | var args = new StartArguments("elasticsearch.bat") 86 | { 87 | SendControlCFirst = true 88 | }; 89 | ``` 90 | 91 | This will attempt to send a `Control+C` into the running process console on windows first before falling back to `Process.Kill`. 92 | Linux and OSX support for this flag is still in the works so thats why this behaviour is opt in. 93 | 94 | 95 | Dealing with `byte[]` characters might not be what you want to program against, so `ObservableProcess` allows the following as well. 96 | 97 | 98 | ```csharp 99 | using (var p = new ObservableProcess(args)) 100 | { 101 | p.SubscribeLines(c => Console.WriteLine(c.Line)); 102 | p.WaitForCompletion(TimeSpan.FromSeconds(2)); 103 | } 104 | ``` 105 | 106 | Instead of proxying `byte[]` as they are received on the socket this buffers and only emits on lines. 107 | 108 | In some cases it can be very useful to introduce your own word boundaries 109 | 110 | ```csharp 111 | public class MyProcObservable : ObservableProcess 112 | { 113 | public MyProcObservable(string binary, params string[] arguments) : base(binary, arguments) { } 114 | 115 | public MyProcObservable(StartArguments startArguments) : base(startArguments) { } 116 | 117 | protected override bool BufferBoundary(char[] stdOut, char[] stdErr) 118 | { 119 | return base.BufferBoundary(stdOut, stdErr); 120 | } 121 | } 122 | ``` 123 | 124 | returning true inside `BufferBoundary` will yield the line to `SubscribeLine()`. This could be usefull e.g if your process 125 | prompts without a new line: 126 | 127 | > Continue [Y/N]: 128 | 129 | A more concrete example of this is when you call a `bat` file on windows and send a `SIGINT` signal it will *always* prompt: 130 | 131 | > Terminate batch job (Y/N)? 132 | 133 | Which would not yield to `SubscribeLines` and block any waithandles unnecessary. `ObservableProcess` handles this edgecase 134 | therefor OOTB and automatically replies with `Y` on `stdin` in this case. 135 | 136 | Also note that `ObservableProcess` will yield whatever is in the buffer before OnCompleted(). 137 | 138 | 139 | # EventBasedObservable 140 | 141 | `ObservableProcess`'s sibbling that utilizes `OutputDataReceived` and `ErrorDataReceived` and can only emit lines. 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /src/Proc/StartArguments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ProcNet.Std; 4 | 5 | namespace ProcNet 6 | { 7 | /// Encompasses all the options you can specify to start Proc processes 8 | public class StartArguments : ProcessArgumentsBase 9 | { 10 | public StartArguments(string binary, IEnumerable args) : base(binary, args) { } 11 | 12 | public StartArguments(string binary, params string[] args) : base(binary, args) { } 13 | 14 | // ReSharper disable UnusedAutoPropertyAccessor.Global 15 | 16 | public TimeSpan? Timeout { get; set; } 17 | 18 | public IConsoleOutWriter ConsoleOutWriter { get; set; } 19 | 20 | /// 21 | /// By default processes are started in a threadpool thread assuming you start multiple from the same thread. 22 | /// By setting this property to true we do not do this. Know however that when multiple instances of observableprocess 23 | /// stop at the same time they are all queueing for which may lead to 24 | /// unexpected behaviour 25 | /// 26 | public bool NoWrapInThread { get; set; } 27 | 28 | /// 29 | /// return true to stop buffering lines. Allows callers to optionally short circuit reading 30 | /// from stdout/stderr without closing the process. 31 | /// This is great for long running processes to only buffer console output until 32 | /// all information is parsed. 33 | /// 34 | /// True to end the buffering of char[] to lines of text 35 | public Func KeepBufferingLines { get; set; } 36 | 37 | /// Attempts to send control+c (SIGINT) to the process first 38 | public bool SendControlCFirst { get; set; } 39 | 40 | /// 41 | /// Filter the lines we are interesting in and want to return on 42 | /// Defaults to true meaning all all lines. 43 | /// 44 | public Func LineOutFilter { get; set; } 45 | 46 | /// 47 | /// Callback with a reference to standard in once the process has started and stdin can be written too 48 | /// 49 | public StandardInputHandler StandardInputHandler { get; set; } 50 | 51 | private static readonly TimeSpan DefaultWaitForExit = TimeSpan.FromSeconds(10); 52 | 53 | /// 54 | /// By default when we kill the process we wait for its completion with a timeout of `10s`. 55 | /// By specifying `null` you omit the wait for completion all together. 56 | /// 57 | public TimeSpan? WaitForExit { get; set; } = DefaultWaitForExit; 58 | 59 | /// 60 | /// How long we should wait for the output stream readers to finish when the process exits before we call 61 | /// is called. By default waits for 5 seconds. 62 | /// Set to null to skip waiting,defaults to 10 seconds 63 | /// 64 | public TimeSpan? WaitForStreamReadersTimeout { get; set; } = DefaultWaitForExit; 65 | 66 | // ReSharper enable UnusedAutoPropertyAccessor.Global 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Proc/Std/CharactersOut.cs: -------------------------------------------------------------------------------- 1 | namespace ProcNet.Std 2 | { 3 | public class CharactersOut : ConsoleOut 4 | { 5 | public override bool Error { get; } 6 | public char[] Characters { get; } 7 | internal CharactersOut(bool error, char[] characters) 8 | { 9 | Error = error; 10 | Characters = characters; 11 | } 12 | 13 | internal bool EndsWithNewLine => 14 | Characters.Length > 0 && Characters[Characters.Length - 1] == '\n'; 15 | 16 | internal bool StartsWithCarriage => 17 | Characters.Length > 0 && Characters[0] == '\r'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Proc/Std/ConsoleOut.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProcNet.Std 4 | { 5 | public abstract class ConsoleOut 6 | { 7 | public abstract bool Error { get; } 8 | 9 | public void OutOrErrrorCharacters(Action outChararchters, Action errorCharacters) 10 | { 11 | switch (this) 12 | { 13 | case CharactersOut c: 14 | if (Error) errorCharacters(c.Characters); 15 | else outChararchters(c.Characters); 16 | break; 17 | default: 18 | throw new Exception($"{nameof(ConsoleOut)} is not of type {nameof(CharactersOut)} {nameof(OutOrErrrorCharacters)} not allowed"); 19 | } 20 | } 21 | public void CharsOrString(Action doCharacters, Action doLine) 22 | { 23 | switch (this) 24 | { 25 | case CharactersOut c: 26 | doCharacters(c.Characters); 27 | break; 28 | case LineOut l: 29 | doLine(l.Line); 30 | break; 31 | } 32 | } 33 | 34 | 35 | public static LineOut ErrorOut(string data) => new LineOut(true, data); 36 | public static LineOut Out(string data) => new LineOut(false, data); 37 | 38 | public static CharactersOut ErrorOut(char[] characters) => new CharactersOut(true, characters); 39 | public static CharactersOut Out(char[] characters) => new CharactersOut(false, characters); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Proc/Std/ConsoleOutColorWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProcNet.Std; 4 | 5 | public class ConsoleOutColorWriter : ConsoleOutWriter 6 | { 7 | public static ConsoleOutColorWriter Default { get; } = new(); 8 | 9 | private readonly object _lock = new(); 10 | public override void Write(ConsoleOut consoleOut) 11 | { 12 | lock (_lock) 13 | { 14 | Console.ResetColor(); 15 | if (consoleOut.Error) 16 | { 17 | Console.ForegroundColor = ConsoleColor.Yellow; 18 | consoleOut.CharsOrString(ErrorCharacters, ErrorLine); 19 | } 20 | else 21 | consoleOut.CharsOrString(OutCharacters, OutLine); 22 | 23 | Console.ResetColor(); 24 | } 25 | } 26 | 27 | public override void Write(Exception e) 28 | { 29 | if (!(e is CleanExitExceptionBase ee)) throw e; 30 | lock (_lock) 31 | { 32 | Console.ResetColor(); 33 | Console.ForegroundColor = ConsoleColor.Red; 34 | Console.Write(e.Message); 35 | if (!string.IsNullOrEmpty(ee.HelpText)) 36 | { 37 | Console.ForegroundColor = ConsoleColor.DarkRed; 38 | Console.Write(ee.HelpText); 39 | } 40 | Console.ResetColor(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Proc/Std/ConsoleOutWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProcNet.Std 4 | { 5 | public class ConsoleOutWriter : IConsoleOutWriter 6 | { 7 | public virtual void Write(Exception e) 8 | { 9 | if (!(e is CleanExitExceptionBase ee)) throw e; 10 | Console.WriteLine(e.Message); 11 | if (!string.IsNullOrEmpty(ee.HelpText)) 12 | { 13 | Console.WriteLine(ee.HelpText); 14 | } 15 | 16 | } 17 | public virtual void Write(ConsoleOut consoleOut) 18 | { 19 | if (consoleOut.Error) 20 | consoleOut.CharsOrString(ErrorCharacters, ErrorLine); 21 | else 22 | consoleOut.CharsOrString(OutCharacters, OutLine); 23 | } 24 | 25 | protected static void ErrorCharacters(char[] data) => Console.Error.Write(data); 26 | protected static void ErrorLine(string data) => Console.Error.WriteLine(data); 27 | protected static void OutCharacters(char[] data) => Console.Write(data); 28 | protected static void OutLine(string data) => Console.WriteLine(data); 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Proc/Std/IConsoleLineHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProcNet.Std 4 | { 5 | public interface IConsoleLineHandler 6 | { 7 | void Handle(LineOut lineOut); 8 | void Handle(Exception e); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/Proc/Std/IConsoleOutWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProcNet.Std 4 | { 5 | public interface IConsoleOutWriter 6 | { 7 | void Write(Exception e); 8 | void Write(ConsoleOut consoleOut); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Proc/Std/LineOut.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProcNet.Std 4 | { 5 | /// 6 | /// This class represents a single line that was printed on the console. 7 | /// 8 | public class LineOut : ConsoleOut 9 | { 10 | private static readonly char[] NewlineChars = Environment.NewLine.ToCharArray(); 11 | 12 | /// 13 | /// Indicates if this messages originated from standard error output. 14 | /// 15 | public override bool Error { get; } 16 | 17 | public string Line { get; } 18 | 19 | internal LineOut(bool error, string line) 20 | { 21 | Error = error; 22 | Line = line; 23 | } 24 | 25 | /// 26 | /// Converts a instance to an instance. 27 | /// If is of type already we simply return that instance 28 | /// otherwise this will return null. 29 | /// 30 | public static LineOut From(ConsoleOut consoleOut) 31 | { 32 | switch (consoleOut) 33 | { 34 | default: return null; 35 | case LineOut l: return l; 36 | case CharactersOut c: 37 | var s = new string(c.Characters, 0, c.Characters.Length).TrimEnd(NewlineChars); 38 | return consoleOut.Error ? ErrorOut(s) : Out(s); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/Proc.Tests.Binary/Proc.Tests.Binary.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | Proc.Tests.Binary 6 | Proc.Tests.Binary 7 | CS1701,CS1591 8 | false 9 | 10 | -------------------------------------------------------------------------------- /tests/Proc.Tests.Binary/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Net.Sockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Proc.Tests.Binary 9 | { 10 | public static class Program 11 | { 12 | public static async Task Main(string[] args) 13 | { 14 | if (args.Length == 0) 15 | { 16 | Console.Error.WriteLine("no testcase specified"); 17 | return 1; 18 | } 19 | 20 | var testCase = args[0].ToLowerInvariant(); 21 | 22 | if (testCase == nameof(PrintArgs).ToLowerInvariant()) return PrintArgs(args.Skip(1).ToArray()); 23 | if (testCase == nameof(SingleLineNoEnter).ToLowerInvariant()) return SingleLineNoEnter(); 24 | if (testCase == nameof(TwoWrites).ToLowerInvariant()) return TwoWrites(); 25 | 26 | if (testCase == nameof(SingleLine).ToLowerInvariant()) return SingleLine(); 27 | 28 | if (testCase == nameof(DelayedWriter).ToLowerInvariant()) return DelayedWriter(); 29 | 30 | if (testCase == nameof(ReadKeyFirst).ToLowerInvariant()) return ReadKeyFirst(); 31 | if (testCase == nameof(ReadKeyAfter).ToLowerInvariant()) return ReadKeyAfter(); 32 | if (testCase == nameof(SlowOutput).ToLowerInvariant()) return SlowOutput(); 33 | if (testCase == nameof(ReadLineFirst).ToLowerInvariant()) return ReadLineFirst(); 34 | if (testCase == nameof(ReadLineAfter).ToLowerInvariant()) return ReadLineAfter(); 35 | if (testCase == nameof(MoreText).ToLowerInvariant()) return MoreText(); 36 | if (testCase == nameof(TrailingLines).ToLowerInvariant()) return TrailingLines(); 37 | if (testCase == nameof(ControlC).ToLowerInvariant()) return ControlC(); 38 | if (testCase == nameof(ControlCNoWait).ToLowerInvariant()) return ControlCNoWait(); 39 | if (testCase == nameof(OverwriteLines).ToLowerInvariant()) return OverwriteLines(); 40 | if (testCase == nameof(InterMixedOutAndError).ToLowerInvariant()) return InterMixedOutAndError(); 41 | if (testCase == nameof(LongRunning).ToLowerInvariant()) return await LongRunning(); 42 | if (testCase == nameof(TrulyLongRunning).ToLowerInvariant()) return await TrulyLongRunning(); 43 | 44 | return 1; 45 | } 46 | private static int PrintArgs(string[] args) 47 | { 48 | foreach (var arg in args) 49 | Console.WriteLine(arg); 50 | return 0; 51 | } 52 | private static int DelayedWriter() 53 | { 54 | Thread.Sleep(3000); 55 | Console.Write(nameof(DelayedWriter)); 56 | return 20; 57 | } 58 | private static int SingleLineNoEnter() 59 | { 60 | Console.Write(nameof(SingleLineNoEnter)); 61 | return 0; 62 | } 63 | private static int TwoWrites() 64 | { 65 | Console.Write(nameof(TwoWrites)); 66 | Console.Write(nameof(TwoWrites)); 67 | return 0; 68 | } 69 | private static int InterMixedOutAndError() 70 | { 71 | for (var i = 0; i < 200; i += 2) 72 | { 73 | Console.Out.WriteLine(new string(i.ToString()[0], 10)); 74 | Console.Error.WriteLine(new string((i + 1).ToString()[0], 10)); 75 | Task.Delay(1).Wait(); 76 | } 77 | return 0; 78 | } 79 | private static int SingleLine() 80 | { 81 | Console.WriteLine(nameof(SingleLine)); 82 | return 0; 83 | } 84 | private static int OverwriteLines() 85 | { 86 | for (var i = 0; i < 10; i++) 87 | { 88 | 89 | Console.Write($"\r{nameof(OverwriteLines)} {i}"); 90 | Thread.Sleep(100); 91 | } 92 | return 102; 93 | } 94 | private static int ReadKeyFirst() 95 | { 96 | var read = Convert.ToChar(Console.Read()); 97 | Console.Write($"{read}{nameof(ReadKeyFirst)}"); 98 | return 21; 99 | } 100 | private static int ReadKeyAfter() 101 | { 102 | Console.Write("input:"); 103 | Console.Read(); 104 | Console.Write(nameof(ReadKeyAfter)); 105 | return 21; 106 | } 107 | private static int SlowOutput() 108 | { 109 | for (var i = 1; i <= 10; i++) 110 | { 111 | Console.WriteLine("x:" + i); 112 | Thread.Sleep(500); 113 | } 114 | return 121; 115 | } 116 | private static int ReadLineFirst() 117 | { 118 | Console.ReadLine(); 119 | Console.Write(nameof(ReadLineFirst)); 120 | return 21; 121 | } 122 | private static int ReadLineAfter() 123 | { 124 | Console.Write("input:"); 125 | Console.ReadLine(); 126 | Console.Write(nameof(ReadLineAfter)); 127 | return 21; 128 | } 129 | private static int ControlC() 130 | { 131 | Console.WriteLine("Written before control+c"); 132 | Console.CancelKeyPress += (sender, args) => 133 | { 134 | Console.WriteLine("Written after control+c"); 135 | }; 136 | Console.ReadLine(); 137 | return 70; 138 | } 139 | private static int ControlCNoWait() 140 | { 141 | Console.WriteLine("Written before control+c"); 142 | Console.CancelKeyPress += (sender, args) => 143 | { 144 | Console.WriteLine("Written after control+c"); 145 | }; 146 | return 71; 147 | } 148 | 149 | private static int TrailingLines() 150 | { 151 | var output = @"Windows IP Configuration 152 | 153 | 154 | "; 155 | Console.Write(output); 156 | 157 | return 60; 158 | } 159 | 160 | private static async Task LongRunning() 161 | { 162 | for (var i = 0; i < 10; i++) 163 | { 164 | Console.WriteLine($"Starting up: {i}"); 165 | await Task.Delay(20); 166 | } 167 | Console.WriteLine($"Started!"); 168 | await Task.Delay(20); 169 | for (var i = 0; i < 10; i++) 170 | { 171 | Console.WriteLine($"Data after startup: {i}"); 172 | await Task.Delay(20); 173 | } 174 | 175 | 176 | return 0; 177 | } 178 | 179 | private static async Task TrulyLongRunning() 180 | { 181 | for (var i = 0; i < 2; i++) 182 | { 183 | Console.WriteLine($"Starting up: {i}"); 184 | await Task.Delay(TimeSpan.FromSeconds(1)); 185 | } 186 | Console.WriteLine($"Started!"); 187 | await Task.Delay(TimeSpan.FromSeconds(3)); 188 | for (var i = 0; i < 10; i++) 189 | { 190 | Console.WriteLine($"Data after startup: {i}"); 191 | await Task.Delay(TimeSpan.FromSeconds(10)); 192 | } 193 | return 0; 194 | } 195 | 196 | private static int MoreText() 197 | { 198 | var output = @" 199 | Windows IP Configuration 200 | 201 | 202 | Ethernet adapter Ethernet 2: 203 | 204 | Media State . . . . . . . . . . . : Media disconnected 205 | Connection-specific DNS Suffix . : 206 | 207 | Wireless LAN adapter Wi-Fi: 208 | 209 | Media State . . . . . . . . . . . : Media disconnected 210 | Connection-specific DNS Suffix . : 211 | 212 | Ethernet adapter Ethernet 3: 213 | 214 | Connection-specific DNS Suffix . : 215 | Link-local IPv6 Address . . . . . : fe80::e477:59c0:1f38:6cfa%16 216 | IPv4 Address. . . . . . . . . . . : 10.2.20.225 217 | Subnet Mask . . . . . . . . . . . : 255.255.255.0 218 | Default Gateway . . . . . . . . . : 10.2.20.1 219 | 220 | Wireless LAN adapter Local Area Connection* 3: 221 | 222 | Media State . . . . . . . . . . . : Media disconnected 223 | Connection-specific DNS Suffix . : 224 | 225 | Wireless LAN adapter Local Area Connection* 4: 226 | 227 | Media State . . . . . . . . . . . : Media disconnected 228 | Connection-specific DNS Suffix . : 229 | 230 | Ethernet adapter Bluetooth Network Connection: 231 | 232 | Media State . . . . . . . . . . . : Media disconnected 233 | Connection-specific DNS Suffix . : 234 | "; 235 | Console.Write(output); 236 | 237 | return 40; 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /tests/Proc.Tests/AsyncReadsStartStopTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using ProcNet.Std; 8 | using Xunit; 9 | 10 | namespace ProcNet.Tests 11 | { 12 | public class AsyncReadsStartStopTests : TestsBase 13 | { 14 | [Fact] 15 | public void SlowOutput() 16 | { 17 | var args = TestCaseArguments(nameof(SlowOutput)); 18 | var process = new ObservableProcess(args); 19 | var consoleOut = new List(); 20 | Exception seenException = null; 21 | bool? readingBeforeCancelling = null; 22 | bool? readingAfterCancelling = null; 23 | process.SubscribeLines(c => 24 | { 25 | consoleOut.Add(c); 26 | if (!c.Line.EndsWith("3")) return; 27 | 28 | readingBeforeCancelling = process.IsActivelyReading; 29 | 30 | process.CancelAsyncReads(); 31 | Task.Run(async () => 32 | { 33 | await Task.Delay(TimeSpan.FromSeconds(2)); 34 | 35 | readingAfterCancelling = process.IsActivelyReading; 36 | process.StartAsyncReads(); 37 | 38 | }); 39 | }, e=> seenException = e); 40 | 41 | process.WaitForCompletion(TimeSpan.FromSeconds(20)); 42 | 43 | process.ExitCode.Should().HaveValue().And.Be(121); 44 | seenException.Should().BeNull(); 45 | consoleOut.Should().NotBeEmpty() 46 | .And.Contain(l => l.Line.EndsWith("9")); 47 | 48 | readingBeforeCancelling.Should().HaveValue().And.BeTrue(); 49 | readingAfterCancelling.Should().HaveValue().And.BeFalse(); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/Proc.Tests/ControlCTestCases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using FluentAssertions; 5 | 6 | namespace ProcNet.Tests 7 | { 8 | public class ControlCTestCases : TestsBase 9 | { 10 | [SkipOnNonWindowsFact] public void ControlC() 11 | { 12 | var args = TestCaseArguments(nameof(ControlC)); 13 | args.SendControlCFirst = true; 14 | 15 | var process = new ObservableProcess(args); 16 | var seen = new List(); 17 | process.SubscribeLines(c=> 18 | { 19 | seen.Add(c.Line); 20 | }); 21 | process.WaitForCompletion(TimeSpan.FromSeconds(1)); 22 | 23 | seen.Should().NotBeEmpty().And.HaveCount(2, string.Join(Environment.NewLine, seen)); 24 | seen[0].Should().Be("Written before control+c"); 25 | seen[1].Should().Be("Written after control+c"); 26 | } 27 | 28 | [SkipOnNonWindowsFact] public void ControlCSend() 29 | { 30 | var args = TestCaseArguments(nameof(ControlC)); 31 | args.SendControlCFirst = true; 32 | 33 | var process = new ObservableProcess(args); 34 | var seen = new List(); 35 | process.SubscribeLines(c=> 36 | { 37 | seen.Add(c.Line); 38 | if (c.Line.Contains("before")) process.SendControlC(); 39 | }); 40 | process.WaitForCompletion(TimeSpan.FromSeconds(1)); 41 | 42 | seen.Should().NotBeEmpty().And.HaveCount(2, string.Join(Environment.NewLine, seen)); 43 | seen[0].Should().Be("Written before control+c"); 44 | seen[1].Should().Be("Written after control+c"); 45 | } 46 | 47 | [SkipOnNonWindowsFact] public void ControlCNoWait() 48 | { 49 | var args = TestCaseArguments(nameof(ControlCNoWait)); 50 | args.SendControlCFirst = true; 51 | 52 | var process = new ObservableProcess(args); 53 | var seen = new List(); 54 | process.SubscribeLines(c=> 55 | { 56 | seen.Add(c.Line); 57 | }); 58 | process.WaitForCompletion(TimeSpan.FromSeconds(1)); 59 | 60 | //process exits before its control c handler is invoked 61 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 62 | seen[0].Should().Be("Written before control+c"); 63 | } 64 | 65 | [SkipOnNonWindowsFact] 66 | public void ControlCIngoredByCmd() { 67 | var args = CmdTestCaseArguments("TrulyLongRunning"); 68 | args.SendControlCFirst = true; 69 | args.WaitForExit = TimeSpan.FromSeconds(2); 70 | 71 | var process = new ObservableProcess(args); 72 | process.SubscribeLines(c => { }); 73 | 74 | Action call = () => process.WaitForCompletion(TimeSpan.FromSeconds(1)); 75 | 76 | call.ShouldNotThrow(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Proc.Tests/DisposeTestCases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace ProcNet.Tests 7 | { 8 | public class DisposeTestCases : TestsBase 9 | { 10 | [Fact] 11 | public void DelayedWriterRunsToCompletion() 12 | { 13 | var seen = new List(); 14 | Exception ex = null; 15 | var process = new ObservableProcess(TestCaseArguments(nameof(DelayedWriter))); 16 | var subscription = process.SubscribeLines(c=>seen.Add(c.Line), e => ex = e); 17 | process.WaitForCompletion(WaitTimeout); 18 | 19 | process.ExitCode.Should().Be(20); 20 | ex.Should().BeNull(); 21 | 22 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 23 | seen[0].Should().Be(nameof(DelayedWriter)); 24 | } 25 | [Fact] 26 | public void DelayedWriterStopNoWaitDispose() 27 | { 28 | var seen = new List(); 29 | var args = TestCaseArguments(nameof(DelayedWriter)); 30 | args.WaitForExit = null; //never wait for exit 31 | var process = new ObservableProcess(args); 32 | process.SubscribeLines(c=>seen.Add(c.Line)); 33 | process.Dispose(); 34 | 35 | //disposing the process itself will stop the underlying Process 36 | process.ExitCode.Should().NotHaveValue(); 37 | seen.Should().BeEmpty(string.Join(Environment.NewLine, seen)); 38 | } 39 | [Fact] 40 | public void DelayedWriterStopWaitToShort() 41 | { 42 | var seen = new List(); 43 | var args = TestCaseArguments(nameof(DelayedWriter)); 44 | args.WaitForExit = TimeSpan.FromMilliseconds(200); 45 | var process = new ObservableProcess(args); 46 | process.SubscribeLines(c=>seen.Add(c.Line)); 47 | process.Dispose(); 48 | 49 | process.ExitCode.Should().NotHaveValue(); 50 | seen.Should().BeEmpty(string.Join(Environment.NewLine, seen)); 51 | } 52 | [Fact] 53 | public void DelayedWriter() 54 | { 55 | var seen = new List(); 56 | var process = new ObservableProcess(TestCaseArguments(nameof(DelayedWriter))); 57 | var subscription = process.SubscribeLines(c=>seen.Add(c.Line)); 58 | subscription.Dispose(); 59 | process.WaitForCompletion(WaitTimeout); 60 | 61 | //disposing the subscription did not kill the process 62 | //so we should see the exit code 63 | process.ExitCode.Should().Be(20); 64 | //but because we subscribed instantly we see no output 65 | seen.Should().BeEmpty(string.Join(Environment.NewLine, seen)); 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Proc.Tests/EventBasedObservableProcessTestCases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace ProcNet.Tests 7 | { 8 | public class EventBasedObservableProcessTestCases : TestsBase 9 | { 10 | [Fact] 11 | public void SingleLineNoEnter() 12 | { 13 | var seen = new List(); 14 | var process = new EventBasedObservableProcess(TestCaseArguments(nameof(SingleLineNoEnter))); 15 | process.Subscribe(c=>seen.Add(c.Line)); 16 | process.WaitForCompletion(WaitTimeout); 17 | 18 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 19 | seen[0].Should().Be(nameof(SingleLineNoEnter)); 20 | } 21 | 22 | [Fact] 23 | public void SingleLine() 24 | { 25 | var seen = new List(); 26 | var process = new EventBasedObservableProcess(TestCaseArguments(nameof(SingleLine))); 27 | process.Subscribe(c=>seen.Add(c.Line), e=>throw e); 28 | process.WaitForCompletion(WaitTimeout); 29 | 30 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 31 | seen[0].Should().Be(nameof(SingleLine)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Proc.Tests/LineOutputTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using ProcNet.Std; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace ProcNet.Tests 10 | { 11 | public class CharacterOutputTestCases : TestsBase 12 | { 13 | [Fact] 14 | public void OverwriteLines() 15 | { 16 | var args = TestCaseArguments(nameof(OverwriteLines)); 17 | var result = Proc.Start(args); 18 | } 19 | } 20 | 21 | 22 | public class LineOutputTestCases(ITestOutputHelper output) : TestsBase 23 | { 24 | private static readonly string _expected = @" 25 | Windows IP Configuration 26 | 27 | 28 | Ethernet adapter Ethernet 2: 29 | 30 | Media State . . . . . . . . . . . : Media disconnected 31 | Connection-specific DNS Suffix . : 32 | 33 | Wireless LAN adapter Wi-Fi: 34 | 35 | Media State . . . . . . . . . . . : Media disconnected 36 | Connection-specific DNS Suffix . : 37 | 38 | Ethernet adapter Ethernet 3: 39 | 40 | Connection-specific DNS Suffix . : 41 | Link-local IPv6 Address . . . . . : fe80::e477:59c0:1f38:6cfa%16 42 | IPv4 Address. . . . . . . . . . . : 10.2.20.225 43 | Subnet Mask . . . . . . . . . . . : 255.255.255.0 44 | Default Gateway . . . . . . . . . : 10.2.20.1 45 | 46 | Wireless LAN adapter Local Area Connection* 3: 47 | 48 | Media State . . . . . . . . . . . : Media disconnected 49 | Connection-specific DNS Suffix . : 50 | 51 | Wireless LAN adapter Local Area Connection* 4: 52 | 53 | Media State . . . . . . . . . . . : Media disconnected 54 | Connection-specific DNS Suffix . : 55 | 56 | Ethernet adapter Bluetooth Network Connection: 57 | 58 | Media State . . . . . . . . . . . : Media disconnected 59 | Connection-specific DNS Suffix . :"; 60 | 61 | private readonly string[] _expectedLines = _expected.Replace("\r\n", "\n").Split(new [] {"\n"}, StringSplitOptions.None); 62 | 63 | [Fact] 64 | public void ProcSeesAllLines() 65 | { 66 | var args = TestCaseArguments("MoreText"); 67 | var result = Proc.Start(args); 68 | result.ExitCode.Should().HaveValue(); 69 | result.Completed.Should().BeTrue(); 70 | //result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(_expectedLines.Length); 71 | for (var i = 0; i < result.ConsoleOut.Count; i++) 72 | result.ConsoleOut[i].Line.Should().Be(_expectedLines[i], i.ToString()); 73 | } 74 | 75 | [Fact] 76 | public void ProcSeesAllLinesWithoutConsoleOutWriter() 77 | { 78 | var args = TestCaseArguments("MoreText"); 79 | args.ConsoleOutWriter = null; 80 | var result = Proc.Start(args); 81 | result.ExitCode.Should().HaveValue(); 82 | result.Completed.Should().BeTrue(); 83 | //result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(_expectedLines.Length); 84 | for (var i = 0; i < result.ConsoleOut.Count; i++) 85 | result.ConsoleOut[i].Line.Should().Be(_expectedLines[i], i.ToString()); 86 | } 87 | 88 | [Fact] 89 | public void SubscribeLinesSeesAllLines() 90 | { 91 | var args = TestCaseArguments("MoreText"); 92 | var o = new ObservableProcess(args); 93 | var seen = new List(); 94 | o.SubscribeLines(l => seen.Add(l)); 95 | o.WaitForCompletion(WaitTimeout); 96 | seen.Should().NotBeEmpty().And.HaveCount(_expectedLines.Length, string.Join("\r\n", seen.Select(s=>s.Line))); 97 | for (var i = 0; i < seen.Count; i++) 98 | seen[i].Line.Should().Be(_expectedLines[i], i.ToString()); 99 | } 100 | [Fact] 101 | public void ConsoleWriterSeesAllLines() 102 | { 103 | var writer = new TestConsoleOutWriter(output); 104 | var args = TestCaseArguments("MoreText"); 105 | args.ConsoleOutWriter = writer; 106 | var result = Proc.Start(args); 107 | result.ExitCode.Should().HaveValue(); 108 | result.Completed.Should().BeTrue(); 109 | var lines = writer.Lines; 110 | lines.Should().NotBeEmpty().And.HaveCount(_expectedLines.Length + 1); 111 | for (var i = 0; i < lines.Length - 1; i++) 112 | lines[i].Should().Be(_expectedLines[i], i.ToString()); 113 | } 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/Proc.Tests/LongRunningTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace ProcNet.Tests 9 | { 10 | public class LongRunningTests(ITestOutputHelper output) : TestsBase 11 | { 12 | [Fact] 13 | public async Task LongRunningShouldSeeAllOutput() 14 | { 15 | var args = LongRunningTestCaseArguments("LongRunning"); 16 | args.StartedConfirmationHandler = l => l.Line == "Started!"; 17 | 18 | var outputWriter = new TestConsoleOutWriter(output); 19 | 20 | using (var process = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) 21 | { 22 | process.Running.Should().BeTrue(); 23 | await Task.Delay(TimeSpan.FromSeconds(2)); 24 | process.Running.Should().BeFalse(); 25 | } 26 | 27 | var lines = outputWriter.Lines; 28 | lines.Length.Should().BeGreaterThan(0); 29 | lines.Should().Contain(s => s.StartsWith("Starting up:")); 30 | lines.Should().Contain(s => s == "Started!"); 31 | lines.Should().Contain(s => s.StartsWith("Data after startup:")); 32 | } 33 | 34 | [Fact] 35 | public async Task LongRunningShouldStopBufferingOutputWhenAsked() 36 | { 37 | var args = LongRunningTestCaseArguments("TrulyLongRunning"); 38 | args.StartedConfirmationHandler = l => l.Line == "Started!"; 39 | args.StopBufferingAfterStarted = true; 40 | 41 | var outputWriter = new TestConsoleOutWriter(output); 42 | var sw = Stopwatch.StartNew(); 43 | 44 | using (var process = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) 45 | { 46 | process.Running.Should().BeTrue(); 47 | sw.Elapsed.Should().BeGreaterThan(TimeSpan.FromSeconds(1)); 48 | var lines = outputWriter.Lines; 49 | lines.Length.Should().BeGreaterThan(0); 50 | lines.Should().Contain(s => s.StartsWith("Starting up:")); 51 | lines.Should().Contain(s => s == "Started!"); 52 | lines.Should().NotContain(s => s.StartsWith("Data after startup:")); 53 | await Task.Delay(TimeSpan.FromSeconds(2)); 54 | lines.Should().NotContain(s => s.StartsWith("Data after startup:")); 55 | } 56 | 57 | // we dispose before the program's completion 58 | sw.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(20)); 59 | 60 | } 61 | 62 | [Fact] 63 | public async Task LongRunningWithoutConfirmationHandler() 64 | { 65 | var args = LongRunningTestCaseArguments("LongRunning"); 66 | var outputWriter = new TestConsoleOutWriter(output); 67 | 68 | using (var process = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) 69 | { 70 | process.Running.Should().BeTrue(); 71 | await Task.Delay(TimeSpan.FromSeconds(2)); 72 | } 73 | 74 | var lines = outputWriter.Lines; 75 | lines.Should().Contain(s => s.StartsWith("Starting up:")); 76 | lines.Should().Contain(s => s == "Started!"); 77 | lines.Should().Contain(s => s.StartsWith("Data after startup:")); 78 | lines.Length.Should().BeGreaterThan(0); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Proc.Tests/ObservableProcessTestCases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace ProcNet.Tests 8 | { 9 | public class ObservableProcessTestCases : TestsBase 10 | { 11 | [Fact] 12 | public void SingleLineNoEnter() 13 | { 14 | var seen = new List(); 15 | var process = new ObservableProcess(TestCaseArguments(nameof(SingleLineNoEnter))); 16 | process.SubscribeLines(c=>seen.Add(c.Line)); 17 | process.WaitForCompletion(WaitTimeout); 18 | 19 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 20 | seen[0].Should().Be(nameof(SingleLineNoEnter)); 21 | } 22 | [Fact] 23 | public void TwoWrites() 24 | { 25 | var seen = new List(); 26 | var process = new ObservableProcess(TestCaseArguments(nameof(TwoWrites))); 27 | process.SubscribeLines(c=>seen.Add(c.Line)); 28 | process.WaitForCompletion(WaitTimeout); 29 | 30 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 31 | seen[0].Should().Be($"{nameof(TwoWrites)}{nameof(TwoWrites)}"); 32 | } 33 | 34 | [Fact] 35 | public void SingleLine() 36 | { 37 | var seen = new List(); 38 | var process = new ObservableProcess(TestCaseArguments(nameof(SingleLine))); 39 | process.SubscribeLines(c=>seen.Add(c.Line), e=>throw e); 40 | process.WaitForCompletion(WaitTimeout); 41 | 42 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 43 | seen[0].Should().Be(nameof(SingleLine)); 44 | } 45 | 46 | [Fact] 47 | public void SingleLineNoEnterCharacters() 48 | { 49 | var seen = new List(); 50 | var process = new ObservableProcess(TestCaseArguments(nameof(SingleLineNoEnter))); 51 | process.Subscribe(c=>seen.Add(c.Characters)); 52 | process.WaitForCompletion(WaitTimeout); 53 | 54 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 55 | var chars = nameof(SingleLineNoEnter).ToCharArray(); 56 | seen[0].Should() 57 | .HaveCount(chars.Length) 58 | .And.ContainInOrder(chars); 59 | 60 | } 61 | [Fact] 62 | public void SingleLineCharacters() 63 | { 64 | var seen = new List(); 65 | var process = new ObservableProcess(TestCaseArguments(nameof(SingleLine))); 66 | process.Subscribe(c=>seen.Add(c.Characters)); 67 | process.WaitForCompletion(WaitTimeout); 68 | 69 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 70 | var chars = nameof(SingleLine).ToCharArray() 71 | .Concat(Environment.NewLine.ToCharArray()) 72 | .ToArray(); 73 | seen[0].Should() 74 | .HaveCount(chars.Length) 75 | .And.ContainInOrder(chars); 76 | 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Proc.Tests/PrintArgsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace ProcNet.Tests; 6 | 7 | public class PrintArgsTests(ITestOutputHelper output) : TestsBase 8 | { 9 | [Fact] 10 | public void ProcSendsAllArguments() 11 | { 12 | string[] testArgs = ["hello", "world"]; 13 | AssertOutput(testArgs); 14 | } 15 | 16 | #if NETSTANDARD2_1 17 | [Fact] 18 | public void ArgumentsWithSpaceAreNotSplit() 19 | { 20 | string[] testArgs = ["hello", "world", "this argument has spaces"]; 21 | AssertOutput(testArgs); 22 | } 23 | 24 | [Fact] 25 | public void ArgumentsSeesArgumentsAfterQuoted() 26 | { 27 | string[] testArgs = ["this argument has spaces", "hello", "world"]; 28 | AssertOutput(testArgs); 29 | } 30 | [Fact] 31 | public void EscapedQuotes() 32 | { 33 | string[] testArgs = ["\"this argument has spaces\"", "hello", "world"]; 34 | AssertOutput(testArgs); 35 | } 36 | 37 | #else 38 | [Fact] 39 | public void EscapedQuotes() 40 | { 41 | string[] testArgs = ["\"this argument has spaces\"", "hello", "world"]; 42 | var args = TestCaseArguments("PrintArgs", testArgs); 43 | var outputWriter = new TestConsoleOutWriter(output); 44 | args.ConsoleOutWriter = outputWriter; 45 | var result = Proc.Start(args); 46 | result.ExitCode.Should().Be(0); 47 | result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(testArgs.Length); 48 | } 49 | #endif 50 | 51 | private void AssertOutput(string[] testArgs) 52 | { 53 | var args = TestCaseArguments("PrintArgs", testArgs); 54 | var outputWriter = new TestConsoleOutWriter(output); 55 | args.ConsoleOutWriter = outputWriter; 56 | var result = Proc.Start(args); 57 | result.ExitCode.Should().Be(0); 58 | result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(testArgs.Length); 59 | for (var i = 0; i < result.ConsoleOut.Count; i++) 60 | result.ConsoleOut[i].Line.Should().Be(testArgs[i], i.ToString()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tests/Proc.Tests/Proc.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | Proc.Tests 5 | ProcNet.Tests 6 | false 7 | preview 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Proc.Tests/ProcTestCases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using ProcNet.Std; 5 | using Xunit; 6 | 7 | namespace ProcNet.Tests 8 | { 9 | public class ProcTestCases : TestsBase 10 | { 11 | public class TestConsoleOutWriter : IConsoleOutWriter 12 | { 13 | public Exception SeenException { get; private set; } 14 | 15 | public List Out { get; } = new List(); 16 | 17 | public void Write(Exception e) => SeenException = e; 18 | 19 | public void Write(ConsoleOut consoleOut) => Out.Add(consoleOut); 20 | } 21 | 22 | [Fact] 23 | public void ReadKeyFirst() 24 | { 25 | var args = TestCaseArguments(nameof(ReadKeyFirst)); 26 | args.StandardInputHandler = s => s.Write("y"); 27 | var writer = new TestConsoleOutWriter(); 28 | args.ConsoleOutWriter = writer; 29 | var result = Proc.Start(args); 30 | result.Completed.Should().BeTrue("completed"); 31 | result.ExitCode.Should().HaveValue(); 32 | result.ConsoleOut.Should().NotBeEmpty(); 33 | writer.Out.Should().NotBeEmpty(); 34 | } 35 | 36 | [Fact] 37 | public void BadBinary() 38 | { 39 | Action call = () => Proc.Start("this-does-not-exist.exe"); 40 | var shouldThrow = call.ShouldThrow(); 41 | shouldThrow.And.InnerException.Message.Should().NotBeEmpty(); 42 | shouldThrow.And.Message.Should().Contain("this-does-not-exist.exe"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Proc.Tests/ReadInOrderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace ProcNet.Tests 5 | { 6 | public class ReadInOrderTests : TestsBase 7 | { 8 | [Fact] 9 | public void InterMixedOutAndError() 10 | { 11 | var args = TestCaseArguments(nameof(InterMixedOutAndError)); 12 | var result = Proc.Start(args); 13 | result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(200); 14 | 15 | var interspersed = 0; 16 | bool? previous = null; 17 | foreach (var o in result.ConsoleOut) 18 | { 19 | if (previous.HasValue && previous.Value != o.Error) 20 | interspersed++; 21 | previous = o.Error; 22 | } 23 | 24 | interspersed.Should().BeGreaterOrEqualTo(10); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Proc.Tests/ReadLineTestCases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace ProcNet.Tests 7 | { 8 | public class ReadLineTestCases : TestsBase 9 | { 10 | [Fact] 11 | public void ReadKeyFirst() 12 | { 13 | var seen = new List(); 14 | var process = new ObservableProcess(TestCaseArguments(nameof(ReadKeyFirst))); 15 | process.StandardInputReady += (standardInput) => 16 | { 17 | //this particular program does not output anything and expect user input immediatly 18 | //OnNext on the observable is only called on output so we need to write on the started event 19 | process.StandardInput.Write("y"); 20 | }; 21 | process.SubscribeLines(c=>seen.Add(c.Line)); 22 | process.WaitForCompletion(WaitTimeout); 23 | 24 | seen.Should().NotBeEmpty().And.HaveCount(1, string.Join(Environment.NewLine, seen)); 25 | seen[0].Should().Be($"y{nameof(ReadKeyFirst)}"); 26 | } 27 | [Fact] 28 | public void ReadKeyAfter() 29 | { 30 | var seen = new List(); 31 | var process = new ObservableProcess(TestCaseArguments(nameof(ReadKeyAfter))); 32 | process.Subscribe(c=> 33 | { 34 | var chars = new string(c.Characters); 35 | seen.Add(chars); 36 | if (chars == "input:") process.StandardInput.Write("y"); 37 | }); 38 | process.WaitForCompletion(WaitTimeout); 39 | 40 | seen.Should().NotBeEmpty().And.HaveCount(2, string.Join(Environment.NewLine, seen)); 41 | seen[0].Should().Be($"input:"); 42 | seen[1].Should().Be(nameof(ReadKeyAfter)); 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Proc.Tests/SkipOnNonWindowsFact.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Xunit; 4 | 5 | namespace ProcNet.Tests 6 | { 7 | public sealed class SkipOnNonWindowsFact : FactAttribute 8 | { 9 | public SkipOnNonWindowsFact() 10 | { 11 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; 12 | Skip = "Skipped, this test can only run on windows"; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Proc.Tests/TestConsoleOutWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using ProcNet.Std; 4 | using Xunit.Abstractions; 5 | 6 | public class TestConsoleOutWriter(ITestOutputHelper output) : IConsoleOutWriter 7 | { 8 | private readonly StringBuilder _sb = new(); 9 | public string[] Lines => _sb.ToString().Replace("\r\n", "\n").Split(new [] {"\n"}, StringSplitOptions.None); 10 | public string Text => _sb.ToString(); 11 | private static char[] NewLineChars = Environment.NewLine.ToCharArray(); 12 | 13 | public void Write(Exception e) => throw e; 14 | 15 | public void Write(ConsoleOut consoleOut) 16 | { 17 | consoleOut.CharsOrString(c => _sb.Append(new string(c)), s => _sb.AppendLine(s)); 18 | consoleOut.CharsOrString(c => output.WriteLine(new string(c).TrimEnd(NewLineChars)), s => output.WriteLine(s)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Proc.Tests/TestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace ProcNet.Tests 7 | { 8 | public abstract class TestsBase 9 | { 10 | private static string _procTestBinary = "Proc.Tests.Binary"; 11 | 12 | protected static TimeSpan WaitTimeout { get; } = TimeSpan.FromSeconds(5); 13 | protected static TimeSpan WaitTimeoutDebug { get; } = TimeSpan.FromMinutes(5); 14 | 15 | private static string GetWorkingDir() 16 | { 17 | var directoryInfo = new DirectoryInfo(Directory.GetCurrentDirectory()); 18 | 19 | var root = (directoryInfo.Name == "Proc.Tests" 20 | && directoryInfo.Parent != null 21 | && directoryInfo.Parent.Name == "src") 22 | ? "./.." 23 | : @"../../../.."; 24 | 25 | var binaryFolder = Path.Combine(Path.GetFullPath(root), _procTestBinary); 26 | return binaryFolder; 27 | } 28 | 29 | protected static StartArguments CmdTestCaseArguments(string testcase, params string[] args) { 30 | string[] arguments = ["/C", "dotnet", GetDll(), testcase]; 31 | 32 | return new StartArguments("cmd", arguments.Concat(args)) { 33 | WorkingDirectory = GetWorkingDir(), 34 | Timeout = WaitTimeout 35 | }; 36 | } 37 | 38 | protected static StartArguments TestCaseArguments(string testcase, params string[] args) 39 | { 40 | string[] arguments = [GetDll(), testcase]; 41 | 42 | return new StartArguments("dotnet", arguments.Concat(args)) 43 | { 44 | WorkingDirectory = GetWorkingDir(), 45 | Timeout = WaitTimeout 46 | }; 47 | } 48 | 49 | protected static LongRunningArguments LongRunningTestCaseArguments(string testcase) => 50 | new("dotnet", GetDll(), testcase) 51 | { 52 | WorkingDirectory = GetWorkingDir(), 53 | Timeout = WaitTimeout 54 | }; 55 | 56 | private static string GetDll() 57 | { 58 | var dll = Path.Combine("bin", GetRunningConfiguration(), "net8.0", _procTestBinary + ".dll"); 59 | var fullPath = Path.Combine(GetWorkingDir(), dll); 60 | if (!File.Exists(fullPath)) throw new Exception($"Can not find {fullPath}"); 61 | 62 | return dll; 63 | } 64 | 65 | private static string GetRunningConfiguration() 66 | { 67 | var l = typeof(TestsBase).GetTypeInfo().Assembly.Location; 68 | return new DirectoryInfo(l).Parent?.Parent?.Name; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Proc.Tests/TrailingNewLinesTestCases.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using System.Text; 6 | using FluentAssertions; 7 | using ProcNet.Std; 8 | using Xunit; 9 | 10 | namespace ProcNet.Tests 11 | { 12 | public class TrailingNewLineTestCases : TestsBase 13 | { 14 | private static readonly string _expected = @"Windows IP Configuration 15 | 16 | 17 | "; 18 | private readonly string[] _expectedLines = _expected.Split(Environment.NewLine.ToCharArray()); 19 | 20 | [Fact] 21 | public void ProcSeesAllLines() 22 | { 23 | var args = TestCaseArguments("TrailingLines"); 24 | var result = Proc.Start(args); 25 | result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(_expected.Count(c=>c=='\n')); 26 | for (var i = 0; i < result.ConsoleOut.Count; i++) 27 | result.ConsoleOut[i].Line.Should().Be(_expectedLines[i], i.ToString()); 28 | } 29 | 30 | [Fact] 31 | public void SubscribeLinesSeesAllLines() 32 | { 33 | var args = TestCaseArguments("TrailingLines"); 34 | var o = new ObservableProcess(args); 35 | var seen = new List(); 36 | o.SubscribeLines(l => seen.Add(l)); 37 | o.WaitForCompletion(WaitTimeout); 38 | seen.Should().NotBeEmpty().And.HaveCount(_expected.Count(c=>c=='\n')); 39 | for (var i = 0; i < seen.Count; i++) 40 | seen[i].Line.Should().Be(_expectedLines[i], i.ToString()); 41 | } 42 | [Fact] 43 | public void ConsoleWriterSeesAllLines() 44 | { 45 | var writer = new TestConsoleOutWriter(); 46 | var args = TestCaseArguments("TrailingLines"); 47 | args.ConsoleOutWriter = writer; 48 | var result = Proc.Start(args); 49 | var lines = writer.Lines; 50 | lines.Should().NotBeEmpty().And.HaveCount(_expectedLines.Length); 51 | for (var i = 0; i < lines.Length; i++) 52 | lines[i].Should().Be(_expectedLines[i], i.ToString()); 53 | } 54 | 55 | public class TestConsoleOutWriter : IConsoleOutWriter 56 | { 57 | private readonly StringBuilder _sb = new StringBuilder(); 58 | public string[] Lines => _sb.ToString().Split(Environment.NewLine.ToCharArray()); 59 | public string Text => _sb.ToString(); 60 | 61 | public void Write(Exception e) => throw e; 62 | 63 | public void Write(ConsoleOut consoleOut) => consoleOut.CharsOrString(c=>_sb.Append(new string(c)), s=>_sb.AppendLine(s)); 64 | } 65 | } 66 | } 67 | --------------------------------------------------------------------------------