Support for killing synchronizers.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user