diff --git a/apiCount.include.md b/apiCount.include.md index 60540fc5..aa024ac8 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,28 +1,28 @@ -**API count: 941** +**API count: 956** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 914 | -| `net462` | 914 | -| `net47` | 913 | -| `net471` | 912 | -| `net472` | 908 | -| `net48` | 908 | -| `net481` | 908 | -| `netstandard2.0` | 910 | -| `netstandard2.1` | 741 | -| `netcoreapp2.0` | 834 | -| `netcoreapp2.1` | 753 | -| `netcoreapp2.2` | 753 | -| `netcoreapp3.0` | 699 | -| `netcoreapp3.1` | 698 | -| `net5.0` | 570 | -| `net6.0` | 475 | -| `net7.0` | 322 | -| `net8.0` | 204 | -| `net9.0` | 128 | -| `net10.0` | 76 | +| `net461` | 932 | +| `net462` | 932 | +| `net47` | 931 | +| `net471` | 930 | +| `net472` | 926 | +| `net48` | 926 | +| `net481` | 926 | +| `netstandard2.0` | 928 | +| `netstandard2.1` | 759 | +| `netcoreapp2.0` | 852 | +| `netcoreapp2.1` | 771 | +| `netcoreapp2.2` | 771 | +| `netcoreapp3.0` | 717 | +| `netcoreapp3.1` | 716 | +| `net5.0` | 588 | +| `net6.0` | 493 | +| `net7.0` | 340 | +| `net8.0` | 222 | +| `net9.0` | 146 | +| `net10.0` | 94 | | `net11.0` | 57 | -| `uap10.0` | 900 | +| `uap10.0` | 918 | diff --git a/api_list.include.md b/api_list.include.md index 9aa72d2e..4aabfb95 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -751,7 +751,22 @@ #### Process * `void Kill(bool)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.kill?view=net-11.0#system-diagnostics-process-kill(system-boolean)) + * `(byte[] StandardOutput, byte[] StandardError) ReadAllBytes(TimeSpan?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readallbytes?view=net-11.0) + * `Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readallbytesasync?view=net-11.0) + * `IAsyncEnumerable ReadAllLinesAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readalllinesasync?view=net-11.0) + * `(string StandardOutput, string StandardError) ReadAllText(TimeSpan?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readalltext?view=net-11.0) + * `Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readalltextasync?view=net-11.0) * `Task WaitForExitAsync(CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync?view=net-11.0) + * `ProcessExitStatus Run(ProcessStartInfo, TimeSpan?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.run?view=net-11.0#system-diagnostics-process-run(system-diagnostics-processstartinfo-system-nullable((system-timespan)))) + * `ProcessExitStatus Run(string, IList?, TimeSpan?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.run?view=net-11.0#system-diagnostics-process-run(system-string-system-collections-generic-ilist((system-string))-system-nullable((system-timespan)))) + * `ProcessTextOutput RunAndCaptureText(ProcessStartInfo, TimeSpan?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetext?view=net-11.0#system-diagnostics-process-runandcapturetext(system-diagnostics-processstartinfo-system-nullable((system-timespan)))) + * `ProcessTextOutput RunAndCaptureText(string, IList?, TimeSpan?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetext?view=net-11.0#system-diagnostics-process-runandcapturetext(system-string-system-collections-generic-ilist((system-string))-system-nullable((system-timespan)))) + * `Task RunAndCaptureTextAsync(ProcessStartInfo, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetextasync?view=net-11.0#system-diagnostics-process-runandcapturetextasync(system-diagnostics-processstartinfo-system-threading-cancellationtoken)) + * `Task RunAndCaptureTextAsync(string, IList?, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetextasync?view=net-11.0#system-diagnostics-process-runandcapturetextasync(system-string-system-collections-generic-ilist((system-string))-system-threading-cancellationtoken)) + * `Task RunAsync(ProcessStartInfo, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runasync?view=net-11.0#system-diagnostics-process-runasync(system-diagnostics-processstartinfo-system-threading-cancellationtoken)) + * `Task RunAsync(string, IList?, CancellationToken)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runasync?view=net-11.0#system-diagnostics-process-runasync(system-string-system-collections-generic-ilist((system-string))-system-threading-cancellationtoken)) + * `int StartAndForget(ProcessStartInfo)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.startandforget?view=net-11.0#system-diagnostics-process-startandforget(system-diagnostics-processstartinfo)) + * `int StartAndForget(string, IList?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.startandforget?view=net-11.0#system-diagnostics-process-startandforget(system-string-system-collections-generic-ilist((system-string)))) #### PropertyInfo diff --git a/assemblySize.include.md b/assemblySize.include.md index 186d7549..ced75c43 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -2,51 +2,51 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 310.5KB | +302.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netstandard2.1 | 8.5KB | 263.5KB | +255.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net461 | 8.5KB | 309.0KB | +300.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net462 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net47 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net471 | 8.5KB | 311.5KB | +303.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net472 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | -| net48 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | -| net481 | 8.5KB | 310.5KB | +302.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.0 | 9.0KB | 286.5KB | +277.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.1 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB | -| netcoreapp2.2 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB | -| netcoreapp3.0 | 9.5KB | 257.5KB | +248.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 255.5KB | +246.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| net5.0 | 9.5KB | 218.5KB | +209.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB | -| net6.0 | 10.0KB | 157.0KB | +147.0KB | +12.5KB | +10.0KB | +1.0KB | +4.0KB | -| net7.0 | 10.0KB | 122.0KB | +112.0KB | +9.0KB | +5.5KB | +512bytes | +4.0KB | -| net8.0 | 9.5KB | 94.0KB | +84.5KB | +8.5KB | | +512bytes | +3.5KB | -| net9.0 | 9.5KB | 47.5KB | +38.0KB | +9.0KB | | +512bytes | +3.5KB | -| net10.0 | 10.0KB | 24.0KB | +14.0KB | +9.0KB | | +512bytes | +3.5KB | -| net11.0 | 10.0KB | 18.5KB | +8.5KB | +9.0KB | | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 325.0KB | +317.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netstandard2.1 | 8.5KB | 278.5KB | +270.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 323.5KB | +315.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net462 | 7.0KB | 327.5KB | +320.5KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | +| net47 | 7.0KB | 327.0KB | +320.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net471 | 8.5KB | 326.0KB | +317.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net472 | 8.5KB | 325.0KB | +316.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net48 | 8.5KB | 325.0KB | +316.5KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| net481 | 8.5KB | 325.0KB | +316.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 301.0KB | +292.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp2.1 | 9.0KB | 281.0KB | +272.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 281.0KB | +272.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 272.5KB | +263.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp3.1 | 9.5KB | 270.5KB | +261.0KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | +| net5.0 | 9.5KB | 234.0KB | +224.5KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB | +| net6.0 | 10.0KB | 175.0KB | +165.0KB | +10.0KB | +7.0KB | +512bytes | +4.0KB | +| net7.0 | 10.0KB | 137.5KB | +127.5KB | +9.0KB | +5.5KB | +512bytes | +3.5KB | +| net8.0 | 9.5KB | 109.5KB | +100.0KB | +8.5KB | | +512bytes | +3.5KB | +| net9.0 | 9.5KB | 65.5KB | +56.0KB | +9.0KB | | +1.0KB | +3.5KB | +| net10.0 | 10.0KB | 42.0KB | +32.0KB | +9.0KB | | +512bytes | +3.5KB | +| net11.0 | 10.0KB | 19.0KB | +9.0KB | +9.0KB | | +512bytes | +3.5KB | ### Assembly Sizes with EmbedUntrackedSources | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 453.3KB | +445.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netstandard2.1 | 8.5KB | 381.2KB | +372.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net461 | 8.5KB | 452.9KB | +444.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net462 | 7.0KB | 456.4KB | +449.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net47 | 7.0KB | 456.1KB | +449.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net471 | 8.5KB | 454.8KB | +446.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net472 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | -| net48 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | -| net481 | 8.5KB | 452.7KB | +444.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.0 | 9.0KB | 419.5KB | +410.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.1 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB | -| netcoreapp2.2 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB | -| netcoreapp3.0 | 9.5KB | 370.1KB | +360.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 368.1KB | +358.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net5.0 | 9.5KB | 313.0KB | +303.5KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | -| net6.0 | 10.0KB | 231.9KB | +221.9KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB | -| net7.0 | 10.0KB | 178.3KB | +168.3KB | +16.6KB | +6.9KB | +1.1KB | +4.7KB | -| net8.0 | 9.5KB | 135.4KB | +125.9KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | -| net9.0 | 9.5KB | 68.6KB | +59.1KB | +16.5KB | | +1.1KB | +4.2KB | -| net10.0 | 10.0KB | 36.5KB | +26.5KB | +16.5KB | | +1.1KB | +4.2KB | -| net11.0 | 10.0KB | 27.4KB | +17.4KB | +16.5KB | | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 471.8KB | +463.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netstandard2.1 | 8.5KB | 400.1KB | +391.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 471.3KB | +462.8KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net462 | 7.0KB | 475.3KB | +468.3KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | +| net47 | 7.0KB | 474.6KB | +467.6KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net471 | 8.5KB | 473.2KB | +464.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net472 | 8.5KB | 471.2KB | +462.7KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net48 | 8.5KB | 471.2KB | +462.7KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| net481 | 8.5KB | 471.2KB | +462.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 438.0KB | +429.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp2.1 | 9.0KB | 406.8KB | +397.8KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 406.8KB | +397.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 389.0KB | +379.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp3.1 | 9.5KB | 387.0KB | +377.5KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | +| net5.0 | 9.5KB | 332.7KB | +323.2KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | +| net6.0 | 10.0KB | 253.7KB | +243.7KB | +17.7KB | +8.7KB | +1.1KB | +4.7KB | +| net7.0 | 10.0KB | 197.6KB | +187.6KB | +16.6KB | +6.9KB | +1.1KB | +4.2KB | +| net8.0 | 9.5KB | 154.8KB | +145.3KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | +| net9.0 | 9.5KB | 90.4KB | +80.9KB | +16.5KB | | +1.6KB | +4.2KB | +| net10.0 | 10.0KB | 58.4KB | +48.4KB | +16.5KB | | +1.1KB | +4.2KB | +| net11.0 | 10.0KB | 28.0KB | +18.0KB | +16.5KB | | +1.1KB | +4.2KB | diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index d52c1d9a..54afd335 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -1175,6 +1175,63 @@ async Task Process_Methods() var process = new Process(); await process.WaitForExitAsync(); process.Kill(true); + +#if FeatureValueTuple + (string outText, string errText) = process.ReadAllText(); + (outText, errText) = process.ReadAllText(TimeSpan.FromSeconds(1)); + (outText, errText) = await process.ReadAllTextAsync(); + (outText, errText) = await process.ReadAllTextAsync(CancellationToken.None); + + (byte[] outBytes, byte[] errBytes) = process.ReadAllBytes(); + (outBytes, errBytes) = process.ReadAllBytes(TimeSpan.FromSeconds(1)); + (outBytes, errBytes) = await process.ReadAllBytesAsync(); + (outBytes, errBytes) = await process.ReadAllBytesAsync(CancellationToken.None); +#endif + +#if FeatureAsyncInterfaces + await foreach (var line in process.ReadAllLinesAsync(CancellationToken.None)) + { + _ = line.Content; + _ = line.StandardError; + } +#endif + + ProcessExitStatus status = Process.Run("notexists"); + status = Process.Run("notexists", new[] { "a", "b" }); + status = Process.Run("notexists", new[] { "a", "b" }, TimeSpan.FromSeconds(1)); + status = Process.Run(new ProcessStartInfo("notexists")); + status = Process.Run(new ProcessStartInfo("notexists"), TimeSpan.FromSeconds(1)); + _ = status.Canceled; + _ = status.ExitCode; + _ = status.Signal; + + status = await Process.RunAsync("notexists"); + status = await Process.RunAsync("notexists", new[] { "a", "b" }); + status = await Process.RunAsync("notexists", new[] { "a", "b" }, CancellationToken.None); + status = await Process.RunAsync(new ProcessStartInfo("notexists")); + status = await Process.RunAsync(new ProcessStartInfo("notexists"), CancellationToken.None); + + ProcessTextOutput output = Process.RunAndCaptureText("notexists"); + output = Process.RunAndCaptureText("notexists", new[] { "a", "b" }); + output = Process.RunAndCaptureText("notexists", new[] { "a", "b" }, TimeSpan.FromSeconds(1)); + output = Process.RunAndCaptureText(new ProcessStartInfo("notexists")); + _ = output.ExitStatus; + _ = output.ProcessId; + _ = output.StandardError; + _ = output.StandardOutput; + + output = await Process.RunAndCaptureTextAsync("notexists"); + output = await Process.RunAndCaptureTextAsync("notexists", new[] { "a", "b" }); + output = await Process.RunAndCaptureTextAsync("notexists", new[] { "a", "b" }, CancellationToken.None); + output = await Process.RunAndCaptureTextAsync(new ProcessStartInfo("notexists")); + + int pid = Process.StartAndForget("notexists"); + pid = Process.StartAndForget("notexists", new[] { "a", "b" }); + pid = Process.StartAndForget(new ProcessStartInfo("notexists")); + + ProcessOutputLine outputLine = new("text", standardError: false); + _ = outputLine.Content; + _ = outputLine.StandardError; } void ProcessStartInfo_Methods() diff --git a/src/Polyfill/Polyfill_Process.cs b/src/Polyfill/Polyfill_Process.cs index c6dde200..f165b92f 100644 --- a/src/Polyfill/Polyfill_Process.cs +++ b/src/Polyfill/Polyfill_Process.cs @@ -1,8 +1,12 @@ namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -32,9 +36,11 @@ public static void Kill(this Process target, bool entireProcessTree) => //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexitasync?view=net-11.0 public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try @@ -67,5 +73,162 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken } } #endif -} +#if !NET11_0_OR_GREATER + +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readalltext?view=net-11.0 + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readalltextasync?view=net-11.0 + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readallbytes?view=net-11.0 + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readallbytesasync?view=net-11.0 + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif + +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.readalllinesasync?view=net-11.0 + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } +#endif +} diff --git a/src/Polyfill/PosixSignal.cs b/src/Polyfill/PosixSignal.cs new file mode 100644 index 00000000..75153b45 --- /dev/null +++ b/src/Polyfill/PosixSignal.cs @@ -0,0 +1,41 @@ +#if !NET6_0_OR_GREATER + +namespace System.Runtime.InteropServices; + +/// +/// Specifies a POSIX signal number. +/// +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignal?view=net-11.0 +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} + +#else +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.InteropServices.PosixSignal))] +#endif diff --git a/src/Polyfill/ProcessExitStatus.cs b/src/Polyfill/ProcessExitStatus.cs new file mode 100644 index 00000000..d3f5f3c6 --- /dev/null +++ b/src/Polyfill/ProcessExitStatus.cs @@ -0,0 +1,50 @@ +#if !NET11_0_OR_GREATER + +namespace System.Diagnostics; + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +/// +/// Represents the exit status of a process. +/// +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processexitstatus?view=net-11.0 +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} + +#else +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Diagnostics.ProcessExitStatus))] +#endif diff --git a/src/Polyfill/ProcessOutputLine.cs b/src/Polyfill/ProcessOutputLine.cs new file mode 100644 index 00000000..10b4c950 --- /dev/null +++ b/src/Polyfill/ProcessOutputLine.cs @@ -0,0 +1,43 @@ +#if !NET11_0_OR_GREATER + +namespace System.Diagnostics; + +using System.Diagnostics.CodeAnalysis; + +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processoutputline?view=net-11.0 +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + + /// + /// Gets the content of the output line. + /// + public string Content { get; } + + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} + +#else +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Diagnostics.ProcessOutputLine))] +#endif diff --git a/src/Polyfill/ProcessPolyfill.cs b/src/Polyfill/ProcessPolyfill.cs new file mode 100644 index 00000000..ef79385f --- /dev/null +++ b/src/Polyfill/ProcessPolyfill.cs @@ -0,0 +1,233 @@ +#if !NET11_0_OR_GREATER + +namespace Polyfills; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; + +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.run?view=net-11.0#system-diagnostics-process-run(system-diagnostics-processstartinfo-system-nullable((system-timespan))) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.run?view=net-11.0#system-diagnostics-process-run(system-string-system-collections-generic-ilist((system-string))-system-nullable((system-timespan))) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runasync?view=net-11.0#system-diagnostics-process-runasync(system-diagnostics-processstartinfo-system-threading-cancellationtoken) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runasync?view=net-11.0#system-diagnostics-process-runasync(system-string-system-collections-generic-ilist((system-string))-system-threading-cancellationtoken) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetext?view=net-11.0#system-diagnostics-process-runandcapturetext(system-diagnostics-processstartinfo-system-nullable((system-timespan))) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetext?view=net-11.0#system-diagnostics-process-runandcapturetext(system-string-system-collections-generic-ilist((system-string))-system-nullable((system-timespan))) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetextasync?view=net-11.0#system-diagnostics-process-runandcapturetextasync(system-diagnostics-processstartinfo-system-threading-cancellationtoken) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.runandcapturetextasync?view=net-11.0#system-diagnostics-process-runandcapturetextasync(system-string-system-collections-generic-ilist((system-string))-system-threading-cancellationtoken) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.startandforget?view=net-11.0#system-diagnostics-process-startandforget(system-diagnostics-processstartinfo) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.startandforget?view=net-11.0#system-diagnostics-process-startandforget(system-string-system-collections-generic-ilist((system-string))) + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} + +#endif diff --git a/src/Polyfill/ProcessTextOutput.cs b/src/Polyfill/ProcessTextOutput.cs new file mode 100644 index 00000000..224a5b41 --- /dev/null +++ b/src/Polyfill/ProcessTextOutput.cs @@ -0,0 +1,55 @@ +#if !NET11_0_OR_GREATER + +namespace System.Diagnostics; + +using System.Diagnostics.CodeAnalysis; + +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processtextoutput?view=net-11.0 +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} + +#else +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Diagnostics.ProcessTextOutput))] +#endif diff --git a/src/Split/net10.0/Polyfill_Process.cs b/src/Split/net10.0/Polyfill_Process.cs new file mode 100644 index 00000000..3752a4ee --- /dev/null +++ b/src/Split/net10.0/Polyfill_Process.cs @@ -0,0 +1,156 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } +} diff --git a/src/Split/net10.0/ProcessExitStatus.cs b/src/Split/net10.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net10.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net10.0/ProcessOutputLine.cs b/src/Split/net10.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net10.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net10.0/ProcessPolyfill.cs b/src/Split/net10.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net10.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net10.0/ProcessTextOutput.cs b/src/Split/net10.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net10.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net10.0/TypeForwardeds.cs b/src/Split/net10.0/TypeForwardeds.cs index b17d055b..88bafe15 100644 --- a/src/Split/net10.0/TypeForwardeds.cs +++ b/src/Split/net10.0/TypeForwardeds.cs @@ -22,6 +22,7 @@ [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ParamCollectionAttribute))] +[assembly: TypeForwardedTo(typeof(System.Runtime.InteropServices.PosixSignal))] [assembly: TypeForwardedTo(typeof(System.Buffers.ReadOnlySpanAction<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.Versioning.RequiresPreviewFeaturesAttribute))] diff --git a/src/Split/net11.0/TypeForwardeds.cs b/src/Split/net11.0/TypeForwardeds.cs index b17d055b..6c10ba66 100644 --- a/src/Split/net11.0/TypeForwardeds.cs +++ b/src/Split/net11.0/TypeForwardeds.cs @@ -22,6 +22,10 @@ [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ParamCollectionAttribute))] +[assembly: TypeForwardedTo(typeof(System.Runtime.InteropServices.PosixSignal))] +[assembly: TypeForwardedTo(typeof(System.Diagnostics.ProcessExitStatus))] +[assembly: TypeForwardedTo(typeof(System.Diagnostics.ProcessOutputLine))] +[assembly: TypeForwardedTo(typeof(System.Diagnostics.ProcessTextOutput))] [assembly: TypeForwardedTo(typeof(System.Buffers.ReadOnlySpanAction<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.Versioning.RequiresPreviewFeaturesAttribute))] diff --git a/src/Split/net461/Polyfill_Process.cs b/src/Split/net461/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/net461/Polyfill_Process.cs +++ b/src/Split/net461/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/net461/PosixSignal.cs b/src/Split/net461/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net461/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net461/ProcessExitStatus.cs b/src/Split/net461/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net461/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net461/ProcessOutputLine.cs b/src/Split/net461/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net461/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net461/ProcessPolyfill.cs b/src/Split/net461/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net461/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net461/ProcessTextOutput.cs b/src/Split/net461/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net461/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net462/Polyfill_Process.cs b/src/Split/net462/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/net462/Polyfill_Process.cs +++ b/src/Split/net462/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/net462/PosixSignal.cs b/src/Split/net462/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net462/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net462/ProcessExitStatus.cs b/src/Split/net462/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net462/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net462/ProcessOutputLine.cs b/src/Split/net462/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net462/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net462/ProcessPolyfill.cs b/src/Split/net462/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net462/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net462/ProcessTextOutput.cs b/src/Split/net462/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net462/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net47/Polyfill_Process.cs b/src/Split/net47/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/net47/Polyfill_Process.cs +++ b/src/Split/net47/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/net47/PosixSignal.cs b/src/Split/net47/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net47/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net47/ProcessExitStatus.cs b/src/Split/net47/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net47/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net47/ProcessOutputLine.cs b/src/Split/net47/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net47/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net47/ProcessPolyfill.cs b/src/Split/net47/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net47/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net47/ProcessTextOutput.cs b/src/Split/net47/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net47/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net471/Polyfill_Process.cs b/src/Split/net471/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/net471/Polyfill_Process.cs +++ b/src/Split/net471/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/net471/PosixSignal.cs b/src/Split/net471/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net471/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net471/ProcessExitStatus.cs b/src/Split/net471/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net471/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net471/ProcessOutputLine.cs b/src/Split/net471/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net471/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net471/ProcessPolyfill.cs b/src/Split/net471/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net471/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net471/ProcessTextOutput.cs b/src/Split/net471/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net471/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net472/Polyfill_Process.cs b/src/Split/net472/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/net472/Polyfill_Process.cs +++ b/src/Split/net472/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/net472/PosixSignal.cs b/src/Split/net472/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net472/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net472/ProcessExitStatus.cs b/src/Split/net472/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net472/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net472/ProcessOutputLine.cs b/src/Split/net472/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net472/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net472/ProcessPolyfill.cs b/src/Split/net472/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net472/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net472/ProcessTextOutput.cs b/src/Split/net472/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net472/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net48/Polyfill_Process.cs b/src/Split/net48/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/net48/Polyfill_Process.cs +++ b/src/Split/net48/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/net48/PosixSignal.cs b/src/Split/net48/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net48/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net48/ProcessExitStatus.cs b/src/Split/net48/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net48/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net48/ProcessOutputLine.cs b/src/Split/net48/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net48/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net48/ProcessPolyfill.cs b/src/Split/net48/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net48/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net48/ProcessTextOutput.cs b/src/Split/net48/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net48/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net481/Polyfill_Process.cs b/src/Split/net481/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/net481/Polyfill_Process.cs +++ b/src/Split/net481/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/net481/PosixSignal.cs b/src/Split/net481/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net481/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net481/ProcessExitStatus.cs b/src/Split/net481/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net481/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net481/ProcessOutputLine.cs b/src/Split/net481/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net481/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net481/ProcessPolyfill.cs b/src/Split/net481/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net481/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net481/ProcessTextOutput.cs b/src/Split/net481/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net481/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net5.0/Polyfill_Process.cs b/src/Split/net5.0/Polyfill_Process.cs new file mode 100644 index 00000000..3752a4ee --- /dev/null +++ b/src/Split/net5.0/Polyfill_Process.cs @@ -0,0 +1,156 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } +} diff --git a/src/Split/net5.0/PosixSignal.cs b/src/Split/net5.0/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/net5.0/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/net5.0/ProcessExitStatus.cs b/src/Split/net5.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net5.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net5.0/ProcessOutputLine.cs b/src/Split/net5.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net5.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net5.0/ProcessPolyfill.cs b/src/Split/net5.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net5.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net5.0/ProcessTextOutput.cs b/src/Split/net5.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net5.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net6.0/Polyfill_Process.cs b/src/Split/net6.0/Polyfill_Process.cs new file mode 100644 index 00000000..3752a4ee --- /dev/null +++ b/src/Split/net6.0/Polyfill_Process.cs @@ -0,0 +1,156 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } +} diff --git a/src/Split/net6.0/ProcessExitStatus.cs b/src/Split/net6.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net6.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net6.0/ProcessOutputLine.cs b/src/Split/net6.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net6.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net6.0/ProcessPolyfill.cs b/src/Split/net6.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net6.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net6.0/ProcessTextOutput.cs b/src/Split/net6.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net6.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net6.0/TypeForwardeds.cs b/src/Split/net6.0/TypeForwardeds.cs index 13407190..9dc22686 100644 --- a/src/Split/net6.0/TypeForwardeds.cs +++ b/src/Split/net6.0/TypeForwardeds.cs @@ -8,6 +8,7 @@ [assembly: TypeForwardedTo(typeof(System.IO.MatchCasing))] [assembly: TypeForwardedTo(typeof(System.IO.MatchType))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute))] +[assembly: TypeForwardedTo(typeof(System.Runtime.InteropServices.PosixSignal))] [assembly: TypeForwardedTo(typeof(System.Buffers.ReadOnlySpanAction<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.Versioning.RequiresPreviewFeaturesAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.SkipLocalsInitAttribute))] diff --git a/src/Split/net7.0/Polyfill_Process.cs b/src/Split/net7.0/Polyfill_Process.cs new file mode 100644 index 00000000..3752a4ee --- /dev/null +++ b/src/Split/net7.0/Polyfill_Process.cs @@ -0,0 +1,156 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } +} diff --git a/src/Split/net7.0/ProcessExitStatus.cs b/src/Split/net7.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net7.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net7.0/ProcessOutputLine.cs b/src/Split/net7.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net7.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net7.0/ProcessPolyfill.cs b/src/Split/net7.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net7.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net7.0/ProcessTextOutput.cs b/src/Split/net7.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net7.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net7.0/TypeForwardeds.cs b/src/Split/net7.0/TypeForwardeds.cs index ceb68fe9..4deb010f 100644 --- a/src/Split/net7.0/TypeForwardeds.cs +++ b/src/Split/net7.0/TypeForwardeds.cs @@ -11,6 +11,7 @@ [assembly: TypeForwardedTo(typeof(System.IO.MatchCasing))] [assembly: TypeForwardedTo(typeof(System.IO.MatchType))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute))] +[assembly: TypeForwardedTo(typeof(System.Runtime.InteropServices.PosixSignal))] [assembly: TypeForwardedTo(typeof(System.Buffers.ReadOnlySpanAction<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.Versioning.RequiresPreviewFeaturesAttribute))] diff --git a/src/Split/net8.0/Polyfill_Process.cs b/src/Split/net8.0/Polyfill_Process.cs new file mode 100644 index 00000000..3752a4ee --- /dev/null +++ b/src/Split/net8.0/Polyfill_Process.cs @@ -0,0 +1,156 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } +} diff --git a/src/Split/net8.0/ProcessExitStatus.cs b/src/Split/net8.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net8.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net8.0/ProcessOutputLine.cs b/src/Split/net8.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net8.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net8.0/ProcessPolyfill.cs b/src/Split/net8.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net8.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net8.0/ProcessTextOutput.cs b/src/Split/net8.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net8.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net8.0/TypeForwardeds.cs b/src/Split/net8.0/TypeForwardeds.cs index b849a037..1f278cd1 100644 --- a/src/Split/net8.0/TypeForwardeds.cs +++ b/src/Split/net8.0/TypeForwardeds.cs @@ -14,6 +14,7 @@ [assembly: TypeForwardedTo(typeof(System.IO.MatchCasing))] [assembly: TypeForwardedTo(typeof(System.IO.MatchType))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute))] +[assembly: TypeForwardedTo(typeof(System.Runtime.InteropServices.PosixSignal))] [assembly: TypeForwardedTo(typeof(System.Buffers.ReadOnlySpanAction<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.Versioning.RequiresPreviewFeaturesAttribute))] diff --git a/src/Split/net9.0/Polyfill_Process.cs b/src/Split/net9.0/Polyfill_Process.cs new file mode 100644 index 00000000..3752a4ee --- /dev/null +++ b/src/Split/net9.0/Polyfill_Process.cs @@ -0,0 +1,156 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } +} diff --git a/src/Split/net9.0/ProcessExitStatus.cs b/src/Split/net9.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/net9.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/net9.0/ProcessOutputLine.cs b/src/Split/net9.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/net9.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/net9.0/ProcessPolyfill.cs b/src/Split/net9.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/net9.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/net9.0/ProcessTextOutput.cs b/src/Split/net9.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/net9.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/net9.0/TypeForwardeds.cs b/src/Split/net9.0/TypeForwardeds.cs index b17d055b..88bafe15 100644 --- a/src/Split/net9.0/TypeForwardeds.cs +++ b/src/Split/net9.0/TypeForwardeds.cs @@ -22,6 +22,7 @@ [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ModuleInitializerAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.ParamCollectionAttribute))] +[assembly: TypeForwardedTo(typeof(System.Runtime.InteropServices.PosixSignal))] [assembly: TypeForwardedTo(typeof(System.Buffers.ReadOnlySpanAction<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))] [assembly: TypeForwardedTo(typeof(System.Runtime.Versioning.RequiresPreviewFeaturesAttribute))] diff --git a/src/Split/netcoreapp2.0/Polyfill_Process.cs b/src/Split/netcoreapp2.0/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/netcoreapp2.0/Polyfill_Process.cs +++ b/src/Split/netcoreapp2.0/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/netcoreapp2.0/PosixSignal.cs b/src/Split/netcoreapp2.0/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/netcoreapp2.0/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/netcoreapp2.0/ProcessExitStatus.cs b/src/Split/netcoreapp2.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/netcoreapp2.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/netcoreapp2.0/ProcessOutputLine.cs b/src/Split/netcoreapp2.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/netcoreapp2.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/netcoreapp2.0/ProcessPolyfill.cs b/src/Split/netcoreapp2.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/netcoreapp2.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/netcoreapp2.0/ProcessTextOutput.cs b/src/Split/netcoreapp2.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/netcoreapp2.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/netcoreapp2.1/Polyfill_Process.cs b/src/Split/netcoreapp2.1/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/netcoreapp2.1/Polyfill_Process.cs +++ b/src/Split/netcoreapp2.1/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/netcoreapp2.1/PosixSignal.cs b/src/Split/netcoreapp2.1/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/netcoreapp2.1/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/netcoreapp2.1/ProcessExitStatus.cs b/src/Split/netcoreapp2.1/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/netcoreapp2.1/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/netcoreapp2.1/ProcessOutputLine.cs b/src/Split/netcoreapp2.1/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/netcoreapp2.1/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/netcoreapp2.1/ProcessPolyfill.cs b/src/Split/netcoreapp2.1/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/netcoreapp2.1/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/netcoreapp2.1/ProcessTextOutput.cs b/src/Split/netcoreapp2.1/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/netcoreapp2.1/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/netcoreapp2.2/Polyfill_Process.cs b/src/Split/netcoreapp2.2/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/netcoreapp2.2/Polyfill_Process.cs +++ b/src/Split/netcoreapp2.2/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/netcoreapp2.2/PosixSignal.cs b/src/Split/netcoreapp2.2/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/netcoreapp2.2/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/netcoreapp2.2/ProcessExitStatus.cs b/src/Split/netcoreapp2.2/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/netcoreapp2.2/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/netcoreapp2.2/ProcessOutputLine.cs b/src/Split/netcoreapp2.2/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/netcoreapp2.2/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/netcoreapp2.2/ProcessPolyfill.cs b/src/Split/netcoreapp2.2/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/netcoreapp2.2/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/netcoreapp2.2/ProcessTextOutput.cs b/src/Split/netcoreapp2.2/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/netcoreapp2.2/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/netcoreapp3.0/Polyfill_Process.cs b/src/Split/netcoreapp3.0/Polyfill_Process.cs index 2b7c36ba..e6b59ed8 100644 --- a/src/Split/netcoreapp3.0/Polyfill_Process.cs +++ b/src/Split/netcoreapp3.0/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -14,9 +18,10 @@ static partial class Polyfill /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -44,4 +49,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/netcoreapp3.0/PosixSignal.cs b/src/Split/netcoreapp3.0/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/netcoreapp3.0/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/netcoreapp3.0/ProcessExitStatus.cs b/src/Split/netcoreapp3.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/netcoreapp3.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/netcoreapp3.0/ProcessOutputLine.cs b/src/Split/netcoreapp3.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/netcoreapp3.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/netcoreapp3.0/ProcessPolyfill.cs b/src/Split/netcoreapp3.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/netcoreapp3.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/netcoreapp3.0/ProcessTextOutput.cs b/src/Split/netcoreapp3.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/netcoreapp3.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/netcoreapp3.1/Polyfill_Process.cs b/src/Split/netcoreapp3.1/Polyfill_Process.cs index 2b7c36ba..e6b59ed8 100644 --- a/src/Split/netcoreapp3.1/Polyfill_Process.cs +++ b/src/Split/netcoreapp3.1/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -14,9 +18,10 @@ static partial class Polyfill /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -44,4 +49,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/netcoreapp3.1/PosixSignal.cs b/src/Split/netcoreapp3.1/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/netcoreapp3.1/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/netcoreapp3.1/ProcessExitStatus.cs b/src/Split/netcoreapp3.1/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/netcoreapp3.1/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/netcoreapp3.1/ProcessOutputLine.cs b/src/Split/netcoreapp3.1/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/netcoreapp3.1/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/netcoreapp3.1/ProcessPolyfill.cs b/src/Split/netcoreapp3.1/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/netcoreapp3.1/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/netcoreapp3.1/ProcessTextOutput.cs b/src/Split/netcoreapp3.1/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/netcoreapp3.1/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/netstandard2.0/Polyfill_Process.cs b/src/Split/netstandard2.0/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/netstandard2.0/Polyfill_Process.cs +++ b/src/Split/netstandard2.0/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/netstandard2.0/PosixSignal.cs b/src/Split/netstandard2.0/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/netstandard2.0/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/netstandard2.0/ProcessExitStatus.cs b/src/Split/netstandard2.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/netstandard2.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/netstandard2.0/ProcessOutputLine.cs b/src/Split/netstandard2.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/netstandard2.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/netstandard2.0/ProcessPolyfill.cs b/src/Split/netstandard2.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/netstandard2.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/netstandard2.0/ProcessTextOutput.cs b/src/Split/netstandard2.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/netstandard2.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/netstandard2.1/Polyfill_Process.cs b/src/Split/netstandard2.1/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/netstandard2.1/Polyfill_Process.cs +++ b/src/Split/netstandard2.1/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/netstandard2.1/PosixSignal.cs b/src/Split/netstandard2.1/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/netstandard2.1/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/netstandard2.1/ProcessExitStatus.cs b/src/Split/netstandard2.1/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/netstandard2.1/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/netstandard2.1/ProcessOutputLine.cs b/src/Split/netstandard2.1/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/netstandard2.1/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/netstandard2.1/ProcessPolyfill.cs b/src/Split/netstandard2.1/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/netstandard2.1/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/netstandard2.1/ProcessTextOutput.cs b/src/Split/netstandard2.1/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/netstandard2.1/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Split/uap10.0/Polyfill_Process.cs b/src/Split/uap10.0/Polyfill_Process.cs index f10aefcd..f52330f9 100644 --- a/src/Split/uap10.0/Polyfill_Process.cs +++ b/src/Split/uap10.0/Polyfill_Process.cs @@ -2,8 +2,12 @@ #pragma warning disable namespace Polyfills; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.Versioning; +using System.Text; using System.Threading; using System.Threading.Tasks; static partial class Polyfill @@ -23,9 +27,10 @@ public static void Kill(this Process target, bool entireProcessTree) => /// public static async Task WaitForExitAsync(this Process target, CancellationToken cancellationToken = default) { - if (!target.HasExited) + cancellationToken.ThrowIfCancellationRequested(); + if (target.HasExited) { - cancellationToken.ThrowIfCancellationRequested(); + return; } try { @@ -53,4 +58,145 @@ public static async Task WaitForExitAsync(this Process target, CancellationToken target.Exited -= handler; } } +#if FeatureValueTuple + /// + /// Reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static (string StandardOutput, string StandardError) ReadAllText(this Process target, TimeSpan? timeout = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + WaitForExitOrThrow(target, timeout); + return (stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as text, waiting for the process to exit. + /// + public static async Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(this Process target, CancellationToken cancellationToken = default) + { + var stdoutTask = target.StandardOutput.ReadToEndAsync(); + var stderrTask = target.StandardError.ReadToEndAsync(); + await target.WaitForExitAsync(cancellationToken); + return (await stdoutTask, await stderrTask); + } + /// + /// Reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(this Process target, TimeSpan? timeout = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs); + WaitForExitOrThrow(target, timeout); + Task.WaitAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } + /// + /// Asynchronously reads the standard output and standard error of the process as bytes, waiting for the process to exit. + /// + public static async Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(this Process target, CancellationToken cancellationToken = default) + { + var outMs = new MemoryStream(); + var errMs = new MemoryStream(); + var outTask = target.StandardOutput.BaseStream.CopyToAsync(outMs, 81920, cancellationToken); + var errTask = target.StandardError.BaseStream.CopyToAsync(errMs, 81920, cancellationToken); + await target.WaitForExitAsync(cancellationToken); + await Task.WhenAll(outTask, errTask); + return (outMs.ToArray(), errMs.ToArray()); + } +#endif +#if FeatureAsyncInterfaces + /// + /// Asynchronously reads the standard output and standard error of the process line-by-line, waiting for the process to exit. + /// + public static async IAsyncEnumerable ReadAllLinesAsync( + this Process target, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var signal = new SemaphoreSlim(0); + var sync = new object(); + var stdoutDone = false; + var stderrDone = false; + DataReceivedEventHandler outHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stdoutDone = true; + } + else + { + queue.Enqueue(new(e.Data, false)); + } + signal.Release(); + }; + DataReceivedEventHandler errHandler = (_, e) => + { + if (e.Data is null) + { + lock (sync) stderrDone = true; + } + else + { + queue.Enqueue(new(e.Data, true)); + } + signal.Release(); + }; + target.OutputDataReceived += outHandler; + target.ErrorDataReceived += errHandler; + target.BeginOutputReadLine(); + target.BeginErrorReadLine(); + try + { + while (true) + { + await signal.WaitAsync(cancellationToken); + while (queue.TryDequeue(out var line)) + { + yield return line; + } + bool done; + lock (sync) + { + done = stdoutDone && stderrDone; + } + if (done) + { + while (queue.TryDequeue(out var line)) + { + yield return line; + } + yield break; + } + } + } + finally + { + target.OutputDataReceived -= outHandler; + target.ErrorDataReceived -= errHandler; + signal.Dispose(); + } + } +#endif + static void WaitForExitOrThrow(Process target, TimeSpan? timeout) + { + if (timeout is { } value) + { + var ms = (long)value.TotalMilliseconds; + if (ms < 0 || ms > int.MaxValue) + { + target.WaitForExit(); + return; + } + if (!target.WaitForExit((int)ms)) + { + throw new TimeoutException("The process did not exit within the specified timeout."); + } + } + else + { + target.WaitForExit(); + } + } } diff --git a/src/Split/uap10.0/PosixSignal.cs b/src/Split/uap10.0/PosixSignal.cs new file mode 100644 index 00000000..d099d4a6 --- /dev/null +++ b/src/Split/uap10.0/PosixSignal.cs @@ -0,0 +1,35 @@ +// +#pragma warning disable +namespace System.Runtime.InteropServices; +/// +/// Specifies a POSIX signal number. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +enum PosixSignal +{ + /// Hangup + SIGHUP = -1, + /// Interrupt + SIGINT = -2, + /// Quit + SIGQUIT = -3, + /// Termination + SIGTERM = -4, + /// Child stopped + SIGCHLD = -5, + /// Continue + SIGCONT = -6, + /// Window resized + SIGWINCH = -7, + /// Terminal input for background process + SIGTTIN = -8, + /// Terminal output for background process + SIGTTOU = -9, + /// Stop typed at terminal + SIGTSTP = -10, +} diff --git a/src/Split/uap10.0/ProcessExitStatus.cs b/src/Split/uap10.0/ProcessExitStatus.cs new file mode 100644 index 00000000..88a554e1 --- /dev/null +++ b/src/Split/uap10.0/ProcessExitStatus.cs @@ -0,0 +1,40 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +/// +/// Represents the exit status of a process. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessExitStatus +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessExitStatus(int exitCode, bool canceled, PosixSignal? signal = null) + { + ExitCode = exitCode; + Canceled = canceled; + Signal = signal; + } + /// + /// Gets a value indicating whether the process was canceled. + /// + public bool Canceled { get; } + /// + /// Gets the exit code of the process. + /// + public int ExitCode { get; } + /// + /// Gets the POSIX signal that terminated the process, if any. + /// + public PosixSignal? Signal { get; } +} diff --git a/src/Split/uap10.0/ProcessOutputLine.cs b/src/Split/uap10.0/ProcessOutputLine.cs new file mode 100644 index 00000000..eb36fbb8 --- /dev/null +++ b/src/Split/uap10.0/ProcessOutputLine.cs @@ -0,0 +1,34 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents a line of output from a process, including whether it came from standard error. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct ProcessOutputLine +{ + /// + /// Initializes a new instance of the struct. + /// + public ProcessOutputLine(string content, bool standardError) + { + Content = content; + StandardError = standardError; + } + /// + /// Gets the content of the output line. + /// + public string Content { get; } + /// + /// Gets a value indicating whether the line came from the standard error stream. + /// + public bool StandardError { get; } +} diff --git a/src/Split/uap10.0/ProcessPolyfill.cs b/src/Split/uap10.0/ProcessPolyfill.cs new file mode 100644 index 00000000..8d8e55e6 --- /dev/null +++ b/src/Split/uap10.0/ProcessPolyfill.cs @@ -0,0 +1,206 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +static partial class Polyfill +{ + extension(Process) + { + /// + /// Starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + using var process = StartOrThrow(startInfo); + WaitForExitOrThrow(process, timeout); + return new(process.ExitCode, canceled: false); + } + /// + /// Starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessExitStatus Run(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.Run(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + using var process = StartOrThrow(startInfo); + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + return new(canceled ? -1 : process.ExitCode, canceled); + } + /// + /// Asynchronously starts the process described by and , waits for it to exit, and returns the exit status. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + WaitForExitOrThrow(process, timeout); + var status = new ProcessExitStatus(process.ExitCode, canceled: false); + return new(status, stdoutTask.GetAwaiter().GetResult(), stderrTask.GetAwaiter().GetResult(), pid); + } + /// + /// Starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = default) => + Process.RunAndCaptureText(BuildStartInfo(fileName, arguments), timeout); + /// + /// Asynchronously starts the process described by , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static async Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) + { + ConfigureForCapture(startInfo); + using var process = StartOrThrow(startInfo); + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + var pid = process.Id; + var canceled = false; + try + { + await process.WaitForExitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + canceled = true; + TryKill(process); + try + { + await process.WaitForExitAsync(); + } + catch + { + } + } + var status = new ProcessExitStatus(canceled ? -1 : process.ExitCode, canceled); + return new(status, await stdoutTask, await stderrTask, pid); + } + /// + /// Asynchronously starts the process described by and , captures its standard output and standard error as text, waits for it to exit, and returns the captured output. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) => + Process.RunAndCaptureTextAsync(BuildStartInfo(fileName, arguments), cancellationToken); + /// + /// Starts the process described by in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(ProcessStartInfo startInfo) + { + var process = StartOrThrow(startInfo); + try + { + return process.Id; + } + finally + { + process.Dispose(); + } + } + /// + /// Starts the process described by and in detached fashion and returns its process identifier. + /// + [SupportedOSPlatform("maccatalyst")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static int StartAndForget(string fileName, IList? arguments = null) => + Process.StartAndForget(BuildStartInfo(fileName, arguments)); + } + static Process StartOrThrow(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + if (process is null) + { + throw new InvalidOperationException("Failed to start process."); + } + return process; + } + static ProcessStartInfo BuildStartInfo(string fileName, IList? arguments) + { + var info = new ProcessStartInfo + { + FileName = fileName, + }; + if (arguments is not null) + { + foreach (var arg in arguments) + { + info.ArgumentList.Add(arg); + } + } + return info; + } + static void ConfigureForCapture(ProcessStartInfo startInfo) + { + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + } + static void TryKill(Process process) + { + try + { + if (!process.HasExited) + { + process.Kill(); + } + } + catch + { + } + } +} diff --git a/src/Split/uap10.0/ProcessTextOutput.cs b/src/Split/uap10.0/ProcessTextOutput.cs new file mode 100644 index 00000000..70f6174d --- /dev/null +++ b/src/Split/uap10.0/ProcessTextOutput.cs @@ -0,0 +1,44 @@ +// +#pragma warning disable +namespace System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Represents the captured text output from a process, including standard output, standard error, and exit status. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +sealed class ProcessTextOutput +{ + /// + /// Initializes a new instance of the class. + /// + public ProcessTextOutput(ProcessExitStatus exitStatus, string standardOutput, string standardError, int processId) + { + ExitStatus = exitStatus; + StandardOutput = standardOutput; + StandardError = standardError; + ProcessId = processId; + } + /// + /// Gets the exit status of the process. + /// + public ProcessExitStatus ExitStatus { get; } + /// + /// Gets the process identifier. + /// + public int ProcessId { get; } + /// + /// Gets the captured standard error text. + /// + public string StandardError { get; } + /// + /// Gets the captured standard output text. + /// + public string StandardOutput { get; } +} diff --git a/src/Tests/PolyfillTests_Process.cs b/src/Tests/PolyfillTests_Process.cs index be09642b..88abda28 100644 --- a/src/Tests/PolyfillTests_Process.cs +++ b/src/Tests/PolyfillTests_Process.cs @@ -1,3 +1,6 @@ +using System.Linq; +using System.Threading; + partial class PolyfillTests { [Test] @@ -42,4 +45,206 @@ public async Task Process_Kill_EntireProcessTree() await process.WaitForExitAsync(); await Assert.That(process.HasExited).IsTrue(); } + + [Test] + public async Task Process_ReadAllText() + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "--info", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + } + }; + process.Start(); + var (stdout, stderr) = process.ReadAllText(); + await Assert.That(stdout).IsNotNull(); + await Assert.That(stderr).IsNotNull(); + await Assert.That(stdout.Length).IsGreaterThan(0); + } + + [Test] + public async Task Process_ReadAllTextAsync() + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "--info", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + } + }; + process.Start(); + var (stdout, stderr) = await process.ReadAllTextAsync(); + await Assert.That(stdout).IsNotNull(); + await Assert.That(stderr).IsNotNull(); + await Assert.That(stdout.Length).IsGreaterThan(0); + } + + [Test] + public async Task Process_ReadAllBytes() + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "--info", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + } + }; + process.Start(); + var (stdout, stderr) = process.ReadAllBytes(); + await Assert.That(stdout).IsNotNull(); + await Assert.That(stderr).IsNotNull(); + await Assert.That(stdout.Length).IsGreaterThan(0); + } + + [Test] + public async Task Process_ReadAllBytesAsync() + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "--info", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + } + }; + process.Start(); + var (stdout, stderr) = await process.ReadAllBytesAsync(); + await Assert.That(stdout).IsNotNull(); + await Assert.That(stderr).IsNotNull(); + await Assert.That(stdout.Length).IsGreaterThan(0); + } + + [Test] + public async Task Process_ReadAllLinesAsync() + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "--info", + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + } + }; + process.Start(); + var lines = new List(); + await foreach (var line in process.ReadAllLinesAsync()) + { + lines.Add(line); + } + await Assert.That(lines.Count).IsGreaterThan(0); + await Assert.That(lines.Any(_ => !_.StandardError)).IsTrue(); + } + + [Test] + public async Task Process_Run() + { + var status = Process.Run("dotnet", new[] { "--info" }); + await Assert.That(status).IsNotNull(); + await Assert.That(status.Canceled).IsFalse(); + await Assert.That(status.ExitCode).IsEqualTo(0); + } + + [Test] + public async Task Process_RunAsync() + { + var status = await Process.RunAsync("dotnet", new[] { "--info" }); + await Assert.That(status).IsNotNull(); + await Assert.That(status.Canceled).IsFalse(); + await Assert.That(status.ExitCode).IsEqualTo(0); + } + + [Test] + public async Task Process_RunAsync_Canceled() + { + using var source = new CancellationTokenSource(); + source.Cancel(); + var status = await Process.RunAsync("dotnet", new[] { "--info" }, source.Token); + await Assert.That(status.Canceled).IsTrue(); + } + + [Test] + public async Task Process_RunAndCaptureText() + { + var output = Process.RunAndCaptureText("dotnet", new[] { "--info" }); + await Assert.That(output).IsNotNull(); + await Assert.That(output.ExitStatus.ExitCode).IsEqualTo(0); + await Assert.That(output.ExitStatus.Canceled).IsFalse(); + await Assert.That(output.StandardOutput.Length).IsGreaterThan(0); + await Assert.That(output.ProcessId).IsGreaterThan(0); + } + + [Test] + public async Task Process_RunAndCaptureTextAsync() + { + var output = await Process.RunAndCaptureTextAsync("dotnet", new[] { "--info" }); + await Assert.That(output).IsNotNull(); + await Assert.That(output.ExitStatus.ExitCode).IsEqualTo(0); + await Assert.That(output.StandardOutput.Length).IsGreaterThan(0); + } + + [Test] + public async Task Process_StartAndForget() + { + var pid = Process.StartAndForget( + new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "--info", + UseShellExecute = false, + CreateNoWindow = true, + }); + await Assert.That(pid).IsGreaterThan(0); + } + + [Test] + public async Task ProcessOutputLine_Construction() + { + var line = new ProcessOutputLine("content", standardError: true); + await Assert.That(line.Content).IsEqualTo("content"); + await Assert.That(line.StandardError).IsTrue(); + } + + [Test] + public async Task ProcessExitStatus_Construction() + { + var status = new ProcessExitStatus(42, canceled: true); + await Assert.That(status.ExitCode).IsEqualTo(42); + await Assert.That(status.Canceled).IsTrue(); + await Assert.That(status.Signal).IsNull(); + } + + [Test] + public async Task ProcessTextOutput_Construction() + { + var exit = new ProcessExitStatus(0, canceled: false); + var output = new ProcessTextOutput(exit, "out", "err", 1234); + await Assert.That(output.ExitStatus).IsSameReferenceAs(exit); + await Assert.That(output.StandardOutput).IsEqualTo("out"); + await Assert.That(output.StandardError).IsEqualTo("err"); + await Assert.That(output.ProcessId).IsEqualTo(1234); + } }