Replece SingleInstance AsyncMutex with TcpListener.
MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -761,7 +761,7 @@ namespace WalletWasabi.Gui
try
{
- SingleInstanceChecker?.Dispose();
+ await SingleInstanceChecker.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{

MODIFY
WalletWasabi.Tests/UnitTests/Clients/SingleInstanceCheckerTests.cs
WalletWasabi.Tests/UnitTests/Clients/SingleInstanceCheckerTests.cs
@@ -13,42 +13,44 @@ namespace WalletWasabi.Tests.UnitTests.Clients
[Fact]
public async Task SingleInstanceTestsAsync()
{
- string mainLockName = GenerateLockName(Network.Main);
+ int mainNetPort = GenerateRandomPort();
+ int testNetPort = GenerateRandomPort();
+ int regTestPort = GenerateRandomPort();
// Disposal test.
- using (SingleInstanceChecker sic = new SingleInstanceChecker(Network.Main, mainLockName))
+ await using (SingleInstanceChecker sic = new(mainNetPort))
{
await sic.CheckAsync();
}
// Check different networks.
- using (SingleInstanceChecker sic = new SingleInstanceChecker(Network.Main, mainLockName))
+ await using (SingleInstanceChecker sic = new(mainNetPort))
{
await sic.CheckAsync();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sic.CheckAsync());
- using SingleInstanceChecker sicMainNet2 = new SingleInstanceChecker(Network.Main, mainLockName);
+ await using SingleInstanceChecker sicMainNet2 = new(mainNetPort);
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sicMainNet2.CheckAsync());
- string testnetLockName = GenerateLockName(Network.TestNet);
- using SingleInstanceChecker sicTestNet = new SingleInstanceChecker(Network.TestNet, testnetLockName);
+ await using SingleInstanceChecker sicTestNet = new(testNetPort);
await sicTestNet.CheckAsync();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sicTestNet.CheckAsync());
- string regtestLockName = GenerateLockName(Network.RegTest);
- using SingleInstanceChecker sicRegTest = new SingleInstanceChecker(Network.RegTest, regtestLockName);
+ await using SingleInstanceChecker sicRegTest = new(regTestPort);
await sicRegTest.CheckAsync();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sicRegTest.CheckAsync());
}
}
+ private static Random Random { get; } = new();
+
/// <summary>
/// Global lock names may collide when several PRs are being tested on CI at the same time,
/// so we need some sort of non-determinism here (e.g. random numbers).
/// </summary>
- private static string GenerateLockName(Network network)
+ private static int GenerateRandomPort()
{
- return $"{network}-{new Random().Next(1_000_000)}";
+ return Random.Next(37128, 37168);
}
}
}

MODIFY
WalletWasabi/Services/SingleInstanceChecker.cs
WalletWasabi/Services/SingleInstanceChecker.cs
+using Microsoft.Extensions.Hosting;
using NBitcoin;
using Nito.AsyncEx;
using System;
+using System.ComponentModel;
using System.IO;
+using System.Net;
+using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
+using WalletWasabi.Logging;
namespace WalletWasabi.Services
{
- public class SingleInstanceChecker : IDisposable
+ public class SingleInstanceChecker : BackgroundService, IAsyncDisposable
{
- /// <summary>Unique prefix for global mutex name.</summary>
- private const string MutexString = "WalletWasabiSingleInstance";
-
- /// <summary>Name of system-wide mutex.</summary>
- private readonly string _lockName;
-
- private bool _disposedValue;
-
/// <summary>
- /// Creates a new instance of the object where lock name is based on <paramref name="network"/> name.
+ /// Creates an object to ensure mutual exclusion of Wasabi instances per Network <paramref name="network"/>.
+ /// The solution based on TCP socket.
/// </summary>
- /// <param name="network">Bitcoin network selected when Wasabi Wallet was started.</param>
- public SingleInstanceChecker(Network network) : this(network, network.ToString())
+ /// <param name="network">Bitcoin network selected when Wasabi Wallet was started. It will use the port 37129,37130,37131 according to network main,test,reg.</param>
+ public SingleInstanceChecker(Network network) : this(NetworkToPort(network))
{
}
/// <summary>
/// Use this constructor only for testing.
/// </summary>
- /// <param name="network">Bitcoin network selected when Wasabi Wallet was started.</param>
- /// <param name="lockName">Name of system-wide mutex.</param>
- public SingleInstanceChecker(Network network, string lockName)
+ public SingleInstanceChecker(int port)
{
- Network = network;
- _lockName = $"{MutexString}-{lockName}";
+ Port = port;
}
- private IDisposable? SingleApplicationLockHolder { get; set; }
- private Network Network { get; }
+ private int Port { get; }
+
+ private CancellationTokenSource DisposeCts { get; } = new();
+ private TcpListener? TcpListener { get; set; }
public async Task CheckAsync()
{
- if (_disposedValue)
+ if (DisposeCts.IsCancellationRequested)
{
throw new ObjectDisposedException(nameof(SingleInstanceChecker));
}
- // The disposal of this mutex handled by AsyncMutex.WaitForAllMutexToCloseAsync().
- var mutex = new AsyncMutex(_lockName);
try
{
- using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
- SingleApplicationLockHolder = await mutex.LockAsync(cts.Token).ConfigureAwait(false);
+ TcpListener = new TcpListener(IPAddress.Loopback, Port);
+ TcpListener.Start();
+ await StartAsync(DisposeCts.Token).ConfigureAwait(false);
+
+ // This is the first instance, nothing else to do.
+ return;
}
- catch (IOException ex)
+ catch (SocketException ex) when (ex.ErrorCode == 10048)
{
- throw new InvalidOperationException($"Wasabi is already running on {Network}!", ex);
+ // It is already used -> another Wasabi is running on this network.
+ Logger.LogDebug("Detected another Wasabi instance.");
}
+
+ // Signal to the other instance, that there was an attempt to start the software.
+ using TcpClient client = new TcpClient();
+ await client.ConnectAsync(IPAddress.Loopback, Port).ConfigureAwait(false);
+ client.Close();
+
+ throw new InvalidOperationException($"Wasabi is already running. Port {Port}!");
}
- /// <summary>
- /// <list type="bullet">
- /// <item>Unmanaged resources need to be released regardless of the value of the <paramref name="disposing"/> parameter.</item>
- /// <item>Managed resources need to be released if the value of <paramref name="disposing"/> is <c>true</c>.</item>
- /// </list>
- /// </summary>
- /// <param name="disposing">
- /// Indicates whether the method call comes from a <see cref="Dispose()"/> method
- /// (its value is <c>true</c>) or from a finalizer (its value is <c>false</c>).
- /// </param>
- protected virtual void Dispose(bool disposing)
+ private static int NetworkToPort(Network network)
{
- if (!_disposedValue)
+ if (network == Network.Main)
{
- if (disposing)
+ return 37129;
+ }
+ if (network == Network.TestNet)
{
- SingleApplicationLockHolder?.Dispose();
+ return 37130;
}
- _disposedValue = true;
+
+ return 37131;
}
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var listener = TcpListener;
+ if (listener is null)
+ {
+ throw new InvalidOperationException();
}
- /// <summary>
- /// Do not change this code.
- /// </summary>
- public void Dispose()
+ // Stop listener here to ensure thread-safety.
+ using var _ = stoppingToken.Register(() => listener.Stop());
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ await listener.AcceptTcpClientAsync().ConfigureAwait(false);
+ Logger.LogDebug($"Connection arrived on port: {Port}");
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ DisposeCts.Cancel();
+
+ if (TcpListener is { } listener)
{
- // Dispose of unmanaged resources.
- Dispose(true);
- // Suppress finalization.
- GC.SuppressFinalize(this);
+ await StopAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+
+ DisposeCts.Dispose();
}
}
}

Replece SingleInstance AsyncMutex with TcpListener.