feat(storage): add WalletLock to prevent concurrent wallet access
Introduces an exclusive file lock (FileShare.None on wallet.json.lock) held for the duration of a wallet session. A second instance trying to open the same file receives null from TryAcquire and sees a localized error message; UnauthorizedAccessException (OS permission issues) propagates separately so the UI can show a distinct message. The .lock file is deleted on Dispose (best-effort); the real guard is the open FileStream, which the OS releases automatically on crash.
This commit is contained in:
@@ -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"] = [
|
||||
|
||||
@@ -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<string, Transaction>? _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;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class WalletLock : IDisposable
|
||||
{
|
||||
private readonly string _lockPath;
|
||||
private FileStream? _stream;
|
||||
|
||||
private WalletLock(string lockPath, FileStream stream)
|
||||
{
|
||||
_lockPath = lockPath;
|
||||
_stream = stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to acquire an exclusive lock for <paramref name="walletPath"/>.
|
||||
/// Returns null if another process already holds the lock (IOException).
|
||||
/// Lets UnauthorizedAccessException propagate so callers can show a distinct message.
|
||||
/// </summary>
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user