diff --git a/Manager.App/Components/Layout/ApplicationLayout.razor b/Manager.App/Components/Layout/ApplicationLayout.razor
index d4fb985..37590f8 100644
--- a/Manager.App/Components/Layout/ApplicationLayout.razor
+++ b/Manager.App/Components/Layout/ApplicationLayout.razor
@@ -16,7 +16,7 @@
}
-
+
@Body
\ No newline at end of file
diff --git a/Manager.App/Components/Layout/BaseLayout.razor b/Manager.App/Components/Layout/BaseLayout.razor
index 04778dd..f1dca2b 100644
--- a/Manager.App/Components/Layout/BaseLayout.razor
+++ b/Manager.App/Components/Layout/BaseLayout.razor
@@ -7,7 +7,7 @@
-
+
@Body
diff --git a/Manager.App/Components/Pages/Services.razor b/Manager.App/Components/Pages/Services.razor
index 574a7de..b04c4b2 100644
--- a/Manager.App/Components/Pages/Services.razor
+++ b/Manager.App/Components/Pages/Services.razor
@@ -1,26 +1,90 @@
@page "/Services"
@using Manager.App.Services.System
-
+@implements IDisposable
@inject BackgroundServiceManager ServiceManager
Services
-
+
Services
-
+
+
-
+
+
+
+ Pause
+
+ Resume
+
+
+
+
-
+
-
\ No newline at end of file
+
+
+
+
+ Service events
+ @($"{_serviceEvents.Count}/{VisibleEventCapacity} events")
+
+
+ @foreach (var serviceEvent in _serviceEvents)
+ {
+
+ @serviceEvent.Date
+ |
+ @serviceEvent.Severity
+ |
+ @serviceEvent.Source
+ -
+ @serviceEvent.Message
+
+ }
+
+
+
+
\ No newline at end of file
diff --git a/Manager.App/Components/Pages/Services.razor.cs b/Manager.App/Components/Pages/Services.razor.cs
index a28b9bb..a0bc3a1 100644
--- a/Manager.App/Components/Pages/Services.razor.cs
+++ b/Manager.App/Components/Pages/Services.razor.cs
@@ -1,16 +1,67 @@
+using DotBased.Logging;
+using Manager.App.Extensions;
using Manager.App.Services;
using Microsoft.AspNetCore.Components;
-using MudBlazor;
namespace Manager.App.Components.Pages;
public partial class Services : ComponentBase
{
+ private const int VisibleEventCapacity = 500;
private string _searchText = "";
private List _backgroundServices = [];
+
+ private List _serviceEvents = [];
+ private CancellationTokenSource _cts = new();
protected override void OnInitialized()
{
_backgroundServices = ServiceManager.GetServices();
+ _ = Task.Run(() => ReadEventStreamsAsync(_cts.Token));
+ }
+
+ private Func QuickFilter
+ => x => string.IsNullOrWhiteSpace(_searchText) || $"{x.Name} {x.Description} {x.State} {x.ExecuteInterval}".Contains(_searchText);
+
+ private async Task ReadEventStreamsAsync(CancellationToken token)
+ {
+ if (_backgroundServices.Count == 0)
+ {
+ return;
+ }
+
+ var totalToGet = VisibleEventCapacity / _backgroundServices.Count;
+ _serviceEvents.AddRange(_backgroundServices.SelectMany(x => x.ProgressEvents.Items.TakeLast(totalToGet)));
+
+ var asyncEnumerators = _backgroundServices.Select(x => x.ProgressEvents.GetStreamAsync());
+ await foreach (var serviceEvent in AsyncEnumerableExtensions.Merge(asyncEnumerators, token))
+ {
+ if (!_serviceEvents.Contains(serviceEvent))
+ {
+ _serviceEvents.Add(serviceEvent);
+ }
+
+ if (_serviceEvents.Count > VisibleEventCapacity)
+ {
+ _serviceEvents.RemoveAt(0);
+ }
+
+ await InvokeAsync(StateHasChanged);
+ }
+ }
+
+ private string GetLogClass(ServiceEvent serviceEvent) =>
+ serviceEvent.Severity switch
+ {
+ LogSeverity.Info => "log-info",
+ LogSeverity.Warning => "log-warning",
+ LogSeverity.Error => "log-error",
+ _ => "log-info"
+ };
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _cts.Dispose();
}
}
\ No newline at end of file
diff --git a/Manager.App/Extensions/AsyncEnumerableExtensions.cs b/Manager.App/Extensions/AsyncEnumerableExtensions.cs
new file mode 100644
index 0000000..d8bac98
--- /dev/null
+++ b/Manager.App/Extensions/AsyncEnumerableExtensions.cs
@@ -0,0 +1,48 @@
+using System.Threading.Channels;
+
+namespace Manager.App.Extensions;
+
+public static class AsyncEnumerableExtensions
+{
+ public static async IAsyncEnumerable Merge(IEnumerable> sources, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var channel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
+
+ var writerTasks = sources.Select(source => Task.Run(async () =>
+ {
+ try
+ {
+ await foreach (var item in source.WithCancellation(cancellationToken))
+ {
+ await channel.Writer.WriteAsync(item, cancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+
+ }
+ catch (Exception ex)
+ {
+ channel.Writer.TryComplete(ex);
+ }
+ }, cancellationToken)).ToArray();
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await Task.WhenAll(writerTasks);
+ channel.Writer.TryComplete();
+ }
+ catch
+ {
+ channel.Writer.TryComplete();
+ }
+ }, cancellationToken);
+
+ await foreach (var item in channel.Reader.ReadAllAsync(cancellationToken))
+ {
+ yield return item;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Manager.App/Services/CircularBuffer.cs b/Manager.App/Services/CircularBuffer.cs
index fddb645..fb7afc7 100644
--- a/Manager.App/Services/CircularBuffer.cs
+++ b/Manager.App/Services/CircularBuffer.cs
@@ -6,6 +6,7 @@ public class CircularBuffer
{
private readonly T[] _buffer;
private readonly Channel _channel;
+ private readonly object _lock = new();
public int Capacity { get; }
public int Head { get; private set; }
@@ -30,14 +31,17 @@ public class CircularBuffer
public void Add(T item)
{
- _buffer[Head] = item;
- Head = (Head + 1) % _buffer.Length;
-
- if (Count < _buffer.Length)
+ lock (_lock)
{
- Count++;
+ _buffer[Head] = item;
+ Head = (Head + 1) % _buffer.Length;
+
+ if (Count < _buffer.Length)
+ {
+ Count++;
+ }
}
-
+
_channel.Writer.TryWrite(item);
}
@@ -47,7 +51,10 @@ public class CircularBuffer
{
for (var i = 0; i < Count; i++)
{
- yield return _buffer[(Head - Count + i + _buffer.Length) % _buffer.Length];
+ lock (_lock)
+ {
+ yield return _buffer[(Head - Count + i + _buffer.Length) % _buffer.Length];
+ }
}
}
}
diff --git a/Manager.App/Services/ExtendedBackgroundService.cs b/Manager.App/Services/ExtendedBackgroundService.cs
index a2e3721..7ebba01 100644
--- a/Manager.App/Services/ExtendedBackgroundService.cs
+++ b/Manager.App/Services/ExtendedBackgroundService.cs
@@ -9,16 +9,18 @@ public abstract class ExtendedBackgroundService : BackgroundService
private TaskCompletionSource _resumeSignal = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly ILogger _logger;
public ServiceState State { get; private set; } = ServiceState.Stopped;
- public CircularBuffer ProgressLog { get; } = new(500);
+ public CircularBuffer ProgressEvents { get; } = new(500);
public string Name { get; }
+ public string Description { get; set; }
public TimeSpan ExecuteInterval { get; set; }
- public ExtendedBackgroundService(string name, ILogger logger, BackgroundServiceManager manager, TimeSpan? executeInterval = null)
+ public ExtendedBackgroundService(string name, string description, ILogger logger, BackgroundServiceManager manager, TimeSpan? executeInterval = null)
{
Name = name;
+ Description = description;
_logger = logger;
manager.RegisterService(this);
- ExecuteInterval = executeInterval ?? TimeSpan.Zero;
+ ExecuteInterval = executeInterval ?? TimeSpan.FromMinutes(1);
}
protected sealed override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -58,7 +60,7 @@ public abstract class ExtendedBackgroundService : BackgroundService
}
}
- protected void LogProgress(string message, LogSeverity severity = LogSeverity.Info) => ProgressLog.Add(new ServiceProgress(message, DateTime.UtcNow, severity));
+ protected void LogEvent(string message, LogSeverity severity = LogSeverity.Info) => ProgressEvents.Add(new ServiceEvent(Name, message, DateTime.UtcNow, severity));
public void Pause()
{
@@ -101,4 +103,4 @@ public enum ServiceState
Paused
}
-public record ServiceProgress(string Message, DateTime StartTime, LogSeverity Severity);
\ No newline at end of file
+public record ServiceEvent(string Source, string Message, DateTime Date, LogSeverity Severity);
\ No newline at end of file
diff --git a/Manager.App/Services/System/ClientService.cs b/Manager.App/Services/System/ClientService.cs
index f1c1878..ed6dbb7 100644
--- a/Manager.App/Services/System/ClientService.cs
+++ b/Manager.App/Services/System/ClientService.cs
@@ -1,4 +1,5 @@
using System.Net;
+using DotBased.Logging;
using DotBased.Monads;
using Manager.App.Models.Library;
using Manager.Data.Entities.LibraryContext;
@@ -6,7 +7,8 @@ using Manager.YouTube;
namespace Manager.App.Services.System;
-public class ClientService(IServiceScopeFactory scopeFactory, ILogger logger, BackgroundServiceManager serviceManager) : ExtendedBackgroundService("ClientService", logger, serviceManager)
+public class ClientService(IServiceScopeFactory scopeFactory, ILogger logger, BackgroundServiceManager serviceManager)
+ : ExtendedBackgroundService("ClientService", "Managing YouTube clients", logger, serviceManager, TimeSpan.FromMilliseconds(100))
{
private readonly List _clients = [];
private CancellationToken _cancellationToken;
@@ -18,13 +20,15 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger();
- LogProgress("Initializing service...");
+ LogEvent("Initializing service...");
Pause();
}
protected override async Task ExecuteServiceAsync(CancellationToken stoppingToken)
{
-
+ LogEvent("Sending event...");
+ LogEvent("Sending warning event...", LogSeverity.Warning);
+ LogEvent("Sending error event...", LogSeverity.Error);
}
private void CancellationRequested()
diff --git a/Manager.App/appsettings.Development.json b/Manager.App/appsettings.Development.json
index abae8bb..91be6ab 100644
--- a/Manager.App/appsettings.Development.json
+++ b/Manager.App/appsettings.Development.json
@@ -3,11 +3,11 @@
"Logging": {
"Severity": "Debug",
"SeverityFilters":{
- "Microsoft": "Debug",
+ "Microsoft": "Info",
"Microsoft.Hosting.Lifetime": "Debug",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Authentication": "Debug",
- "MudBlazor": "Debug"
+ "MudBlazor": "Info"
}
}
},