Skip to content

Commit

Permalink
Proc.StartLongRunning allows you to yield after waiting for started. (#…
Browse files Browse the repository at this point in the history
…16)

Without necessarily killing the process (unless disposing the long running subscription).

Its now also easier to stop buffering process output after started handler (optional)
  • Loading branch information
Mpdreamz authored Oct 2, 2024
1 parent 9668d9f commit 69ed5c2
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 44 deletions.
9 changes: 5 additions & 4 deletions src/Proc/ObservableProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,12 @@ public virtual IDisposable SubscribeLines(Action<LineOut> onNext, Action<Excepti
public virtual IDisposable SubscribeLinesAndCharacters(
Action<LineOut> onNext, Action<Exception> onError,
Action<CharactersOut> onNextCharacters,
Action<Exception> onExceptionCharacters
) =>
Action<Exception> onExceptionCharacters,
Action? onCompleted = null

Check warning on line 147 in src/Proc/ObservableProcess.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 147 in src/Proc/ObservableProcess.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 147 in src/Proc/ObservableProcess.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 147 in src/Proc/ObservableProcess.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 147 in src/Proc/ObservableProcess.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 147 in src/Proc/ObservableProcess.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
) =>
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<LineOut> onNext) =>
Expand Down
43 changes: 26 additions & 17 deletions src/Proc/Proc.StartLongRunning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <inheritdoc cref="ObservableProcessBase{TConsoleOut}.SendControlC(int)"/>>
public bool SendControlC(int processId) => Process.SendControlC(processId);

/// <inheritdoc cref="ObservableProcessBase{TConsoleOut}.SendControlC()"/>>
public void SendControlC() => Process.SendControlC();

public void Dispose()
Expand Down Expand Up @@ -52,48 +59,50 @@ public static partial class Proc
/// <returns>The exit code and whether the process completed</returns>
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
Expand All @@ -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;
}
}
}
16 changes: 3 additions & 13 deletions tests/Proc.Tests/LineOutputTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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));
}
}
}
28 changes: 18 additions & 10 deletions tests/Proc.Tests/LongRunningTests.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
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()
{
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);
Expand All @@ -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);
Expand All @@ -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));

Expand All @@ -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:"));
Expand Down
19 changes: 19 additions & 0 deletions tests/Proc.Tests/TestConsoleOutWriter.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 69ed5c2

Please sign in to comment.