Auto sync trigger and cancellation support.
This commit is contained in:
@@ -81,6 +81,11 @@ public interface ISynchronizationManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
IWinoSynchronizerBase CreateSynchronizerForAccount(MailAccount account);
|
IWinoSynchronizerBase CreateSynchronizerForAccount(MailAccount account);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancels ongoing synchronizations for the given account.
|
||||||
|
/// </summary>
|
||||||
|
Task CancelSynchronizationsAsync(Guid accountId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Destroys the synchronizer for the given account.
|
/// Destroys the synchronizer for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
public static SynchronizationManager Instance => _instance.Value;
|
public static SynchronizationManager Instance => _instance.Value;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Guid, IWinoSynchronizerBase> _synchronizerCache = new();
|
private readonly ConcurrentDictionary<Guid, IWinoSynchronizerBase> _synchronizerCache = new();
|
||||||
|
private readonly ConcurrentDictionary<Guid, CancellationTokenSource> _accountSynchronizationCancellationSources = new();
|
||||||
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||||
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
|
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}",
|
_logger.Information("Starting mail synchronization for account {AccountId} with type {SyncType}",
|
||||||
options.AccountId, options.Type);
|
options.AccountId, options.Type);
|
||||||
|
|
||||||
|
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
|
||||||
|
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
|
cancellationToken,
|
||||||
|
accountCancellationSource.Token);
|
||||||
|
|
||||||
try
|
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}",
|
_logger.Information("Mail synchronization completed for account {AccountId} with state {State}",
|
||||||
options.AccountId, result.CompletedState);
|
options.AccountId, result.CompletedState);
|
||||||
@@ -156,6 +162,11 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.Information("Mail synchronization canceled for account {AccountId}", options.AccountId);
|
||||||
|
return MailSynchronizationResult.Canceled;
|
||||||
|
}
|
||||||
catch (AuthenticationAttentionException authEx)
|
catch (AuthenticationAttentionException authEx)
|
||||||
{
|
{
|
||||||
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
_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}",
|
_logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}",
|
||||||
options.AccountId, options.Type);
|
options.AccountId, options.Type);
|
||||||
|
|
||||||
|
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
|
||||||
|
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
|
cancellationToken,
|
||||||
|
accountCancellationSource.Token);
|
||||||
|
|
||||||
try
|
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}",
|
_logger.Information("Calendar synchronization completed for account {AccountId} with state {State}",
|
||||||
options.AccountId, result.CompletedState);
|
options.AccountId, result.CompletedState);
|
||||||
@@ -363,6 +379,11 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.Information("Calendar synchronization canceled for account {AccountId}", options.AccountId);
|
||||||
|
return CalendarSynchronizationResult.Canceled;
|
||||||
|
}
|
||||||
catch (AuthenticationAttentionException authEx)
|
catch (AuthenticationAttentionException authEx)
|
||||||
{
|
{
|
||||||
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
_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>
|
/// <summary>
|
||||||
/// Destroys the synchronizer for the given account.
|
/// Destroys the synchronizer for the given account.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -498,6 +551,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
public async Task DestroySynchronizerAsync(Guid accountId)
|
public async Task DestroySynchronizerAsync(Guid accountId)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
|
await CancelSynchronizationsAsync(accountId);
|
||||||
|
|
||||||
if (_synchronizerCache.TryRemove(accountId, out var synchronizer))
|
if (_synchronizerCache.TryRemove(accountId, out var synchronizer))
|
||||||
{
|
{
|
||||||
@@ -506,6 +560,10 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
await synchronizer.KillSynchronizerAsync();
|
await synchronizer.KillSynchronizerAsync();
|
||||||
_logger.Information("Destroyed synchronizer for account {AccountId}", accountId);
|
_logger.Information("Destroyed synchronizer for account {AccountId}", accountId);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.Information("Synchronizer destruction canceled for account {AccountId}", accountId);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
|
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -32,7 +34,13 @@ public partial class App : WinoApplication,
|
|||||||
IRecipient<NewMailSynchronizationRequested>,
|
IRecipient<NewMailSynchronizationRequested>,
|
||||||
IRecipient<NewCalendarSynchronizationRequested>
|
IRecipient<NewCalendarSynchronizationRequested>
|
||||||
{
|
{
|
||||||
|
private const int InboxSyncsPerFullSync = 20;
|
||||||
private ISynchronizationManager? _synchronizationManager;
|
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()
|
public App()
|
||||||
{
|
{
|
||||||
@@ -136,6 +144,12 @@ public partial class App : WinoApplication,
|
|||||||
await InitializeServicesAsync();
|
await InitializeServicesAsync();
|
||||||
|
|
||||||
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
|
_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.
|
// Check if launched from toast notification.
|
||||||
if (IsNotificationActivation(out AppNotificationActivatedEventArgs toastArgs))
|
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>
|
/// <summary>
|
||||||
/// Handles activation redirected from another instance (single-instancing).
|
/// Handles activation redirected from another instance (single-instancing).
|
||||||
/// This is called when a second instance tries to launch and redirects to this existing instance.
|
/// 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);
|
set => SetPropertyAndSave(nameof(MailItemDisplayMode), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSemanticZoomEnabled
|
|
||||||
{
|
|
||||||
get => _configurationService.Get(nameof(IsSemanticZoomEnabled), true);
|
|
||||||
set => SetPropertyAndSave(nameof(IsSemanticZoomEnabled), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsHardDeleteProtectionEnabled
|
public bool IsHardDeleteProtectionEnabled
|
||||||
{
|
{
|
||||||
get => _configurationService.Get(nameof(IsHardDeleteProtectionEnabled), true);
|
get => _configurationService.Get(nameof(IsHardDeleteProtectionEnabled), true);
|
||||||
|
|||||||
Reference in New Issue
Block a user