diff --git a/src/Proc/BufferedObservableProcess.cs b/src/Proc/BufferedObservableProcess.cs index 0cb5345..7d839c8 100644 --- a/src/Proc/BufferedObservableProcess.cs +++ b/src/Proc/BufferedObservableProcess.cs @@ -74,11 +74,14 @@ private IDisposable KickOff(IObserver observer) if (Process.HasExited) { + Process.ReadStandardErrBlocking(_observer, BufferSize, () => ContinueReadingFromProcessReaders()); + Process.ReadStandardOutBlocking(_observer, BufferSize, () => ContinueReadingFromProcessReaders()); OnExit(observer); return Disposable.Empty; } _observer = observer; + StartAsyncReads(); Process.Exited += (o, s) => diff --git a/src/Proc/Extensions/ArgumentExtensions.cs b/src/Proc/Extensions/ArgumentExtensions.cs index 4a875e9..4cbda3d 100644 --- a/src/Proc/Extensions/ArgumentExtensions.cs +++ b/src/Proc/Extensions/ArgumentExtensions.cs @@ -1,7 +1,5 @@ -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Text; namespace ProcNet.Extensions; diff --git a/src/Proc/Extensions/ObserveOutputExtensions.cs b/src/Proc/Extensions/ObserveOutputExtensions.cs index cf8a70b..a5991ae 100644 --- a/src/Proc/Extensions/ObserveOutputExtensions.cs +++ b/src/Proc/Extensions/ObserveOutputExtensions.cs @@ -70,5 +70,26 @@ private static async Task BufferedRead(Process p, StreamReader r, IObserver observer, int bufferSize, Func keepBuffering) => + BufferedReadBlocking(process, process.StandardError, observer, bufferSize, ConsoleOut.ErrorOut, keepBuffering); + + public static void ReadStandardOutBlocking(this Process process, IObserver observer, int bufferSize, Func keepBuffering) => + BufferedReadBlocking(process, process.StandardOutput, observer, bufferSize, ConsoleOut.Out, keepBuffering); + + private static void BufferedReadBlocking(this Process p, StreamReader r, IObserver o, int b, Func m, Func keepBuffering) + { + using var sr = new StreamReader(r.BaseStream, Encoding.UTF8, true, b, true); + while (keepBuffering()) + { + var buffer = new char[b]; + var read = sr.Read(buffer, 0, buffer.Length); + + if (read > 0) + o.OnNext(m(buffer)); + else + if (sr.EndOfStream) break; + } + } + } } diff --git a/src/Proc/ObservableProcess.cs b/src/Proc/ObservableProcess.cs index 2b22943..33e8cc7 100644 --- a/src/Proc/ObservableProcess.cs +++ b/src/Proc/ObservableProcess.cs @@ -144,7 +144,7 @@ public virtual IDisposable SubscribeLinesAndCharacters( Action onNext, Action onError, Action onNextCharacters, Action onExceptionCharacters, - Action? onCompleted = null + Action onCompleted = null ) => Subscribe( Observer.Create(onNext, onError, onCompleted ?? delegate { }), diff --git a/src/Proc/Proc.Exec.cs b/src/Proc/Proc.Exec.cs index 3bef3ef..85a7d69 100644 --- a/src/Proc/Proc.Exec.cs +++ b/src/Proc/Proc.Exec.cs @@ -51,14 +51,13 @@ public static partial class Proc var info = new ProcessStartInfo(arguments.Binary) { UseShellExecute = false - #if !NETSTANDARD2_1 - , Arguments = args - #endif }; - #if NETSTANDARD2_1 +#if NETSTANDARD2_1 foreach (var arg in arguments.Args) info.ArgumentList.Add(arg); - #endif +#else + info.Arguments = args; +#endif var pwd = arguments.WorkingDirectory; if (!string.IsNullOrWhiteSpace(pwd)) info.WorkingDirectory = pwd; @@ -93,6 +92,7 @@ public static partial class Proc return exitCode; } + private static void HardWaitForExit(Process process, TimeSpan timeSpan) { using var task = Task.Run(() => process.WaitForExit()); diff --git a/tests/Proc.Tests.Binary/Program.cs b/tests/Proc.Tests.Binary/Program.cs index 32273ba..583d899 100644 --- a/tests/Proc.Tests.Binary/Program.cs +++ b/tests/Proc.Tests.Binary/Program.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -18,6 +19,7 @@ public static async Task Main(string[] args) var testCase = args[0].ToLowerInvariant(); + if (testCase == nameof(PrintArgs).ToLowerInvariant()) return PrintArgs(args.Skip(1).ToArray()); if (testCase == nameof(SingleLineNoEnter).ToLowerInvariant()) return SingleLineNoEnter(); if (testCase == nameof(TwoWrites).ToLowerInvariant()) return TwoWrites(); @@ -41,6 +43,12 @@ public static async Task Main(string[] args) return 1; } + private static int PrintArgs(string[] args) + { + foreach (var arg in args) + Console.WriteLine(arg); + return 0; + } private static int DelayedWriter() { Thread.Sleep(3000); diff --git a/tests/Proc.Tests/PrintArgsTests.cs b/tests/Proc.Tests/PrintArgsTests.cs new file mode 100644 index 0000000..35892fc --- /dev/null +++ b/tests/Proc.Tests/PrintArgsTests.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace ProcNet.Tests; + +public class PrintArgsTests(ITestOutputHelper output) : TestsBase +{ + [Fact] + public void ProcSendsAllArguments() + { + string[] testArgs = ["hello", "world"]; + AssertOutput(testArgs); + } + + [Fact] + public void ArgumentsWithSpaceAreNotSplit() + { + string[] testArgs = ["hello", "world", "this argument has spaces"]; + AssertOutput(testArgs); + } + + [Fact] + public void ArgumentsSeesArgumentsAfterQuoted() + { + string[] testArgs = ["this argument has spaces", "hello", "world"]; + AssertOutput(testArgs); + } + [Fact] + public void EscapedQuotes() + { + string[] testArgs = ["\"this argument has spaces\"", "hello", "world"]; + AssertOutput(testArgs); + } + + private void AssertOutput(string[] testArgs) + { + var args = TestCaseArguments("PrintArgs", testArgs); + var outputWriter = new TestConsoleOutWriter(output); + var result = Proc.Start(args, WaitTimeout, outputWriter); + result.ExitCode.Should().Be(0); + result.ConsoleOut.Should().NotBeEmpty().And.HaveCount(testArgs.Length); + for (var i = 0; i < result.ConsoleOut.Count; i++) + result.ConsoleOut[i].Line.Should().Be(testArgs[i], i.ToString()); + } + +} diff --git a/tests/Proc.Tests/Proc.Tests.csproj b/tests/Proc.Tests/Proc.Tests.csproj index 1bd273d..913694a 100644 --- a/tests/Proc.Tests/Proc.Tests.csproj +++ b/tests/Proc.Tests/Proc.Tests.csproj @@ -4,6 +4,7 @@ Proc.Tests ProcNet.Tests false + preview diff --git a/tests/Proc.Tests/TestConsoleOutWriter.cs b/tests/Proc.Tests/TestConsoleOutWriter.cs index 85579e9..7a12aa5 100644 --- a/tests/Proc.Tests/TestConsoleOutWriter.cs +++ b/tests/Proc.Tests/TestConsoleOutWriter.cs @@ -8,12 +8,13 @@ public class TestConsoleOutWriter(ITestOutputHelper output) : IConsoleOutWriter private readonly StringBuilder _sb = new(); public string[] Lines => _sb.ToString().Replace("\r\n", "\n").Split(new [] {"\n"}, StringSplitOptions.None); public string Text => _sb.ToString(); + private static char[] NewLineChars = Environment.NewLine.ToCharArray(); public void Write(Exception e) => throw e; public void Write(ConsoleOut consoleOut) { consoleOut.CharsOrString(c => _sb.Append(new string(c)), s => _sb.AppendLine(s)); - consoleOut.CharsOrString(c => output.WriteLine(new string(c)), output.WriteLine); + consoleOut.CharsOrString(c => output.WriteLine(new string(c).TrimEnd(NewLineChars)), s => output.WriteLine(s)); } } diff --git a/tests/Proc.Tests/TestsBase.cs b/tests/Proc.Tests/TestsBase.cs index 9a6b167..3de8117 100644 --- a/tests/Proc.Tests/TestsBase.cs +++ b/tests/Proc.Tests/TestsBase.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Reflection; namespace ProcNet.Tests @@ -25,11 +26,15 @@ private static string GetWorkingDir() return binaryFolder; } - protected static StartArguments TestCaseArguments(string testcase) => - new("dotnet", GetDll(), testcase) + protected static StartArguments TestCaseArguments(string testcase, params string[] args) + { + string[] arguments = [GetDll(), testcase]; + + return new StartArguments("dotnet", arguments.Concat(args)) { WorkingDirectory = GetWorkingDir(), }; + } protected static LongRunningArguments LongRunningTestCaseArguments(string testcase) => new("dotnet", GetDll(), testcase)