diff --git a/src/App/Localization/Loc.cs b/src/App/Localization/Loc.cs index bbbcb03..316abe8 100644 --- a/src/App/Localization/Loc.cs +++ b/src/App/Localization/Loc.cs @@ -281,6 +281,8 @@ public sealed class Loc "As duas senhas não coincidem.", "Die beiden Passwörter stimmen nicht überein."], ["msg.wrongpassword"] = ["Password errata.", "Wrong password.", "Contraseña incorrecta.", "Mot de passe incorrect.", "Senha incorreta.", "Falsches Passwort."], + ["msg.wallet.locked"] = ["Wallet già aperto in un'altra istanza dell'applicazione.", "Wallet already open in another instance of the application.", "El wallet ya está abierto en otra instancia de la aplicación.", "Le wallet est déjà ouvert dans une autre instance de l'application.", "A carteira já está aberta em outra instância do aplicativo.", "Wallet ist bereits in einer anderen Instanz der Anwendung geöffnet."], + ["msg.wallet.noaccess"] = ["Impossibile accedere al file del wallet: verificare i permessi.", "Cannot access the wallet file: check file permissions.", "No se puede acceder al archivo del wallet: verifique los permisos.", "Impossible d'accéder au fichier du wallet : vérifiez les autorisations.", "Não é possível acessar o arquivo da carteira: verifique as permissões.", "Zugriff auf die Wallet-Datei nicht möglich: Berechtigungen prüfen."], ["msg.opened"] = ["Wallet aperto: connessione al server…", "Wallet opened: connecting to server…", "Wallet abierto: conectando al servidor…", "Wallet ouvert : connexion au serveur…", "Carteira aberta: conectando ao servidor…", "Wallet geöffnet: Verbindung zum Server…"], ["msg.synced"] = ["Sincronizzato", "Synchronized", "Sincronizado", "Synchronisé", "Sincronizado", "Synchronisiert"], ["msg.synced.detail"] = [ diff --git a/src/App/ViewModels/MainWindowViewModel.cs b/src/App/ViewModels/MainWindowViewModel.cs index 0b40675..4842736 100644 --- a/src/App/ViewModels/MainWindowViewModel.cs +++ b/src/App/ViewModels/MainWindowViewModel.cs @@ -57,6 +57,7 @@ public partial class MainWindowViewModel : ViewModelBase private HdAccount? _account; private string? _walletPath; private string? _password; + private WalletLock? _walletLock; private ElectrumClient? _client; private WalletSynchronizer? _synchronizer; private IReadOnlyDictionary? _lastTransactions; @@ -896,6 +897,10 @@ public partial class MainWindowViewModel : ViewModelBase { StatusMessage = Loc.Tr("msg.wrongpassword"); } + catch (UnauthorizedAccessException) + { + StatusMessage = Loc.Tr("msg.wallet.noaccess"); + } catch (Exception ex) { StatusMessage = $"Errore: {ex.Message}"; @@ -923,6 +928,10 @@ public partial class MainWindowViewModel : ViewModelBase SetupStep = StepOpen; StatusMessage = ""; } + catch (UnauthorizedAccessException) + { + StatusMessage = Loc.Tr("msg.wallet.noaccess"); + } catch (Exception ex) { StatusMessage = $"Errore: {ex.Message}"; @@ -939,8 +948,24 @@ public partial class MainWindowViewModel : ViewModelBase StatusMessage = ""; } + private bool TryAcquireWalletLock(string path) + { + var acquired = WalletLock.TryAcquire(path); + if (acquired is null) + { + StatusMessage = Loc.Tr("msg.wallet.locked"); + return false; + } + _walletLock?.Dispose(); + _walletLock = acquired; + return true; + } + private void OpenLoaded(WalletDocument doc, HdAccount account, string path, string? password) { + if (!TryAcquireWalletLock(path)) + return; + // La rete del wallet comanda (registry, pin TLS, indirizzi). SelectedNetwork = doc.Network; _doc = doc; @@ -1246,6 +1271,8 @@ public partial class MainWindowViewModel : ViewModelBase [RelayCommand] private void CloseWallet() { + _walletLock?.Dispose(); + _walletLock = null; _ = _client?.DisposeAsync().AsTask(); _client = null; _synchronizer = null; diff --git a/src/Core/Storage/WalletLock.cs b/src/Core/Storage/WalletLock.cs new file mode 100644 index 0000000..cd4ebac --- /dev/null +++ b/src/Core/Storage/WalletLock.cs @@ -0,0 +1,48 @@ +namespace PalladiumWallet.Core.Storage; + +/// +/// Exclusive lock on a wallet file held for the lifetime of the session. +/// The real lock is the open FileStream with FileShare.None — the .lock file +/// is just its vessel. The OS releases it automatically on process exit or crash. +/// +public sealed class WalletLock : IDisposable +{ + private readonly string _lockPath; + private FileStream? _stream; + + private WalletLock(string lockPath, FileStream stream) + { + _lockPath = lockPath; + _stream = stream; + } + + /// + /// Tries to acquire an exclusive lock for . + /// Returns null if another process already holds the lock (IOException). + /// Lets UnauthorizedAccessException propagate so callers can show a distinct message. + /// + public static WalletLock? TryAcquire(string walletPath) + { + var lockPath = walletPath + ".lock"; + try + { + var stream = new FileStream( + lockPath, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.None); + return new WalletLock(lockPath, stream); + } + catch (IOException) + { + return null; + } + } + + public void Dispose() + { + _stream?.Dispose(); + _stream = null; + try { File.Delete(_lockPath); } catch { } + } +}