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

View File

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

View File

@@ -7,5 +7,6 @@ namespace Wino.Core.Domain.Interfaces
{
Task<IWinoSynchronizerBase> GetAccountSynchronizerAsync(Guid accountId);
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="cancellationToken">Cancellation token.</param>
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.
/// Makes sure that we don't have too many connections to the server.
/// 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>
/// <param name="customServerInformation">Connection/Authentication info to be used to configure ImapClient.</param>
public class ImapClientPool : IDisposable
@@ -57,6 +55,7 @@ namespace Wino.Core.Integration
private readonly CustomServerInformation _customServerInformation;
private readonly Stream _protocolLogStream;
private readonly ILogger _logger = Log.ForContext<ImapClientPool>();
private bool _disposedValue;
public ImapClientPool(ImapClientPoolOptions imapClientPoolOptions)
{
@@ -185,7 +184,7 @@ namespace Wino.Core.Integration
_clients.TryPop(out _);
item.Dispose();
}
else
else if (!_disposedValue)
{
_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()
{
_clients.ForEach(client =>
{
lock (client.SyncRoot)
{
client.Disconnect(true);
}
});
_clients.ForEach(client =>
{
client.Dispose();
});
_clients.Clear();
_protocolLogStream?.Dispose();
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -109,5 +109,18 @@ namespace Wino.Core.Services
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>
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
// TODO: What if account is deleted during synchronization?
public bool CancelActiveSynchronization() => true;
/// <summary>
/// Refreshes remote mail account profile if possible.
/// 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
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
{
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;
_applicationConfiguration = applicationConfiguration;
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, CreateAccountProtocolLogFileStream());
var protocolLogStream = CreateAccountProtocolLogFileStream();
var poolOptions = ImapClientPoolOptions.CreateDefault(Account.ServerInformation, protocolLogStream);
_clientPool = new ImapClientPool(poolOptions);
}
@@ -66,7 +67,7 @@ namespace Wino.Core.Synchronizers.Mail
{
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.
if (File.Exists(logFile)) File.Delete(logFile);
@@ -313,6 +314,8 @@ namespace Wino.Core.Synchronizers.Mail
var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested) return MailSynchronizationResult.Canceled;
downloadedMessageIds.AddRange(folderDownloadedMessageIds);
}
}
@@ -524,6 +527,11 @@ namespace Wino.Core.Synchronizers.Mail
if (remoteFolder.IsNamespace && !remoteFolder.Attributes.HasFlag(FolderAttributes.Inbox) || !remoteFolder.Exists)
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);
if (existingLocalFolder == null)
@@ -641,6 +649,10 @@ namespace Wino.Core.Synchronizers.Mail
goto retry;
}
catch (OperationCanceledException)
{
// Ignore cancellations.
}
catch (Exception)
{
@@ -716,6 +728,10 @@ namespace Wino.Core.Synchronizers.Mail
Log.Warning(ioException, "Idle client received IO exception.");
reconnect = true;
}
catch (OperationCanceledException)
{
reconnect = !IsDisposing;
}
catch (Exception ex)
{
Log.Warning(ex, "Idle client failed to start.");
@@ -782,5 +798,14 @@ namespace Wino.Core.Synchronizers.Mail
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);
}
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
{
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 WinoSynchronizer(MailAccount account) : base(account)
{
}
protected WinoSynchronizer(MailAccount account) : base(account) { }
/// <summary>
/// How many items per single HTTP call can be modified.
@@ -91,9 +91,10 @@ namespace Wino.Core.Synchronizers
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);
@@ -413,6 +414,20 @@ namespace Wino.Core.Synchronizers
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.Navigation;
using Wino.Messaging.Client.Navigation;
using Wino.Messaging.Server;
using Wino.Messaging.UI;
namespace Wino.Mail.ViewModels
@@ -20,6 +21,7 @@ namespace Wino.Mail.ViewModels
{
private readonly IMailDialogService _dialogService;
private readonly IAccountService _accountService;
private readonly IWinoServerConnectionManager _serverConnectionManager;
private readonly IFolderService _folderService;
public MailAccount Account { get; set; }
@@ -48,10 +50,12 @@ namespace Wino.Mail.ViewModels
public AccountDetailsPageViewModel(IMailDialogService dialogService,
IAccountService accountService,
IWinoServerConnectionManager serverConnectionManager,
IFolderService folderService)
{
_dialogService = dialogService;
_accountService = accountService;
_serverConnectionManager = serverConnectionManager;
_folderService = folderService;
}
@@ -102,13 +106,17 @@ namespace Wino.Mail.ViewModels
if (!confirmation)
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)

View File

@@ -113,9 +113,6 @@ namespace Wino.Mail.ViewModels
};
creationDialog.ShowDialog(accountCreationCancellationTokenSource);
await Task.Delay(1000);
creationDialog.State = AccountCreationDialogState.SigningIn;
string tokenInformation = string.Empty;
@@ -140,16 +137,17 @@ namespace Wino.Mail.ViewModels
}
else
{
// Hanle special imap providers like iCloud and Yahoo.
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.
customServerInformation = _specialImapProviderConfigResolver.GetServerInformation(createdAccount, accountCreationDialogResult);
customServerInformation.Id = Guid.NewGuid();
customServerInformation.AccountId = createdAccount.Id;
createdAccount.SenderName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
createdAccount.Address = customServerInformation.Address;
await _imapTestService.TestImapConnectionAsync(customServerInformation, true);
}
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(TerminateServerRequested) => App.Current.Services.GetService<TerminateServerRequestHandler>(),
nameof(ImapConnectivityTestRequested) => App.Current.Services.GetService<ImapConnectivityTestHandler>(),
nameof(KillAccountSynchronizerRequested) => App.Current.Services.GetService<KillAccountSynchronizerHandler>(),
_ => throw new Exception($"Server handler for {typeName} is not registered."),
};
}
@@ -42,6 +43,7 @@ namespace Wino.Server.Core
serviceCollection.AddTransient<ServerTerminationModeHandler>();
serviceCollection.AddTransient<TerminateServerRequestHandler>();
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();
break;
case nameof(KillAccountSynchronizerRequested):
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<KillAccountSynchronizerRequested>(messageJson, _jsonSerializerOptions));
break;
default:
Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync.");
break;

View File

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