Improve HostedServices
MODIFY
WalletWasabi.Backend/Global.cs
WalletWasabi.Backend/Global.cs
@@ -58,11 +58,11 @@ namespace WalletWasabi.Backend
CoordinatorParameters coordinatorParameters = new(DataDir);
WabiSabiCoordinator = new(coordinatorParameters, RpcClient);
- HostedServices.Register(WabiSabiCoordinator, "WabiSabi Coordinator");
+ HostedServices.Register<WabiSabiCoordinator>(WabiSabiCoordinator, "WabiSabi Coordinator");
if (roundConfig.FilePath is { })
{
- HostedServices.Register(
+ HostedServices.Register<ConfigWatcher>(
new ConfigWatcher(
TimeSpan.FromSeconds(10), // Every 10 seconds check the config
RoundConfig,
@@ -87,7 +87,7 @@ namespace WalletWasabi.Backend
// Initialize index building
var indexBuilderServiceDir = Path.Combine(DataDir, "IndexBuilderService");
var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{RpcClient.Network}.dat");
- var blockNotifier = HostedServices.FirstOrDefault<BlockNotifier>();
+ var blockNotifier = HostedServices.Get<BlockNotifier>();
IndexBuilderService = new(RpcClient, blockNotifier, indexFilePath);
Coordinator = new(RpcClient.Network, blockNotifier, Path.Combine(DataDir, "CcjCoordinator"), RpcClient, roundConfig);
IndexBuilderService.Synchronize();
@@ -105,7 +105,7 @@ namespace WalletWasabi.Backend
// We have to find it, because it's cloned by the node and not perfectly cloned (event handlers cannot be cloned.)
P2pNode = new(network, endPoint, new(), $"/WasabiCoordinator:{Constants.BackendMajorVersion}/");
await P2pNode.ConnectAsync(cancel).ConfigureAwait(false);
- HostedServices.Register(new BlockNotifier(TimeSpan.FromSeconds(7), RpcClient, P2pNode), "Block Notifier");
+ HostedServices.Register<BlockNotifier>(new BlockNotifier(TimeSpan.FromSeconds(7), RpcClient, P2pNode), "Block Notifier");
}
private async Task AssertRpcNodeFullyInitializedAsync()

MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -138,13 +138,11 @@ namespace WalletWasabi.Gui
var userAgent = Constants.UserAgents.RandomElement();
var connectionParameters = new NodeConnectionParameters { UserAgent = userAgent };
- UpdateChecker updateChecker = new(TimeSpan.FromMinutes(7), Synchronizer);
- await LegalChecker.InitializeAsync(updateChecker).ConfigureAwait(false);
- HostedServices.Register(updateChecker, "Software Update Checker");
+ HostedServices.Register<UpdateChecker>(new UpdateChecker(TimeSpan.FromMinutes(7), Synchronizer), "Software Update Checker");
- HostedServices.Register(new UpdateChecker(TimeSpan.FromMinutes(7), Synchronizer), "Software Update Checker");
+ await LegalChecker.InitializeAsync(HostedServices.Get<UpdateChecker>()).ConfigureAwait(false);
- HostedServices.Register(new SystemAwakeChecker(WalletManager), "System Awake Checker");
+ HostedServices.Register<SystemAwakeChecker>(new SystemAwakeChecker(WalletManager), "System Awake Checker");
cancel.ThrowIfCancellationRequested();
@@ -160,7 +158,7 @@ namespace WalletWasabi.Gui
Tor.Http.TorHttpClient torHttpClient = HttpClientFactory.NewTorHttpClient(isolateStream: false);
#pragma warning disable CA2000 // Dispose objects before losing scope
- HostedServices.Register(new TorMonitor(period: TimeSpan.FromSeconds(3), fallbackBackendUri: Config.GetFallbackBackendUri(), torHttpClient, TorManager), nameof(TorMonitor));
+ HostedServices.Register<TorMonitor>(new TorMonitor(period: TimeSpan.FromSeconds(3), fallbackBackendUri: Config.GetFallbackBackendUri(), torHttpClient, TorManager), nameof(TorMonitor));
#pragma warning restore CA2000 // Dispose objects before losing scope
}
@@ -222,11 +220,9 @@ namespace WalletWasabi.Gui
Logger.LogError(ex);
}
- await HostedServices.StartAllAsync(cancel).ConfigureAwait(false);
-
- var rpcFeeProvider = HostedServices.FirstOrDefault<RpcFeeProvider>();
+ FeeProviders = new FeeProviders(Synchronizer, HostedServices.GetOrDefault<RpcFeeProvider>());
- FeeProviders = new FeeProviders(Synchronizer, rpcFeeProvider);
+ await HostedServices.StartAllAsync(cancel).ConfigureAwait(false);
#endregion BitcoinCoreInitialization

MODIFY
WalletWasabi.Gui/ViewModels/StatusBarViewModel.cs
WalletWasabi.Gui/ViewModels/StatusBarViewModel.cs
@@ -162,11 +162,10 @@ namespace WalletWasabi.Gui.ViewModels
UseTor = Config.UseTor; // Do not make it dynamic, because if you change this config settings only next time will it activate.
UseBitcoinCore = Config.StartLocalBitcoinCoreOnStartup;
- var updateChecker = HostedServices.FirstOrDefault<UpdateChecker>();
- Guard.NotNull(nameof(updateChecker), updateChecker);
+ var updateChecker = HostedServices.Get<UpdateChecker>();
UpdateStatus = updateChecker.UpdateStatus;
- var rpcMonitor = HostedServices.FirstOrDefault<RpcMonitor>();
+ var rpcMonitor = HostedServices.GetOrDefault<RpcMonitor>();
BitcoinCoreStatus = rpcMonitor?.RpcStatus ?? RpcStatus.Unresponsive;
_status = ActiveStatuses.WhenAnyValue(x => x.CurrentStatus)

MODIFY
WalletWasabi.Tests/RegressionTests/BackendTests.cs
WalletWasabi.Tests/RegressionTests/BackendTests.cs
@@ -111,7 +111,7 @@ namespace WalletWasabi.Tests.RegressionTests
var indexBuilderServiceDir = Helpers.Common.GetWorkDir();
var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{rpc.Network}.dat");
- var indexBuilderService = new IndexBuilderService(rpc, global.HostedServices.FirstOrDefault<BlockNotifier>(), indexFilePath);
+ var indexBuilderService = new IndexBuilderService(rpc, global.HostedServices.Get<BlockNotifier>(), indexFilePath);
try
{
indexBuilderService.Synchronize();

MODIFY
WalletWasabi.Tests/RegressionTests/CoinJoinTests.cs
WalletWasabi.Tests/RegressionTests/CoinJoinTests.cs
@@ -72,7 +72,7 @@ namespace WalletWasabi.Tests.RegressionTests
var cjfile = Path.Combine(folder, $"CoinJoins{network}.txt");
File.WriteAllLines(cjfile, new[] { coinbaseTxId.ToString(), offchainTxId.ToString(), mempoolTxId.ToString() });
- using (var coordinatorToTest = new Coordinator(network, global.HostedServices.FirstOrDefault<BlockNotifier>(), folder, rpc, coordinator.RoundConfig))
+ using (var coordinatorToTest = new Coordinator(network, global.HostedServices.Get<BlockNotifier>(), folder, rpc, coordinator.RoundConfig))
{
var txIds = await File.ReadAllLinesAsync(cjfile);
@@ -84,7 +84,7 @@ namespace WalletWasabi.Tests.RegressionTests
Directory.CreateDirectory(folder);
File.WriteAllLines(cjfile, new[] { coinbaseTxId.ToString(), "This line is invalid (the file is corrupted)", offchainTxId.ToString() });
- var coordinatorToTest2 = new Coordinator(network, global.HostedServices.FirstOrDefault<BlockNotifier>(), folder, rpc, coordinatorToTest.RoundConfig);
+ var coordinatorToTest2 = new Coordinator(network, global.HostedServices.Get<BlockNotifier>(), folder, rpc, coordinatorToTest.RoundConfig);
coordinatorToTest2?.Dispose();
txIds = await File.ReadAllLinesAsync(cjfile);
Assert.Single(txIds);

MODIFY
WalletWasabi.Tests/UnitTests/BitcoinCore/P2pBasedTests.cs
WalletWasabi.Tests/UnitTests/BitcoinCore/P2pBasedTests.cs
@@ -160,7 +160,7 @@ namespace WalletWasabi.Tests.UnitTests.BitcoinCore
var walletName = "wallet.dat";
await rpc.CreateWalletAsync(walletName);
- BlockNotifier notifier = services.FirstOrDefault<BlockNotifier>();
+ BlockNotifier notifier = services.Get<BlockNotifier>();
// Make sure we get notification for one block.
EventAwaiter<Block> blockEventAwaiter = new(h => notifier.OnBlock += h, h => notifier.OnBlock -= h);

MODIFY
WalletWasabi/BitcoinCore/CoreNode.cs
WalletWasabi/BitcoinCore/CoreNode.cs
@@ -239,9 +239,9 @@ namespace WalletWasabi.BitcoinCore
await coreNode.P2pNode.ConnectAsync(cancel).ConfigureAwait(false);
cancel.ThrowIfCancellationRequested();
- coreNode.HostedServices.Register(new BlockNotifier(TimeSpan.FromSeconds(7), coreNode.RpcClient, coreNode.P2pNode), "Block Notifier");
- coreNode.HostedServices.Register(new RpcMonitor(TimeSpan.FromSeconds(7), coreNode.RpcClient), "RPC Monitor");
- coreNode.HostedServices.Register(new RpcFeeProvider(TimeSpan.FromMinutes(1), coreNode.RpcClient), "RPC Fee Provider");
+ coreNode.HostedServices.Register<BlockNotifier>(new BlockNotifier(TimeSpan.FromSeconds(7), coreNode.RpcClient, coreNode.P2pNode), "Block Notifier");
+ coreNode.HostedServices.Register<RpcMonitor>(new RpcMonitor(TimeSpan.FromSeconds(7), coreNode.RpcClient), "RPC Monitor");
+ coreNode.HostedServices.Register<RpcFeeProvider>(new RpcFeeProvider(TimeSpan.FromMinutes(1), coreNode.RpcClient), "RPC Fee Provider");
return coreNode;
}

MODIFY
WalletWasabi/Services/HostedServices.cs
WalletWasabi/Services/HostedServices.cs
@@ -16,10 +16,14 @@ namespace WalletWasabi.Services
private object ServicesLock { get; } = new object();
private bool IsStartAllAsyncStarted { get; set; } = false;
- private bool IsStartAllAsyncCompleted { get; set; } = false;
- public void Register(IHostedService service, string friendlyName)
+ public void Register<T>(IHostedService service, string friendlyName) where T : class, IHostedService
{
+ if (typeof(T) != service.GetType())
+ {
+ throw new ArgumentException($"Type mismatch: {nameof(T)} is {typeof(T).Name}, but {nameof(service)} is {service.GetType()}.");
+ }
+
if (IsStartAllAsyncStarted)
{
throw new InvalidOperationException("Services are already started.");
@@ -27,6 +31,10 @@ namespace WalletWasabi.Services
lock (ServicesLock)
{
+ if (AnyNoLock<T>())
+ {
+ throw new InvalidOperationException($"{typeof(T).Name} is already registered.");
+ }
Services.Add(new HostedService(service, friendlyName));
}
}
@@ -65,8 +73,6 @@ namespace WalletWasabi.Services
{
throw new AggregateException(exceptions);
}
-
- IsStartAllAsyncCompleted = true;
}
public async Task StopAllAsync(CancellationToken token = default)
@@ -95,23 +101,32 @@ namespace WalletWasabi.Services
}
}
- public T FirstOrDefault<T>() where T : class
+ public T? GetOrDefault<T>() where T : class, IHostedService
{
- if (!IsStartAllAsyncCompleted)
+ lock (ServicesLock)
{
- throw new InvalidOperationException("Services are not yet started.");
+ return Services.SingleOrDefault(x => x.Service is T)?.Service as T;
}
- lock (ServicesLock)
+ }
+
+ public T Get<T>() where T : class, IHostedService
{
- var found = Services.FirstOrDefault(x => x.Service is T);
- if (found?.Service is null)
+ lock (ServicesLock)
{
- return default;
+ return (T)Services.Single(x => x.Service is T).Service;
}
- return found.Service as T;
+ }
+
+ public bool Any<T>() where T : class, IHostedService
+ {
+ lock (ServicesLock)
+ {
+ return AnyNoLock<T>();
}
}
+ private bool AnyNoLock<T>() where T : class, IHostedService => Services.Any(x => x.Service is T);
+
#region IDisposable Support
protected virtual void Dispose(bool disposing)

Improve HostedServices