Update 0.1.8 - Fix interface & ajout syncshell perma/temp
This commit is contained in:
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
225
MareSynchronos/Services/TemporarySyncshellNotificationService.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class TemporarySyncshellNotificationService : MediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private static readonly int[] NotificationThresholdMinutes = [30, 15, 5, 1];
|
||||
private readonly ApiController _apiController;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly Lock _stateLock = new();
|
||||
private readonly Dictionary<string, TrackedGroup> _trackedGroups = new(StringComparer.Ordinal);
|
||||
private CancellationTokenSource? _loopCts;
|
||||
private Task? _loopTask;
|
||||
|
||||
public TemporarySyncshellNotificationService(ILogger<TemporarySyncshellNotificationService> logger, MareMediator mediator, PairManager pairManager, ApiController apiController)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_pairManager = pairManager;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_loopCts = new CancellationTokenSource();
|
||||
Mediator.Subscribe<ConnectedMessage>(this, _ => ResetTrackedGroups());
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, _ => ResetTrackedGroups());
|
||||
_loopTask = Task.Run(() => MonitorLoopAsync(_loopCts.Token), _loopCts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.UnsubscribeAll(this);
|
||||
if (_loopCts == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_loopCts.Cancel();
|
||||
if (_loopTask != null)
|
||||
{
|
||||
await _loopTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loopTask = null;
|
||||
_loopCts.Dispose();
|
||||
_loopCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MonitorLoopAsync(CancellationToken ct)
|
||||
{
|
||||
var delay = TimeSpan.FromSeconds(30);
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
CheckGroups();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to check temporary syncshell expirations");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(delay, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckGroups()
|
||||
{
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
var groupsSnapshot = _pairManager.Groups.Values.ToList();
|
||||
var notifications = new List<NotificationPayload>();
|
||||
var expiredGroups = new List<GroupFullInfoDto>();
|
||||
var seenTemporaryGids = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
using (var guard = _stateLock.EnterScope())
|
||||
{
|
||||
foreach (var group in groupsSnapshot)
|
||||
{
|
||||
if (!group.IsTemporary || group.ExpiresAt == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_apiController.UID) || !string.Equals(group.OwnerUID, _apiController.UID, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var gid = group.Group.GID;
|
||||
seenTemporaryGids.Add(gid);
|
||||
var expiresAtUtc = NormalizeToUtc(group.ExpiresAt.Value);
|
||||
var remaining = expiresAtUtc - nowUtc;
|
||||
|
||||
if (!_trackedGroups.TryGetValue(gid, out var state))
|
||||
{
|
||||
state = new TrackedGroup(expiresAtUtc);
|
||||
_trackedGroups[gid] = state;
|
||||
}
|
||||
else if (state.ExpiresAtUtc != expiresAtUtc)
|
||||
{
|
||||
state.UpdateExpiresAt(expiresAtUtc);
|
||||
}
|
||||
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
_trackedGroups.Remove(gid);
|
||||
expiredGroups.Add(group);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!state.LastRemaining.HasValue)
|
||||
{
|
||||
state.UpdateRemaining(remaining);
|
||||
continue;
|
||||
}
|
||||
|
||||
var previousRemaining = state.LastRemaining.Value;
|
||||
|
||||
foreach (var thresholdMinutes in NotificationThresholdMinutes)
|
||||
{
|
||||
var threshold = TimeSpan.FromMinutes(thresholdMinutes);
|
||||
if (previousRemaining > threshold && remaining <= threshold)
|
||||
{
|
||||
notifications.Add(new NotificationPayload(group, thresholdMinutes, expiresAtUtc));
|
||||
}
|
||||
}
|
||||
|
||||
state.UpdateRemaining(remaining);
|
||||
}
|
||||
|
||||
var toRemove = _trackedGroups.Keys.Where(k => !seenTemporaryGids.Contains(k)).ToList();
|
||||
foreach (var gid in toRemove)
|
||||
{
|
||||
_trackedGroups.Remove(gid);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var expiredGroup in expiredGroups)
|
||||
{
|
||||
Logger.LogInformation("Temporary syncshell {gid} expired locally; removing", expiredGroup.Group.GID);
|
||||
_pairManager.RemoveGroup(expiredGroup.Group);
|
||||
}
|
||||
|
||||
foreach (var notification in notifications)
|
||||
{
|
||||
PublishNotification(notification.Group, notification.ThresholdMinutes, notification.ExpiresAtUtc);
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishNotification(GroupFullInfoDto group, int thresholdMinutes, DateTime expiresAtUtc)
|
||||
{
|
||||
string displayName = string.IsNullOrWhiteSpace(group.GroupAlias) ? group.Group.GID : group.GroupAlias!;
|
||||
string threshold = thresholdMinutes == 1 ? "1 minute" : $"{thresholdMinutes} minutes";
|
||||
string expiresLocal = expiresAtUtc.ToLocalTime().ToString("t", CultureInfo.CurrentCulture);
|
||||
|
||||
string message = $"La Syncshell temporaire \"{displayName}\" sera supprimee dans {threshold} (a {expiresLocal}).";
|
||||
Mediator.Publish(new NotificationMessage("Syncshell temporaire", message, NotificationType.Warning, TimeSpan.FromSeconds(6)));
|
||||
}
|
||||
|
||||
private static DateTime NormalizeToUtc(DateTime expiresAt)
|
||||
{
|
||||
return expiresAt.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => expiresAt,
|
||||
DateTimeKind.Local => expiresAt.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(expiresAt, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
|
||||
private void ResetTrackedGroups()
|
||||
{
|
||||
using (var guard = _stateLock.EnterScope())
|
||||
{
|
||||
_trackedGroups.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TrackedGroup
|
||||
{
|
||||
public TrackedGroup(DateTime expiresAtUtc)
|
||||
{
|
||||
ExpiresAtUtc = expiresAtUtc;
|
||||
}
|
||||
|
||||
public DateTime ExpiresAtUtc { get; private set; }
|
||||
public TimeSpan? LastRemaining { get; private set; }
|
||||
|
||||
public void UpdateExpiresAt(DateTime expiresAtUtc)
|
||||
{
|
||||
ExpiresAtUtc = expiresAtUtc;
|
||||
LastRemaining = null;
|
||||
}
|
||||
|
||||
public void UpdateRemaining(TimeSpan remaining)
|
||||
{
|
||||
LastRemaining = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record NotificationPayload(GroupFullInfoDto Group, int ThresholdMinutes, DateTime ExpiresAtUtc);
|
||||
}
|
||||
Reference in New Issue
Block a user