Implement shutdown logic
MODIFY
WalletWasabi.Gui/CommandLine/MixerCommand.cs
WalletWasabi.Gui/CommandLine/MixerCommand.cs
@@ -49,9 +49,10 @@ namespace WalletWasabi.Gui.CommandLine
await Daemon.RunAsync(WalletName, MixAll, KeepMixAlive);
}
}
- catch (Exception)
+ catch (Exception ex)
{
Console.WriteLine($"commands: There was a problem interpreting the command, please review it.");
+ Logger.LogDebug<MixerCommand>(ex);
error = true;
}
Environment.Exit(error ? 1 : 0);

MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -4,6 +4,7 @@ using NBitcoin;
using NBitcoin.Protocol;
using NBitcoin.Protocol.Behaviors;
using NBitcoin.Protocol.Connectors;
+using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
@@ -169,6 +170,9 @@ namespace WalletWasabi.Gui
{
Application.Current?.MainWindow?.Close();
});
+ await DisposeAsync();
+
+ Logger.LogInfo($"Wasabi stopped gracefully.", Logger.InstanceGuid.ToString());
};
#endregion ProcessKillSubscription
@@ -541,21 +545,37 @@ namespace WalletWasabi.Gui
}
}
+ /// <summary>
+ /// 0: nobody called
+ /// 1: somebody called
+ /// 2: call finished
+ /// </summary>
+ private static long Dispose = 0; // To detect redundant calls
+
public static async Task DisposeAsync()
{
+ if (Interlocked.CompareExchange(ref Dispose, 1, 0) == 1)
+ {
+ while (Interlocked.Read(ref Dispose) != 2)
+ {
+ await Task.Delay(50);
+ }
+ return;
+ }
+
try
{
await DisposeInWalletDependentServicesAsync();
if (UpdateChecker != null)
{
- UpdateChecker?.Dispose();
+ await UpdateChecker?.StopAsync();
Logger.LogInfo($"{nameof(UpdateChecker)} is stopped.", nameof(Global));
}
if (Synchronizer != null)
{
- Synchronizer?.Dispose();
+ await Synchronizer?.StopAsync();
Logger.LogInfo($"{nameof(Synchronizer)} is stopped.", nameof(Global));
}
@@ -571,6 +591,11 @@ namespace WalletWasabi.Gui
if (Nodes != null)
{
+ Nodes?.Disconnect();
+ while (Nodes.ConnectedNodes.Any(x => x.IsConnected))
+ {
+ await Task.Delay(50);
+ }
Nodes?.Dispose();
Logger.LogInfo($"{nameof(Nodes)} are disposed.", nameof(Global));
}
@@ -583,14 +608,28 @@ namespace WalletWasabi.Gui
if (TorManager != null)
{
- TorManager?.Dispose();
+ await TorManager?.StopAsync();
Logger.LogInfo($"{nameof(TorManager)} is stopped.", nameof(Global));
}
+
+ try
+ {
+ await AsyncMutex.WaitForAllMutexToCloseAsync();
+ Logger.LogInfo($"{nameof(AsyncMutex)}(es) are stopped.", nameof(Global));
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error during stopping {nameof(AsyncMutex)}: {ex}", nameof(Global));
+ }
}
catch (Exception ex)
{
Logger.LogWarning(ex, nameof(Global));
}
+ finally
+ {
+ Interlocked.Exchange(ref Dispose, 2);
+ }
}
}
}

MODIFY
WalletWasabi.Gui/MainWindow.xaml.cs
WalletWasabi.Gui/MainWindow.xaml.cs
@@ -109,11 +109,22 @@ namespace WalletWasabi.Gui
await Global.UiConfig.ToFileAsync();
Logging.Logger.LogInfo<UiConfig>("UiConfig is saved.");
}
+
+ Hide();
+ var wm = IoC.Get<IShell>().Documents?.OfType<WalletManagerViewModel>().FirstOrDefault();
+ if (wm != null)
+ {
+ wm.OnClose();
+ Logging.Logger.LogInfo<MainWindowViewModel>($"{nameof(WalletManagerViewModel)} closed, hwi enumeration stopped.");
+ }
+
+ await Global.DisposeAsync();
}
catch (Exception ex)
{
Logging.Logger.LogWarning<MainWindow>(ex);
}
+
Interlocked.Exchange(ref _closingState, 2); //now we can close the app
Close(); // start the closing process. Will call MainWindow_ClosingAsync again!
}

MODIFY
WalletWasabi.Tests/P2pTests.cs
WalletWasabi.Tests/P2pTests.cs
@@ -157,6 +157,8 @@ namespace WalletWasabi.Tests
addressManager?.SavePeerFile(addressManagerFilePath, network);
Logger.LogInfo<P2pTests>($"Saved {nameof(AddressManager)} to `{addressManagerFilePath}`.");
nodes?.Dispose();
+
+ await syncer?.StopAsync();
}
}

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -305,7 +305,8 @@ namespace WalletWasabi.Tests
{
(string password, RPCClient rpc, Network network, CcjCoordinator coordinator, ServiceConfiguration serviceConfiguration, BitcoinStore bitcoinStore) = await InitializeTestEnvironmentAsync(1);
- using (var synchronizer = new WasabiSynchronizer(rpc.Network, bitcoinStore, new Uri(RegTestFixture.BackendEndPoint), null))
+ var synchronizer = new WasabiSynchronizer(rpc.Network, bitcoinStore, new Uri(RegTestFixture.BackendEndPoint), null);
+ try
{
synchronizer.Start(requestInterval: TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), 1000);
@@ -358,6 +359,10 @@ namespace WalletWasabi.Tests
Assert.Null(filter.Filter);
}
}
+ finally
+ {
+ await synchronizer?.StopAsync();
+ }
}
[Fact]
@@ -383,8 +388,8 @@ namespace WalletWasabi.Tests
var node = RegTestFixture.BackendRegTestNode;
- using (var synchronizer = new WasabiSynchronizer(rpc.Network, bitcoinStore, new Uri(RegTestFixture.BackendEndPoint), null))
- {
+ var synchronizer = new WasabiSynchronizer(rpc.Network, bitcoinStore, new Uri(RegTestFixture.BackendEndPoint), null);
+
try
{
synchronizer.Start(requestInterval: TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(5), 1000);
@@ -478,7 +483,7 @@ namespace WalletWasabi.Tests
finally
{
bitcoinStore.IndexStore.Reorged -= ReorgTestAsync_Downloader_Reorged;
- }
+ await synchronizer?.StopAsync();
}
}
@@ -695,7 +700,7 @@ namespace WalletWasabi.Tests
wallet.NewFilterProcessed -= Wallet_NewFilterProcessed;
wallet?.Dispose();
// Dispose wasabi synchronizer service.
- synchronizer?.Dispose();
+ await synchronizer?.StopAsync();
// Dispose mempool service.
memPoolService.TransactionReceived -= WalletTestsAsync_MemPoolService_TransactionReceived;
@@ -1169,7 +1174,7 @@ namespace WalletWasabi.Tests
wallet.NewFilterProcessed -= Wallet_NewFilterProcessed;
wallet?.Dispose();
// Dispose wasabi synchronizer service.
- synchronizer?.Dispose();
+ await synchronizer?.StopAsync();
// Dispose connection service.
nodes?.Dispose();
// Dispose mempool serving node.
@@ -1332,7 +1337,7 @@ namespace WalletWasabi.Tests
{
wallet?.Dispose();
// Dispose wasabi synchronizer service.
- synchronizer?.Dispose();
+ await synchronizer?.StopAsync();
// Dispose connection service.
nodes?.Dispose();
// Dispose mempool serving node.
@@ -1496,7 +1501,7 @@ namespace WalletWasabi.Tests
{
wallet?.Dispose();
// Dispose wasabi synchronizer service.
- synchronizer?.Dispose();
+ await synchronizer?.StopAsync();
// Dispose connection service.
nodes?.Dispose();
// Dispose mempool serving node.
@@ -1662,7 +1667,7 @@ namespace WalletWasabi.Tests
{
wallet?.Dispose();
// Dispose wasabi synchronizer service.
- synchronizer?.Dispose();
+ await synchronizer?.StopAsync();
// Dispose connection service.
nodes?.Dispose();
// Dispose mempool serving node.
@@ -1768,7 +1773,7 @@ namespace WalletWasabi.Tests
{
wallet?.Dispose();
// Dispose wasabi synchronizer service.
- synchronizer?.Dispose();
+ await synchronizer?.StopAsync();
// Dispose connection service.
nodes?.Dispose();
// Dispose mempool serving node.
@@ -3431,8 +3436,6 @@ namespace WalletWasabi.Tests
{
wallet.NewFilterProcessed -= Wallet_NewFilterProcessed;
wallet?.Dispose();
- // Dispose wasabi synchronizer service.
- synchronizer?.Dispose();
// Dispose connection service.
nodes?.Dispose();
// Dispose mempool serving node.
@@ -3444,7 +3447,7 @@ namespace WalletWasabi.Tests
}
wallet2?.Dispose();
// Dispose wasabi synchronizer service.
- synchronizer2?.Dispose();
+ await synchronizer?.StopAsync();
// Dispose connection service.
nodes2?.Dispose();
// Dispose chaumian coinjoin client.

MODIFY
WalletWasabi/Nito/AsyncEx/AsyncMutex.cs
WalletWasabi/Nito/AsyncEx/AsyncMutex.cs
@@ -25,6 +25,8 @@ namespace Nito.AsyncEx
private int _status;
+ public bool IsQuitPending { get; set; }
+
/// <summary>
/// Short name of the mutex. This string added to the end of the mutex name.
/// </summary>
@@ -50,6 +52,8 @@ namespace Nito.AsyncEx
/// </summary>
private Thread MutexThread { get; set; }
+ private bool IsAlive => MutexThread?.IsAlive is true;
+
/// <summary>
/// Static storage for local mutexes. It can be used to get an already existing AsyncLock by name of the mutex.
/// </summary>
@@ -208,7 +212,7 @@ namespace Nito.AsyncEx
/// <returns></returns>
private async Task SetCommandAsync(int command, CancellationToken cancellationToken, int pollInterval)
{
- if (!(MutexThread?.IsAlive == true))
+ if (!IsAlive)
{
throw new InvalidOperationException($"Thread should be alive.");
}
@@ -269,10 +273,15 @@ namespace Nito.AsyncEx
try
{
+ if (IsQuitPending)
+ {
+ throw new OperationCanceledException($"AsyncMutex.LockAsync failed because quit is pending on: {ShortName}");
+ }
+
// Local lock for thread safety.
await AsyncLock.LockAsync(cancellationToken);
- if (MutexThread?.IsAlive == true)
+ if (IsAlive)
{
throw new InvalidOperationException($"Thread should not be alive.");
}
@@ -336,7 +345,7 @@ namespace Nito.AsyncEx
try
{
var start = DateTime.Now;
- while (MutexThread.IsAlive)
+ while (IsAlive)
{
SetCommand(2);
MutexThread?.Join(TimeSpan.FromSeconds(1));
@@ -363,7 +372,7 @@ namespace Nito.AsyncEx
private void ReleaseLock()
{
- if (MutexThread != null && !MutexThread.IsAlive)
+ if (!IsAlive)
{
throw new InvalidOperationException($"Thread should be alive.");
}
@@ -379,6 +388,38 @@ namespace Nito.AsyncEx
ChangeStatus(AsyncLockStatus.StatusReady, AsyncLockStatus.StatusReleasing);
}
+ public static async Task WaitForAllMutexToCloseAsync()
+ {
+ DateTime start = DateTime.Now;
+ lock (AsyncMutexesLock)
+ {
+ foreach (var mutex in AsyncMutexes)
+ {
+ mutex.Value.IsQuitPending = true;
+ }
+ }
+
+ while (true)
+ {
+ lock (AsyncMutexesLock)
+ {
+ bool stillRunning = AsyncMutexes.Where(am => am.Value.IsAlive).Any();
+ if (!stillRunning)
+ {
+ return;
+ }
+ Logger.LogDebug($"Waiting for: {string.Join(", ", AsyncMutexes.Where(am => am.Value.IsAlive).Select(m => m.Value.ShortName))}", nameof(AsyncMutex));
+ }
+ await Task.Delay(200);
+ if (DateTime.Now - start > TimeSpan.FromSeconds(60))
+ {
+ var mutexesAlive = AsyncMutexes.Where(am => am.Value.IsAlive).Select(m => m.Value.ShortName);
+ var names = string.Join(", ", mutexesAlive);
+ throw new TimeoutException($"Asyncmutex(es) still alive after Timeout: {names}");
+ }
+ }
+ }
+
/// <summary>
/// The disposable which releases the mutex.
/// </summary>

MODIFY
WalletWasabi/Services/CcjClient.cs
WalletWasabi/Services/CcjClient.cs
@@ -57,7 +57,6 @@ namespace WalletWasabi.Services
private long _running;
public bool IsRunning => Interlocked.Read(ref _running) == 1;
- public bool IsStopping => Interlocked.Read(ref _running) == 2;
private long _statusProcessing;
@@ -117,7 +116,10 @@ namespace WalletWasabi.Services
public void Start()
{
- Interlocked.Exchange(ref _running, 1);
+ if (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
+ {
+ return;
+ }
// The client is asking for status periodically, randomly between every 0.2 * connConfTimeout and 0.7 * connConfTimeout.
// - if the GUI is at the mixer tab -Activate(), DeactivateIfNotMixing().
@@ -1047,12 +1049,13 @@ namespace WalletWasabi.Services
Interlocked.CompareExchange(ref _running, 2, 1); // If running, make it stopping.
Cancel?.Cancel();
- while (IsStopping)
+ while (Interlocked.CompareExchange(ref _running, 3, 0) == 2)
{
- Task.Delay(50).GetAwaiter().GetResult(); // DO NOT MAKE IT ASYNC (.NET Core threading brainfart)
+ await Task.Delay(50);
}
Cancel?.Dispose();
+ Cancel = null;
using (await MixLock.LockAsync())
{

MODIFY
WalletWasabi/Services/IndexBuilderService.cs
WalletWasabi/Services/IndexBuilderService.cs
@@ -113,7 +113,6 @@ namespace WalletWasabi.Services
private Dictionary<OutPoint, Script> Bech32UtxoSet { get; }
private List<ActionHistoryHelper> Bech32UtxoSetHistory { get; }
- private FilterModel StartingFilter { get; set; }
private Height StartingHeight { get; set; }
/// <summary>
@@ -122,7 +121,6 @@ namespace WalletWasabi.Services
private long _running;
public bool IsRunning => Interlocked.Read(ref _running) == 1;
- public bool IsStopping => Interlocked.Read(ref _running) == 2;
public IndexBuilderService(RPCClient rpc, TrustedNodeNotifyingBehavior trustedNodeNotifyingBehavior, string indexFilePath, string bech32UtxoSetFilePath)
{
@@ -136,7 +134,6 @@ namespace WalletWasabi.Services
Index = new List<FilterModel>();
IndexLock = new AsyncLock();
- StartingFilter = StartingFilters.GetStartingFilter(RpcClient.Network);
StartingHeight = StartingFilters.GetStartingHeight(RpcClient.Network);
_running = 0;
@@ -485,7 +482,8 @@ namespace WalletWasabi.Services
}
Interlocked.CompareExchange(ref _running, 2, 1); // If running, make it stopping.
- while (IsStopping)
+
+ while (Interlocked.CompareExchange(ref _running, 3, 0) == 2)
{
await Task.Delay(50);
}

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -71,14 +71,6 @@ namespace WalletWasabi.Services
public Network Network => Synchronizer.Network;
- /// <summary>
- /// 0: Not started, 1: Running, 2: Stopping, 3: Stopped
- /// </summary>
- private long _running;
-
- public bool IsRunning => Interlocked.Read(ref _running) == 1;
- public bool IsStopping => Interlocked.Read(ref _running) == 2;
-
public WalletService(
BitcoinStore bitcoinStore,
KeyManager keyManager,
@@ -113,8 +105,6 @@ namespace WalletWasabi.Services
KeyManager.AssertCleanKeysIndexed();
KeyManager.AssertLockedInternalKeysIndexed(14);
- _running = 0;
-
if (Directory.Exists(BlocksFolderPath))
{
if (Synchronizer.Network == Network.RegTest)
@@ -246,7 +236,7 @@ namespace WalletWasabi.Services
}
} while (Synchronizer.AreRequestsBlocked()); // If requests are blocked, delay mempool cleanup, because coinjoin answers are always priority.
- await MemPool?.TryPerformMempoolCleanupAsync(Synchronizer.WasabiClient.TorClient.DestinationUriAction, Synchronizer.WasabiClient.TorClient.TorSocks5EndPoint);
+ await MemPool?.TryPerformMempoolCleanupAsync(Synchronizer?.WasabiClient?.TorClient?.DestinationUriAction, Synchronizer?.WasabiClient?.TorClient?.TorSocks5EndPoint);
}
catch (Exception ex)
{

MODIFY
WalletWasabi/Services/WasabiSynchronizer.cs
WalletWasabi/Services/WasabiSynchronizer.cs
@@ -22,7 +22,7 @@ using WalletWasabi.WebClients.Wasabi;
namespace WalletWasabi.Services
{
- public class WasabiSynchronizer : IDisposable, INotifyPropertyChanged
+ public class WasabiSynchronizer : INotifyPropertyChanged
{
#region MembersPropertiesEvents
@@ -122,7 +122,6 @@ namespace WalletWasabi.Services
private long _running;
public bool IsRunning => Interlocked.Read(ref _running) == 1;
- public bool IsStopping => Interlocked.Read(ref _running) == 2;
private CancellationTokenSource Cancel { get; set; }
@@ -165,7 +164,10 @@ namespace WalletWasabi.Services
MaxRequestIntervalForMixing = requestInterval; // Let's start with this, it'll be modified from outside.
- Interlocked.Exchange(ref _running, 1);
+ if (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
+ {
+ return;
+ }
Task.Run(async () =>
{
@@ -362,41 +364,21 @@ namespace WalletWasabi.Services
#endregion Methods
- #region IDisposable Support
-
- private volatile bool _disposedValue = false; // To detect redundant calls
-
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposedValue)
- {
- if (disposing)
+ public async Task StopAsync()
{
Interlocked.CompareExchange(ref _running, 2, 1); // If running, make it stopping.
Cancel?.Cancel();
- while (IsStopping)
+ while (Interlocked.CompareExchange(ref _running, 3, 0) == 2)
{
- Task.Delay(50).GetAwaiter().GetResult(); // DO NOT MAKE IT ASYNC (.NET Core threading brainfart)
+ await Task.Delay(50);
}
Cancel?.Dispose();
+ Cancel = null;
WasabiClient?.Dispose();
+ WasabiClient = null;
EnableRequests(); // Enable requests (it's possible something is being blocked outside the class by AreRequestsBlocked.
}
-
- _disposedValue = true;
- }
- }
-
- // This code added to correctly implement the disposable pattern.
- public void Dispose()
- {
- // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
- Dispose(true);
- // GC.SuppressFinalize(this);
- }
-
- #endregion IDisposable Support
}
}

Implement shutdown logic