├── .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 |
--------------------------------------------------------------------------------