Support for killing synchronizers.

This commit is contained in:
Burak Kaan Köse
2025-01-25 00:00:10 +01:00
parent 20010e77ae
commit 973ab1570d
19 changed files with 189 additions and 56 deletions

View File

@@ -81,7 +81,7 @@ namespace Wino.Calendar.ViewModels
if (accountCreationDialogResult == null) return; if (accountCreationDialogResult == null) return;
var accountCreationCancellationTokenSource = new CancellationTokenSource(); var accountCreationCancellationTokenSource = new CancellationTokenSource();
var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType); var accountCreationDialog = CalendarDialogService.GetAccountCreationDialog(accountCreationDialogResult);
accountCreationDialog.ShowDialog(accountCreationCancellationTokenSource); accountCreationDialog.ShowDialog(accountCreationCancellationTokenSource);
accountCreationDialog.State = AccountCreationDialogState.SigningIn; accountCreationDialog.State = AccountCreationDialogState.SigningIn;
@@ -92,7 +92,6 @@ namespace Wino.Calendar.ViewModels
{ {
ProviderType = accountCreationDialogResult.ProviderType, ProviderType = accountCreationDialogResult.ProviderType,
Name = accountCreationDialogResult.AccountName, Name = accountCreationDialogResult.AccountName,
AccountColorHex = accountCreationDialogResult.AccountColorHex,
Id = Guid.NewGuid() Id = Guid.NewGuid()
}; };
@@ -104,13 +103,8 @@ namespace Wino.Calendar.ViewModels
if (accountCreationDialog.State == AccountCreationDialogState.Canceled) if (accountCreationDialog.State == AccountCreationDialogState.Canceled)
throw new AccountSetupCanceledException(); throw new AccountSetupCanceledException();
tokenInformationResponse.ThrowIfFailed(); tokenInformationResponse.ThrowIfFailed();
//var tokenInformation = tokenInformationResponse.Data;
//createdAccount.Address = tokenInformation.Address;
//tokenInformation.AccountId = createdAccount.Id;
await AccountService.CreateAccountAsync(createdAccount, null); await AccountService.CreateAccountAsync(createdAccount, null);
// Sync profile information if supported. // Sync profile information if supported.

View File

@@ -23,12 +23,6 @@ namespace Wino.Core.Domain.Interfaces
/// <param name="request">Request to queue.</param> /// <param name="request">Request to queue.</param>
void QueueRequest(IRequestBase request); void QueueRequest(IRequestBase request);
/// <summary>
/// TODO
/// </summary>
/// <returns>Whether active synchronization is stopped or not.</returns>
bool CancelActiveSynchronization();
/// <summary> /// <summary>
/// Synchronizes profile information with the server. /// Synchronizes profile information with the server.
/// Sender name and Profile picture are updated. /// Sender name and Profile picture are updated.

View File

@@ -7,5 +7,6 @@ namespace Wino.Core.Domain.Interfaces
{ {
Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId); Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId);
Task InitializeAsync(); Task InitializeAsync();
Task DeleteSynchronizerAsync(Guid accountId);
} }
} }

View File

@@ -28,5 +28,12 @@ namespace Wino.Core.Domain.Interfaces
/// <param name="transferProgress">Optional progress reporting for download operation.</param> /// <param name="transferProgress">Optional progress reporting for download operation.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
/// <summary>
/// 1. Cancel active synchronization.
/// 2. Stop all running tasks.
/// 3. Dispose all resources.
/// </summary>
Task KillSynchronizerAsync();
} }
} }

View File

@@ -25,8 +25,6 @@ namespace Wino.Core.Integration
/// Provides a pooling mechanism for ImapClient. /// Provides a pooling mechanism for ImapClient.
/// Makes sure that we don't have too many connections to the server. /// Makes sure that we don't have too many connections to the server.
/// Rents a connected & authenticated client from the pool all the time. /// Rents a connected & authenticated client from the pool all the time.
/// TODO: Keeps the clients alive by sending NOOP command periodically.
/// TODO: Listens to the Inbox folder for new messages.
/// </summary> /// </summary>
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param> /// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
public class ImapClientPool : IDisposable public class ImapClientPool : IDisposable
@@ -57,6 +55,7 @@ namespace Wino.Core.Integration
private readonly CustomServerInformation _customServerInformation; private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream; private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>(); private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private bool _disposedValue;
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions) public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{ {
@@ -185,7 +184,7 @@ namespace Wino.Core.Integration
_clients.TryPop(out _); _clients.TryPop(out _);
item.Dispose(); item.Dispose();
} }
else else if (!_disposedValue)
{ {
_clients.Push(item); _clients.Push(item);
} }
@@ -332,24 +331,38 @@ namespace Wino.Core.Integration
}; };
} }
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
}
_disposedValue = true;
}
}
public void Dispose() public void Dispose()
{ {
_clients.ForEach(client => Dispose(disposing: true);
{ GC.SuppressFinalize(this);
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
} }
} }
} }

View File

@@ -109,5 +109,18 @@ namespace Wino.Core.Services
isInitialized = true; isInitialized = true;
} }
public async Task DeleteSynchronizerAsync(Guid accountId)
{
var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId);
if (synchronizer != null)
{
// Stop the current synchronization.
await synchronizer.KillSynchronizerAsync();
synchronizerCache.Remove(synchronizer);
}
}
} }
} }

View File

@@ -51,9 +51,6 @@ namespace Wino.Core.Synchronizers
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default); public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
// TODO: What if account is deleted during synchronization?
public bool CancelActiveSynchronization() => true;
/// <summary> /// <summary>
/// Refreshes remote mail account profile if possible. /// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step. /// Profile picture, sender name and mailbox settings (todo) will be handled in this step.

View File

@@ -1193,5 +1193,15 @@ namespace Wino.Core.Synchronizers.Mail
} }
#endregion #endregion
public override async Task KillSynchronizerAsync()
{
await base.KillSynchronizerAsync();
_gmailService.Dispose();
_peopleService.Dispose();
_calendarService.Dispose();
_googleHttpClient.Dispose();
}
} }
} }

View File

@@ -139,11 +139,14 @@ namespace Wino.Core.Synchronizers.ImapSync
} }
finally finally
{ {
if (remoteFolder != null) if (!cancellationToken.IsCancellationRequested)
{ {
if (remoteFolder.IsOpen) if (remoteFolder != null)
{ {
await remoteFolder.CloseAsync().ConfigureAwait(false); if (remoteFolder.IsOpen)
{
await remoteFolder.CloseAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
} }
} }
} }

View File

@@ -57,7 +57,8 @@ namespace Wino.Core.Synchronizers.Mail
_imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider; _imapSynchronizationStrategyProvider = imapSynchronizationStrategyProvider;
_applicationConfiguration = applicationConfiguration; _applicationConfiguration = applicationConfiguration;
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, CreateAccountProtocolLogFileStream()); var protocolLogStream = CreateAccountProtocolLogFileStream();
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream);
_clientPool = new ImapClientPool(poolOptions); _clientPool = new ImapClientPool(poolOptions);
} }
@@ -66,7 +67,7 @@ namespace Wino.Core.Synchronizers.Mail
{ {
if (Account == null) throw new ArgumentNullException(nameof(Account)); if (Account == null) throw new ArgumentNullException(nameof(Account));
var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}.log"); var logFile = Path.Combine(_applicationConfiguration.ApplicationDataFolderPath, $"Protocol_{Account.Address}_{Account.Id}.log");
// Each session should start a new log. // Each session should start a new log.
if (File.Exists(logFile)) File.Delete(logFile); if (File.Exists(logFile)) File.Delete(logFile);
@@ -313,6 +314,8 @@ namespace Wino.Core.Synchronizers.Mail
var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false); var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested) return MailSynchronizationResult.Canceled;
downloadedMessageIds.AddRange(folderDownloadedMessageIds); downloadedMessageIds.AddRange(folderDownloadedMessageIds);
} }
} }
@@ -524,6 +527,11 @@ namespace Wino.Core.Synchronizers.Mail
if (remoteFolder.IsNamespace && !remoteFolder.Attributes.HasFlag(FolderAttributes.Inbox) || !remoteFolder.Exists) if (remoteFolder.IsNamespace && !remoteFolder.Attributes.HasFlag(FolderAttributes.Inbox) || !remoteFolder.Exists)
continue; continue;
// Check for NoSelect folders. These are not selectable folders.
// TODO: With new MailKit version 'CanOpen' will be implemented for ease of use. Use that one.
if (remoteFolder.Attributes.HasFlag(FolderAttributes.NoSelect))
continue;
var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.FullName); var existingLocalFolder = localFolders.FirstOrDefault(a => a.RemoteFolderId == remoteFolder.FullName);
if (existingLocalFolder == null) if (existingLocalFolder == null)
@@ -641,6 +649,10 @@ namespace Wino.Core.Synchronizers.Mail
goto retry; goto retry;
} }
catch (OperationCanceledException)
{
// Ignore cancellations.
}
catch (Exception) catch (Exception)
{ {
@@ -716,6 +728,10 @@ namespace Wino.Core.Synchronizers.Mail
Log.Warning(ioException, "Idle client received IO exception."); Log.Warning(ioException, "Idle client received IO exception.");
reconnect = true; reconnect = true;
} }
catch (OperationCanceledException)
{
reconnect = !IsDisposing;
}
catch (Exception ex) catch (Exception ex)
{ {
Log.Warning(ex, "Idle client failed to start."); Log.Warning(ex, "Idle client failed to start.");
@@ -782,5 +798,14 @@ namespace Wino.Core.Synchronizers.Mail
return Task.CompletedTask; return Task.CompletedTask;
} }
public override async Task KillSynchronizerAsync()
{
await base.KillSynchronizerAsync();
await StopIdleClientAsync();
// Make sure the client pool safely disconnects all ImapClients.
_clientPool.Dispose();
}
} }
} }

View File

@@ -1163,5 +1163,12 @@ namespace Wino.Core.Synchronizers.Mail
return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase); return !localCalendarName.Equals(remoteCalendarName, StringComparison.OrdinalIgnoreCase);
} }
public override async Task KillSynchronizerAsync()
{
await base.KillSynchronizerAsync();
_graphClient.Dispose();
}
} }
} }

View File

@@ -25,13 +25,13 @@ namespace Wino.Core.Synchronizers
{ {
public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType> : BaseSynchronizer<TBaseRequest>, IWinoSynchronizerBase
{ {
protected Dictionary<MailSynchronizationOptions, CancellationToken> PendingSynchronizationRequest = new(); protected bool IsDisposing { get; private set; }
protected Dictionary<MailSynchronizationOptions, CancellationTokenSource> PendingSynchronizationRequest = new();
protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>(); protected ILogger Logger = Log.ForContext<WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEventType>>();
protected WinoSynchronizer(MailAccount account) : base(account) protected WinoSynchronizer(MailAccount account) : base(account) { }
{
}
/// <summary> /// <summary>
/// How many items per single HTTP call can be modified. /// How many items per single HTTP call can be modified.
@@ -91,9 +91,10 @@ namespace Wino.Core.Synchronizers
return MailSynchronizationResult.Canceled; return MailSynchronizationResult.Canceled;
} }
PendingSynchronizationRequest.Add(options, cancellationToken); var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
activeSynchronizationCancellationToken = cancellationToken; PendingSynchronizationRequest.Add(options, newCancellationTokenSource);
activeSynchronizationCancellationToken = newCancellationTokenSource.Token;
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
@@ -413,6 +414,20 @@ namespace Wino.Core.Synchronizers
return ret; return ret;
} }
public virtual Task KillSynchronizerAsync()
{
IsDisposing = true;
CancelAllSynchronizations();
return Task.CompletedTask;
}
protected void CancelAllSynchronizations()
{
foreach (var request in PendingSynchronizationRequest)
{
request.Value.Cancel();
}
}
} }
} }

View File

@@ -12,6 +12,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
using Wino.Messaging.Server;
using Wino.Messaging.UI; using Wino.Messaging.UI;
namespace Wino.Mail.ViewModels namespace Wino.Mail.ViewModels
@@ -20,6 +21,7 @@ namespace Wino.Mail.ViewModels
{ {
private readonly IMailDialogService _dialogService; private readonly IMailDialogService _dialogService;
private readonly IAccountService _accountService; private readonly IAccountService _accountService;
private readonly IWinoServerConnectionManager _serverConnectionManager;
private readonly IFolderService _folderService; private readonly IFolderService _folderService;
public MailAccount Account { get; set; } public MailAccount Account { get; set; }
@@ -48,10 +50,12 @@ namespace Wino.Mail.ViewModels
public AccountDetailsPageViewModel(IMailDialogService dialogService, public AccountDetailsPageViewModel(IMailDialogService dialogService,
IAccountService accountService, IAccountService accountService,
IWinoServerConnectionManager serverConnectionManager,
IFolderService folderService) IFolderService folderService)
{ {
_dialogService = dialogService; _dialogService = dialogService;
_accountService = accountService; _accountService = accountService;
_serverConnectionManager = serverConnectionManager;
_folderService = folderService; _folderService = folderService;
} }
@@ -102,13 +106,17 @@ namespace Wino.Mail.ViewModels
if (!confirmation) if (!confirmation)
return; return;
await _accountService.DeleteAccountAsync(Account);
// TODO: Server: Cancel ongoing calls from server for this account. var isSynchronizerKilledResponse = await _serverConnectionManager.GetResponseAsync<bool, KillAccountSynchronizerRequested>(new KillAccountSynchronizerRequested(Account.Id));
_dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success); if (isSynchronizerKilledResponse.IsSuccess)
{
await _accountService.DeleteAccountAsync(Account);
Messenger.Send(new BackBreadcrumNavigationRequested()); _dialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success);
Messenger.Send(new BackBreadcrumNavigationRequested());
}
} }
public override async void OnNavigatedTo(NavigationMode mode, object parameters) public override async void OnNavigatedTo(NavigationMode mode, object parameters)

View File

@@ -113,9 +113,6 @@ namespace Wino.Mail.ViewModels
}; };
creationDialog.ShowDialog(accountCreationCancellationTokenSource); creationDialog.ShowDialog(accountCreationCancellationTokenSource);
await Task.Delay(1000);
creationDialog.State = AccountCreationDialogState.SigningIn; creationDialog.State = AccountCreationDialogState.SigningIn;
string tokenInformation = string.Empty; string tokenInformation = string.Empty;
@@ -140,16 +137,17 @@ namespace Wino.Mail.ViewModels
} }
else else
{ {
// Hanle special imap providers like iCloud and Yahoo.
if (accountCreationDialogResult.SpecialImapProviderDetails != null) if (accountCreationDialogResult.SpecialImapProviderDetails != null)
{ {
createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
createdAccount.Address = customServerInformation.Address;
// Special imap provider testing dialog. This is only available for iCloud and Yahoo. // Special imap provider testing dialog. This is only available for iCloud and Yahoo.
customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult); customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult);
customServerInformation.Id = Guid.NewGuid(); customServerInformation.Id = Guid.NewGuid();
customServerInformation.AccountId = createdAccount.Id; customServerInformation.AccountId = createdAccount.Id;
createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
createdAccount.Address = customServerInformation.Address;
await _imapTestService.TestImapConnectionAsync(customServerInformation, true); await _imapTestService.TestImapConnectionAsync(customServerInformation, true);
} }
else else

View File

@@ -0,0 +1,11 @@
using System;
using Wino.Core.Domain.Interfaces;
namespace Wino.Messaging.Server
{
/// <summary>
/// Client message that requests to kill the account synchronizer.
/// </summary>
/// <param name="AccountId">Account id to kill synchronizer for.</param>
public record KillAccountSynchronizerRequested(Guid AccountId) : IClientMessage;
}

View File

@@ -24,6 +24,7 @@ namespace Wino.Server.Core
nameof(ServerTerminationModeChanged) => App.Current.Services.GetService<ServerTerminationModeHandler>(), nameof(ServerTerminationModeChanged) => App.Current.Services.GetService<ServerTerminationModeHandler>(),
nameof(TerminateServerRequested) => App.Current.Services.GetService<TerminateServerRequestHandler>(), nameof(TerminateServerRequested) => App.Current.Services.GetService<TerminateServerRequestHandler>(),
nameof(ImapConnectivityTestRequested) => App.Current.Services.GetService<ImapConnectivityTestHandler>(), nameof(ImapConnectivityTestRequested) => App.Current.Services.GetService<ImapConnectivityTestHandler>(),
nameof(KillAccountSynchronizerRequested) => App.Current.Services.GetService<KillAccountSynchronizerHandler>(),
_ => throw new Exception($"Server handler for {typeName} is not registered."), _ => throw new Exception($"Server handler for {typeName} is not registered."),
}; };
} }
@@ -42,6 +43,7 @@ namespace Wino.Server.Core
serviceCollection.AddTransient<ServerTerminationModeHandler>(); serviceCollection.AddTransient<ServerTerminationModeHandler>();
serviceCollection.AddTransient<TerminateServerRequestHandler>(); serviceCollection.AddTransient<TerminateServerRequestHandler>();
serviceCollection.AddTransient<ImapConnectivityTestHandler>(); serviceCollection.AddTransient<ImapConnectivityTestHandler>();
serviceCollection.AddTransient<KillAccountSynchronizerHandler>();
} }
} }
} }

View File

@@ -0,0 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Server;
using Wino.Messaging.Server;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public class KillAccountSynchronizerHandler : ServerMessageHandler<KillAccountSynchronizerRequested, bool>
{
private readonly ISynchronizerFactory _synchronizerFactory;
public override WinoServerResponse<bool> FailureDefaultResponse(Exception ex)
=> WinoServerResponse<bool>.CreateErrorResponse(ex.Message);
public KillAccountSynchronizerHandler(ISynchronizerFactory synchronizerFactory)
{
_synchronizerFactory = synchronizerFactory;
}
protected override async Task<WinoServerResponse<bool>> HandleAsync(KillAccountSynchronizerRequested message, CancellationToken cancellationToken = default)
{
await _synchronizerFactory.DeleteSynchronizerAsync(message.AccountId);
return WinoServerResponse<bool>.CreateSuccessResponse(true);
}
}
}

View File

@@ -328,6 +328,9 @@ namespace Wino.Server
KillServer(); KillServer();
break; break;
case nameof(KillAccountSynchronizerRequested):
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<KillAccountSynchronizerRequested>(messageJson, _jsonSerializerOptions));
break;
default: default:
Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync."); Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync.");
break; break;

View File

@@ -319,6 +319,8 @@ namespace Wino.Services
} }
} }
ReportUIChange(new AccountRemovedMessage(account)); ReportUIChange(new AccountRemovedMessage(account));
} }