Skip to content

Commit 99ccbc8

Browse files
committed
feat: add progress and status handling with cancellation support and new payloads
1 parent 5935e50 commit 99ccbc8

11 files changed

Lines changed: 155 additions & 123 deletions

src/Extensions/ILoggerExtensions.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,32 @@ public static ILogger HorizontalRule(this ILogger @this, string? title = null)
7474
return @this;
7575
}
7676

77+
public static async Task ProgressAsync(this ILogger @this, Func<Progress, Task> action, CancellationToken cancellationToken = default)
78+
{
79+
ArgumentNullException.ThrowIfNull(@this);
80+
ArgumentNullException.ThrowIfNull(action);
81+
82+
ProgressStartPayload startPayload = new()
83+
{
84+
CancellationToken = cancellationToken,
85+
};
86+
87+
@this.Log(null, startPayload);
88+
89+
Progress progress = await startPayload.WaitForProgressAsync().ConfigureAwait(false);
90+
91+
await action(progress).ConfigureAwait(false);
92+
93+
ProgressFinishedPayload finishedPayload = new()
94+
{
95+
CancellationToken = cancellationToken,
96+
};
97+
98+
@this.Log(null, finishedPayload);
99+
100+
await finishedPayload.WaitForFinishedAsync().ConfigureAwait(false);
101+
}
102+
77103
/// <summary>
78104
/// Starts a progress with the <paramref name="progressConfiguration"/> running the specified <paramref name="action"/> function.
79105
/// The progress will be automatically completed when the <paramref name="action"/> function completes.
@@ -112,7 +138,7 @@ public static async Task StartProgressAsync(this ILogger @this, Action<Progress>
112138
public static async Task StartProgressAsync(this ILogger @this, Func<ProgressContext, CancellationToken, Task> action, CancellationToken cancellationToken = default)
113139
=> await StartProgressAsync(@this, _ => { }, action, cancellationToken).ConfigureAwait(false);
114140

115-
public static async Task StartStatusAsync(this ILogger @this, string message, Action<Status> status, Func<StatusContext, Task> action)
141+
public static async Task StartStatusAsync(this ILogger @this, string message, Action<Status> status, Func<StatusContext, CancellationToken, Task> action, CancellationToken cancellationToken = default)
116142
{
117143
ArgumentNullException.ThrowIfNull(@this);
118144

@@ -121,15 +147,16 @@ public static async Task StartStatusAsync(this ILogger @this, string message, Ac
121147
StatusConfigurationAction = status,
122148
StatusAction = action,
123149
StatusMessage = message,
150+
CancellationToken = cancellationToken,
124151
};
125152

126-
await @this.FlushAsync().ConfigureAwait(false);
153+
await @this.FlushAsync(cancellationToken).ConfigureAwait(false);
127154

128155
@this.Log(null, statusPayload);
129156

130157
await statusPayload.Completed.ConfigureAwait(false);
131158
}
132159

133160
public static Task StartStatusAsync(this ILogger @this, string statusMessage, Func<StatusContext, Task> action)
134-
=> StartStatusAsync(@this, statusMessage, _ => { }, action);
161+
=> StartStatusAsync(@this, statusMessage, _ => { }, (context, cancellationToken) => action(context), CancellationToken.None);
135162
}

src/Extensions/TaskExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Spectre.Console;
5+
6+
namespace WB.Logging.LogSinks.Console.Spectre;
7+
8+
internal static class TaskExtensions
9+
{
10+
internal static async Task StartAsync(this Task<Progress> @this, Func<ProgressContext, CancellationToken, Task> action, CancellationToken cancellationToken = default)
11+
{
12+
Progress progress = await @this.ConfigureAwait(false);
13+
14+
await progress.StartAsync(context => action(context, cancellationToken)).ConfigureAwait(false);
15+
}
16+
}

src/LogMessageWriters/ProgressConsoleMessageWriter.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ namespace WB.Logging.LogSinks.Console.Spectre;
1010
/// A log message writer that uses Spectre.Console's progress bar to
1111
/// render progress updates in the console.
1212
/// </summary>
13-
internal sealed class ProgressConsoleMessageWriter : IAsyncLogMessageWriter<ProgressPayload, IAnsiConsole>
13+
internal sealed class ProgressConsoleMessageWriter
14+
: IAsyncLogMessageWriter<ProgressStartPayload, IAnsiConsole>
15+
, IAsyncLogMessageWriter<ProgressFinishedPayload, IAnsiConsole>
1416
{
17+
private IDisposable? logSinkDisabledSubscription;
18+
1519
// ┌─────────────────────────────────────────────────────────────────────────────┐
1620
// │ Public Properties │
1721
// └─────────────────────────────────────────────────────────────────────────────┘
@@ -26,26 +30,29 @@ internal sealed class ProgressConsoleMessageWriter : IAsyncLogMessageWriter<Prog
2630
// │ Public Methods │
2731
// └─────────────────────────────────────────────────────────────────────────────┘
2832

29-
/// <inheritdoc/>
30-
public async ValueTask WriteAsync(DateTimeOffset timestamp, LogLevel? logLevel, IEnumerable<string> senders, ProgressPayload payload)
33+
public ValueTask WriteAsync(DateTimeOffset timestamp, LogLevel? logLevel, IEnumerable<string> senders, ProgressStartPayload payload)
3134
{
32-
#pragma warning disable CA1031 // Do not catch general exception types
33-
try
35+
if (logSinkDisabledSubscription is null)
3436
{
35-
using IDisposable _ = payload.CancellationToken.Register(payload.SetCanceled);
36-
37-
Progress progress = Writer.Progress();
37+
logSinkDisabledSubscription = LogSink?.Disable();
3838

39-
payload.ProgressConfigurationAction(progress);
39+
payload.SetProgress(Writer.Progress());
40+
}
4041

41-
await progress.StartAsync(context => payload.ProgressAction(context, payload.CancellationToken)).ConfigureAwait(false);
42+
return ValueTask.CompletedTask;
43+
}
4244

43-
payload.SetCompleted();
44-
}
45-
catch (Exception exception)
45+
public ValueTask WriteAsync(DateTimeOffset timestamp, LogLevel? logLevel, IEnumerable<string> senders, ProgressFinishedPayload payload)
46+
{
47+
if (logSinkDisabledSubscription is not null)
4648
{
47-
payload.SetException(exception);
49+
logSinkDisabledSubscription.Dispose();
50+
logSinkDisabledSubscription = null;
51+
52+
payload.SetFinished();
4853
}
49-
#pragma warning restore CA1031 // Do not catch general exception types
54+
55+
return ValueTask.CompletedTask;
5056
}
57+
5158
}

src/LogMessageWriters/StatusConsoleMessageWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public async ValueTask WriteAsync(DateTimeOffset timestamp, LogLevel? logLevel,
3636

3737
payload.StatusConfigurationAction(status);
3838

39-
await status.StartAsync(payload.StatusMessage, payload.StatusAction).ConfigureAwait(false);
39+
await status.StartAsync(payload.StatusMessage, context => payload.StatusAction(context, payload.CancellationToken)).ConfigureAwait(false);
4040

4141
payload.SetCompleted();
4242
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Spectre.Console;
4+
5+
namespace WB.Logging.LogSinks.Console.Spectre;
6+
7+
internal sealed class ProgressFinishedPayload
8+
{
9+
private readonly TaskCompletionSource taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
10+
11+
public CancellationToken CancellationToken { get; init; } = CancellationToken.None;
12+
13+
public void SetFinished()
14+
=> taskCompletionSource.TrySetResult();
15+
16+
public Task WaitForFinishedAsync()
17+
=> taskCompletionSource.Task;
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Spectre.Console;
4+
5+
namespace WB.Logging.LogSinks.Console.Spectre;
6+
7+
internal sealed class ProgressStartPayload
8+
{
9+
private readonly TaskCompletionSource<Progress> taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
10+
11+
public CancellationToken CancellationToken { get; init; } = CancellationToken.None;
12+
13+
public void SetProgress(Progress progress)
14+
=> taskCompletionSource.TrySetResult(progress);
15+
16+
public Task<Progress> WaitForProgressAsync()
17+
=> taskCompletionSource.Task;
18+
}

src/Payloads/StatusPayload.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading;
23
using System.Threading.Tasks;
34
using Spectre.Console;
45

@@ -24,10 +25,12 @@ internal sealed class StatusPayload
2425
/// <summary>
2526
/// Gets or sets the action that performs the status update.
2627
/// </summary>
27-
public required Func<StatusContext, Task> StatusAction { get; init; }
28+
public required Func<StatusContext, CancellationToken, Task> StatusAction { get; init; }
2829

2930
public Action<Status> StatusConfigurationAction { get; init; } = _ => { };
3031

32+
public CancellationToken CancellationToken { get; init; } = CancellationToken.None;
33+
3134
/// <summary>
3235
/// Gets a <see cref="Task"/> that represents the completion of the status. The
3336
/// <see cref="Task"/> will complete when the status is completed or if an exception

src/SpectreConsoleLogSink.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace WB.Logging.LogSinks.Console.Spectre;
1010
/// </summary>
1111
public sealed class SpectreConsoleLogSink : AsyncLogSinkBase<IAnsiConsole>
1212
{
13+
private readonly ProgressConsoleMessageWriter progressConsoleMessageWriter = new();
14+
1315
// ┌─────────────────────────────────────────────────────────────────────────────┐
1416
// │ Public Constructors │
1517
// └─────────────────────────────────────────────────────────────────────────────┘
@@ -20,6 +22,7 @@ public sealed class SpectreConsoleLogSink : AsyncLogSinkBase<IAnsiConsole>
2022
public SpectreConsoleLogSink() : base(new SpectreConsoleLogMessageWriter<object>(), AnsiConsole.Console)
2123
{
2224
RegisterLogMessageWriter(new PayloadLogMessageWriter());
23-
RegisterLogMessageWriter(new ProgressConsoleMessageWriter());
25+
RegisterLogMessageWriter<ProgressStartPayload>(progressConsoleMessageWriter);
26+
RegisterLogMessageWriter<ProgressFinishedPayload>(progressConsoleMessageWriter);
2427
}
2528
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Threading.Tasks;
2+
using AwesomeAssertions;
3+
using Spectre.Console;
4+
using Spectre.Console.Testing;
5+
using WB.Logging;
6+
using WB.Logging.LogSinks.Console.Spectre;
7+
8+
namespace ExtensionsTests.ILoggerExtensionsTests.ProgressAsyncMethodTests;
9+
10+
public sealed class TheProgressAsyncMethod
11+
{
12+
[Test]
13+
public async Task ShouldRunAProgress()
14+
{
15+
// Arrange
16+
TestConsole testConsole = new();
17+
ILogger logger = new Logger("test");
18+
logger.AttachSpectreConsole(logSink =>
19+
{
20+
logSink.Writer = testConsole;
21+
});
22+
23+
// Act
24+
await logger.ProgressAsync(async progress =>
25+
{
26+
await progress.StartAsync(async context =>
27+
{
28+
ProgressTask task = context.AddTask("Processing...").MaxValue(100);
29+
30+
for (int i = 0; i < 100; i++)
31+
{
32+
task.Increment(1);
33+
await Task.Delay(10);
34+
}
35+
}).ConfigureAwait(false);
36+
});
37+
await logger.FlushAsync();
38+
39+
// Assert
40+
testConsole.Output.Should().Contain("Processing...");
41+
}
42+
}

tests/ExtensionsTests/src/ILoggerExtensionsTests/StartProgressAsyncMethodTests.cs

Lines changed: 0 additions & 98 deletions
This file was deleted.

0 commit comments

Comments
 (0)