From 2be43d3177397a5d8f6766aff975d08fa2f772f5 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 2 Jul 2026 14:56:47 +0200 Subject: [PATCH] fix(sync): allow server change at any time, even during an active sync When IsSyncing and the user requests a different server, cancel the running CancellationTokenSource instead of silently ignoring the click. OperationCanceledException is caught separately (not an error), and ConnectAndSync restarts automatically with the new server after the cancelled task unwinds. The "Connect" button no longer has IsEnabled="{Binding !IsSyncing}": it is always clickable, and clicking it with the same server sets _resyncRequested as before. CancellationToken is now passed to both ElectrumClient.ConnectAsync and WalletSynchronizer.SyncOnceAsync. --- .../ViewModels/MainWindowViewModel.Sync.cs | 41 +++++++++++++++++-- src/App/Views/MainView.axaml | 4 +- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/App/ViewModels/MainWindowViewModel.Sync.cs b/src/App/ViewModels/MainWindowViewModel.Sync.cs index 6195b54..5f2b86a 100644 --- a/src/App/ViewModels/MainWindowViewModel.Sync.cs +++ b/src/App/ViewModels/MainWindowViewModel.Sync.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; @@ -55,6 +56,8 @@ public partial class MainWindowViewModel [ObservableProperty] private bool isSyncing; + private CancellationTokenSource _syncCts = new(); + public ObservableCollection KnownServers { get; } = []; [ObservableProperty] @@ -165,11 +168,23 @@ public partial class MainWindowViewModel { if (IsSyncing) { - _resyncRequested = true; + var (newHost, newPort) = ParseServer(); + bool serverChanged = _client is null + || _client.Host != newHost + || _client.Port != newPort + || _client.UseSsl != UseSsl; + if (serverChanged) + _syncCts.Cancel(); + else + _resyncRequested = true; return; } + + _syncCts = new CancellationTokenSource(); + var ct = _syncCts.Token; IsSyncing = true; StatusMessage = ""; + bool cancelled = false; try { var (host, port) = ParseServer(); @@ -194,12 +209,13 @@ public partial class MainWindowViewModel Exception? lastError = null; foreach (var (h, p) in candidates) { + ct.ThrowIfCancellationRequested(); ConnectionStatus = $"{Loc.Tr("conn.connectingto")} {h}:{p}…"; ConnectionStatusShort = Loc.Tr("conn.connectingto") + "…"; try { var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(Net)); - _client = await ElectrumClient.ConnectAsync(h, p, UseSsl, pins); + _client = await ElectrumClient.ConnectAsync(h, p, UseSsl, pins, ct); _client.NotificationReceived += OnServerNotification; _client.Disconnected += _ => Dispatcher.UIThread.Post(() => { @@ -226,6 +242,10 @@ public partial class MainWindowViewModel lastError = null; break; } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { lastError = ex; @@ -258,7 +278,7 @@ public partial class MainWindowViewModel do { _resyncRequested = false; - var result = await _synchronizer.SyncOnceAsync(); + var result = await _synchronizer.SyncOnceAsync(ct); _lastTransactions = result.Transactions; var (rawHex, verifiedAt, blockHeaders) = _synchronizer.ExportCaches( @@ -282,7 +302,16 @@ public partial class MainWindowViewModel _syncFailed = false; StatusMessage = $"{Loc.Tr("msg.synced")}: {Loc.Tr("msg.height")} {result.TipHeight}, " + $"{result.History.Count} {Loc.Tr("msg.synced.detail")}"; - } while (_resyncRequested); + } while (_resyncRequested && !ct.IsCancellationRequested); + } + catch (OperationCanceledException) + { + // Intentional cancellation due to server change request — not an error. + cancelled = true; + IsConnected = false; + ConnectionStatus = Loc.Tr("conn.none"); + ConnectionStatusShort = Loc.Tr("conn.none"); + StatusMessage = ""; } catch (CertificatePinMismatchException ex) { @@ -309,6 +338,10 @@ public partial class MainWindowViewModel { IsSyncing = false; } + + // If cancelled due to a server change, restart immediately with the new server. + if (cancelled) + _ = ConnectAndSync(); } [RelayCommand] diff --git a/src/App/Views/MainView.axaml b/src/App/Views/MainView.axaml index 28369f5..ff521e5 100644 --- a/src/App/Views/MainView.axaml +++ b/src/App/Views/MainView.axaml @@ -1233,14 +1233,14 @@