diff --git a/src/Proc/ObservableProcess.cs b/src/Proc/ObservableProcess.cs index bf6d0b8..2b22943 100644 --- a/src/Proc/ObservableProcess.cs +++ b/src/Proc/ObservableProcess.cs @@ -143,11 +143,12 @@ public virtual IDisposable SubscribeLines(Action onNext, Action onNext, Action onError, Action onNextCharacters, - Action onExceptionCharacters - ) => + Action onExceptionCharacters, + Action? onCompleted = null + ) => Subscribe( - Observer.Create(onNext, onError, delegate { }), - Observer.Create(onNextCharacters, onExceptionCharacters, delegate { }) + Observer.Create(onNext, onError, onCompleted ?? delegate { }), + Observer.Create(onNextCharacters, onExceptionCharacters, onCompleted ?? delegate { }) ); public virtual IDisposable SubscribeLines(Action onNext) => diff --git a/src/Proc/Proc.StartLongRunning.cs b/src/Proc/Proc.StartLongRunning.cs index 5d0fab6..ada85f8 100644 --- a/src/Proc/Proc.StartLongRunning.cs +++ b/src/Proc/Proc.StartLongRunning.cs @@ -17,9 +17,16 @@ internal LongRunningApplicationSubscription(ObservableProcess process, Composite private IDisposable Subscription { get; } - public ObservableProcess Process { get; } + private ObservableProcess Process { get; } + public bool Running { get; internal set; } + + internal ManualResetEvent WaitHandle { get; } = new(false); + + /// > public bool SendControlC(int processId) => Process.SendControlC(processId); + + /// > public void SendControlC() => Process.SendControlC(); public void Dispose() @@ -52,48 +59,50 @@ public static partial class Proc /// The exit code and whether the process completed public static LongRunningApplicationSubscription StartLongRunning(LongRunningArguments arguments, TimeSpan waitForStartedConfirmation, IConsoleOutWriter consoleOutWriter = null) { - var started = false; - var confirmWaitHandle = new ManualResetEvent(false); var composite = new CompositeDisposable(); var process = new ObservableProcess(arguments); + var subscription = new LongRunningApplicationSubscription(process, composite); consoleOutWriter ??= new ConsoleOutColorWriter(); var startedConfirmation = arguments.StartedConfirmationHandler ?? (_ => true); if (arguments.StartedConfirmationHandler != null && arguments.StopBufferingAfterStarted) - arguments.KeepBufferingLines = _ => !started; + arguments.KeepBufferingLines = _ => !subscription.Running; Exception seenException = null; composite.Add(process); composite.Add(process.SubscribeLinesAndCharacters( l => { - if (startedConfirmation(l)) - { - confirmWaitHandle.Set(); - started = true; - } + if (!startedConfirmation(l)) return; + subscription.Running = true; + subscription.WaitHandle.Set(); }, e => { seenException = e; - confirmWaitHandle.Set(); + subscription.Running = false; + subscription.WaitHandle.Set(); }, l => consoleOutWriter.Write(l), - l => consoleOutWriter.Write(l) - ) + l => consoleOutWriter.Write(l), + onCompleted: () => + { + subscription.Running = false; + subscription.WaitHandle.Set(); + }) ); if (seenException != null) ExceptionDispatchInfo.Capture(seenException).Throw(); if (arguments.StartedConfirmationHandler == null) { - confirmWaitHandle.Set(); - started = true; + subscription.Running = true; + subscription.WaitHandle.Set(); } else { - var completed = confirmWaitHandle.WaitOne(waitForStartedConfirmation); - if (completed) return new(process, composite); + var completed = subscription.WaitHandle.WaitOne(waitForStartedConfirmation); + if (completed) return subscription; var pwd = arguments.WorkingDirectory; var args = arguments.Args.NaivelyQuoteArguments(); var printBinary = arguments.OnlyPrintBinaryInExceptionMessage @@ -102,7 +111,7 @@ public static LongRunningApplicationSubscription StartLongRunning(LongRunningArg throw new ProcExecException($"Could not yield started confirmation after {waitForStartedConfirmation} while running {printBinary}"); } - return new(process, composite); + return subscription; } } } diff --git a/tests/Proc.Tests/LineOutputTests.cs b/tests/Proc.Tests/LineOutputTests.cs index 166afc7..d8d2aa2 100644 --- a/tests/Proc.Tests/LineOutputTests.cs +++ b/tests/Proc.Tests/LineOutputTests.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using FluentAssertions; using ProcNet.Std; using Xunit; +using Xunit.Abstractions; namespace ProcNet.Tests { @@ -19,7 +19,7 @@ public void OverwriteLines() } - public class LineOutputTestCases : TestsBase + public class LineOutputTestCases(ITestOutputHelper output) : TestsBase { private static readonly string _expected = @" Windows IP Configuration @@ -99,7 +99,7 @@ public void SubscribeLinesSeesAllLines() [Fact] public void ConsoleWriterSeesAllLines() { - var writer = new TestConsoleOutWriter(); + var writer = new TestConsoleOutWriter(output); var args = TestCaseArguments("MoreText"); var result = Proc.Start(args, WaitTimeout, writer); result.ExitCode.Should().HaveValue(); @@ -110,15 +110,5 @@ public void ConsoleWriterSeesAllLines() lines[i].Should().Be(_expectedLines[i], i.ToString()); } - public class TestConsoleOutWriter : IConsoleOutWriter - { - private readonly StringBuilder _sb = new StringBuilder(); - public string[] Lines => _sb.ToString().Replace("\r\n", "\n").Split(new [] {"\n"}, StringSplitOptions.None); - public string Text => _sb.ToString(); - - public void Write(Exception e) => throw e; - - public void Write(ConsoleOut consoleOut) => consoleOut.CharsOrString(c=>_sb.Append(new string(c)), s=>_sb.AppendLine(s)); - } } } diff --git a/tests/Proc.Tests/LongRunningTests.cs b/tests/Proc.Tests/LongRunningTests.cs index bbe784d..1efe388 100644 --- a/tests/Proc.Tests/LongRunningTests.cs +++ b/tests/Proc.Tests/LongRunningTests.cs @@ -1,15 +1,13 @@ using System; using System.Diagnostics; -using System.Text; using System.Threading.Tasks; using FluentAssertions; -using ProcNet.Std; -using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace ProcNet.Tests { - public class LongRunningTests : TestsBase + public class LongRunningTests(ITestOutputHelper output) : TestsBase { [Fact] public async Task LongRunningShouldSeeAllOutput() @@ -17,9 +15,14 @@ public async Task LongRunningShouldSeeAllOutput() var args = LongRunningTestCaseArguments("LongRunning"); args.StartedConfirmationHandler = l => l.Line == "Started!"; - var outputWriter = new LineOutputTestCases.TestConsoleOutWriter(); - using (var result = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) + var outputWriter = new TestConsoleOutWriter(output); + + using (var process = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) + { + process.Running.Should().BeTrue(); await Task.Delay(TimeSpan.FromSeconds(2)); + process.Running.Should().BeFalse(); + } var lines = outputWriter.Lines; lines.Length.Should().BeGreaterThan(0); @@ -35,11 +38,12 @@ public async Task LongRunningShouldStopBufferingOutputWhenAsked() args.StartedConfirmationHandler = l => l.Line == "Started!"; args.StopBufferingAfterStarted = true; - var outputWriter = new LineOutputTestCases.TestConsoleOutWriter(); + var outputWriter = new TestConsoleOutWriter(output); var sw = Stopwatch.StartNew(); - using (var result = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) + using (var process = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) { + process.Running.Should().BeTrue(); sw.Elapsed.Should().BeGreaterThan(TimeSpan.FromSeconds(1)); var lines = outputWriter.Lines; lines.Length.Should().BeGreaterThan(0); @@ -49,6 +53,7 @@ public async Task LongRunningShouldStopBufferingOutputWhenAsked() await Task.Delay(TimeSpan.FromSeconds(2)); lines.Should().NotContain(s => s.StartsWith("Data after startup:")); } + // we dispose before the program's completion sw.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(20)); @@ -58,10 +63,13 @@ public async Task LongRunningShouldStopBufferingOutputWhenAsked() public async Task LongRunningWithoutConfirmationHandler() { var args = LongRunningTestCaseArguments("LongRunning"); - var outputWriter = new LineOutputTestCases.TestConsoleOutWriter(); + var outputWriter = new TestConsoleOutWriter(output); - using (var result = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) + using (var process = Proc.StartLongRunning(args, WaitTimeout, outputWriter)) + { + process.Running.Should().BeTrue(); await Task.Delay(TimeSpan.FromSeconds(2)); + } var lines = outputWriter.Lines; lines.Should().Contain(s => s.StartsWith("Starting up:")); diff --git a/tests/Proc.Tests/TestConsoleOutWriter.cs b/tests/Proc.Tests/TestConsoleOutWriter.cs new file mode 100644 index 0000000..85579e9 --- /dev/null +++ b/tests/Proc.Tests/TestConsoleOutWriter.cs @@ -0,0 +1,19 @@ +using System; +using System.Text; +using ProcNet.Std; +using Xunit.Abstractions; + +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(); + + 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); + } +}