Auto sync trigger and cancellation support.
This commit is contained in:
@@ -81,6 +81,11 @@ public interface ISynchronizationManager
|
||||
/// </summary>
|
||||
IWinoSynchronizerBase CreateSynchronizerForAccount(MailAccount account);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels ongoing synchronizations for the given account.
|
||||
/// </summary>
|
||||
Task CancelSynchronizationsAsync(Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the synchronizer for the given account.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,7 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
public static SynchronizationManager Instance => _instance.Value;
|
||||
|
||||
private readonly ConcurrentDictionary<Guid, IWinoSynchronizerBase> _synchronizerCache = new();
|
||||
private readonly ConcurrentDictionary<Guid, CancellationTokenSource> _accountSynchronizationCancellationSources = new();
|
||||
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
|
||||
|
||||
@@ -141,9 +142,14 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
_logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}",
|
||||
options.AccountId, options.Type);
|
||||
|
||||
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
|
||||
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken,
|
||||
accountCancellationSource.Token);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await synchronizer.SynchronizeMailsAsync(options, cancellationToken);
|
||||
var result = await synchronizer.SynchronizeMailsAsync(options, linkedCancellationTokenSource.Token);
|
||||
|
||||
_logger.Information("Mail synchronization completed for account {AccountId} with state {State}",
|
||||
options.AccountId, result.CompletedState);
|
||||
@@ -156,6 +162,11 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Information("Mail synchronization canceled for account {AccountId}", options.AccountId);
|
||||
return MailSynchronizationResult.Canceled;
|
||||
}
|
||||
catch (AuthenticationAttentionException authEx)
|
||||
{
|
||||
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
||||
@@ -350,9 +361,14 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
_logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}",
|
||||
options.AccountId, options.Type);
|
||||
|
||||
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
|
||||
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken,
|
||||
accountCancellationSource.Token);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await synchronizer.SynchronizeCalendarEventsAsync(options, cancellationToken);
|
||||
var result = await synchronizer.SynchronizeCalendarEventsAsync(options, linkedCancellationTokenSource.Token);
|
||||
|
||||
_logger.Information("Calendar synchronization completed for account {AccountId} with state {State}",
|
||||
options.AccountId, result.CompletedState);
|
||||
@@ -363,6 +379,11 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Information("Calendar synchronization canceled for account {AccountId}", options.AccountId);
|
||||
return CalendarSynchronizationResult.Canceled;
|
||||
}
|
||||
catch (AuthenticationAttentionException authEx)
|
||||
{
|
||||
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
||||
@@ -491,6 +512,38 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all in-flight synchronizations for the given account.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account ID to cancel synchronizations for</param>
|
||||
public Task CancelSynchronizationsAsync(Guid accountId)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
if (_accountSynchronizationCancellationSources.TryRemove(accountId, out var cancellationSource))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!cancellationSource.IsCancellationRequested)
|
||||
{
|
||||
cancellationSource.Cancel();
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancellationSource.Dispose();
|
||||
}
|
||||
|
||||
_logger.Information("Canceled ongoing synchronizations for account {AccountId}", accountId);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the synchronizer for the given account.
|
||||
/// </summary>
|
||||
@@ -498,6 +551,7 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
public async Task DestroySynchronizerAsync(Guid accountId)
|
||||
{
|
||||
EnsureInitialized();
|
||||
await CancelSynchronizationsAsync(accountId);
|
||||
|
||||
if (_synchronizerCache.TryRemove(accountId, out var synchronizer))
|
||||
{
|
||||
@@ -506,6 +560,10 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
await synchronizer.KillSynchronizerAsync();
|
||||
_logger.Information("Destroyed synchronizer for account {AccountId}", accountId);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Information("Synchronizer destruction canceled for account {AccountId}", accountId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -32,7 +34,13 @@ public partial class App : WinoApplication,
|
||||
IRecipient<NewMailSynchronizationRequested>,
|
||||
IRecipient<NewCalendarSynchronizationRequested>
|
||||
{
|
||||
private const int InboxSyncsPerFullSync = 20;
|
||||
private ISynchronizationManager? _synchronizationManager;
|
||||
private IPreferencesService? _preferencesService;
|
||||
private IAccountService? _accountService;
|
||||
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
||||
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
||||
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
|
||||
|
||||
public App()
|
||||
{
|
||||
@@ -136,6 +144,12 @@ public partial class App : WinoApplication,
|
||||
await InitializeServicesAsync();
|
||||
|
||||
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
|
||||
_preferencesService = Services.GetRequiredService<IPreferencesService>();
|
||||
_accountService = Services.GetRequiredService<IAccountService>();
|
||||
|
||||
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||
RestartAutoSynchronizationLoop();
|
||||
|
||||
// Check if launched from toast notification.
|
||||
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
||||
@@ -463,6 +477,132 @@ public partial class App : WinoApplication,
|
||||
};
|
||||
}
|
||||
|
||||
private void PreferencesServiceChanged(object? sender, string propertyName)
|
||||
{
|
||||
if (propertyName != nameof(IPreferencesService.EmailSyncIntervalMinutes))
|
||||
return;
|
||||
|
||||
RestartAutoSynchronizationLoop();
|
||||
}
|
||||
|
||||
private void RestartAutoSynchronizationLoop()
|
||||
{
|
||||
if (_preferencesService == null)
|
||||
return;
|
||||
|
||||
StopAutoSynchronizationLoop();
|
||||
|
||||
int intervalMinutes = Math.Max(1, _preferencesService.EmailSyncIntervalMinutes);
|
||||
_autoSynchronizationLoopCts = new CancellationTokenSource();
|
||||
|
||||
_ = RunAutoSynchronizationLoopAsync(TimeSpan.FromMinutes(intervalMinutes), _autoSynchronizationLoopCts.Token);
|
||||
LogActivation($"Automatic sync loop started. Interval: {intervalMinutes} minute(s).");
|
||||
}
|
||||
|
||||
private void StopAutoSynchronizationLoop()
|
||||
{
|
||||
if (_autoSynchronizationLoopCts == null)
|
||||
return;
|
||||
|
||||
_autoSynchronizationLoopCts.Cancel();
|
||||
_autoSynchronizationLoopCts.Dispose();
|
||||
_autoSynchronizationLoopCts = null;
|
||||
}
|
||||
|
||||
private async Task RunAutoSynchronizationLoopAsync(TimeSpan interval, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExecuteAutoSynchronizationAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var timer = new PeriodicTimer(interval);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await ExecuteAutoSynchronizationAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogActivation($"Automatic sync loop failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteAutoSynchronizationAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_synchronizationManager == null || _accountService == null)
|
||||
return;
|
||||
|
||||
bool lockTaken = false;
|
||||
|
||||
try
|
||||
{
|
||||
lockTaken = await _autoSynchronizationSemaphore.WaitAsync(0, cancellationToken).ConfigureAwait(false);
|
||||
if (!lockTaken)
|
||||
return;
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||
var currentAccountIds = accounts.Select(a => a.Id).ToHashSet();
|
||||
_inboxSyncCounters.Keys.Where(a => !currentAccountIds.Contains(a)).ToList().ForEach(a => _inboxSyncCounters.Remove(a));
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_synchronizationManager.IsAccountSynchronizing(account.Id))
|
||||
continue;
|
||||
|
||||
var inboxSyncOptions = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Type = MailSynchronizationType.InboxOnly
|
||||
};
|
||||
|
||||
var inboxSyncResult = await _synchronizationManager.SynchronizeMailAsync(inboxSyncOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (inboxSyncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted)
|
||||
{
|
||||
_inboxSyncCounters.TryAdd(account.Id, 0);
|
||||
_inboxSyncCounters[account.Id]++;
|
||||
|
||||
if (_inboxSyncCounters[account.Id] >= InboxSyncsPerFullSync)
|
||||
{
|
||||
var fullSyncOptions = new MailSynchronizationOptions()
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Type = MailSynchronizationType.FullFolders
|
||||
};
|
||||
|
||||
await _synchronizationManager.SynchronizeMailAsync(fullSyncOptions, cancellationToken).ConfigureAwait(false);
|
||||
_inboxSyncCounters[account.Id] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!account.IsCalendarAccessGranted)
|
||||
continue;
|
||||
|
||||
var calendarOptions = new CalendarSynchronizationOptions()
|
||||
{
|
||||
AccountId = account.Id,
|
||||
Type = CalendarSynchronizationType.CalendarMetadata
|
||||
};
|
||||
|
||||
await _synchronizationManager.SynchronizeCalendarAsync(calendarOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (lockTaken)
|
||||
{
|
||||
_autoSynchronizationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles activation redirected from another instance (single-instancing).
|
||||
/// This is called when a second instance tries to launch and redirects to this existing instance.
|
||||
|
||||
@@ -50,12 +50,6 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
||||
set => SetPropertyAndSave(nameof(MailItemDisplayMode), value);
|
||||
}
|
||||
|
||||
public bool IsSemanticZoomEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsSemanticZoomEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsSemanticZoomEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsHardDeleteProtectionEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsHardDeleteProtectionEnabled), true);
|
||||
|
||||
Reference in New Issue
Block a user