Finish safe file operations
MODIFY
WalletWasabi.Tests/NodeBuilding/NodeBuilder.cs
WalletWasabi.Tests/NodeBuilding/NodeBuilder.cs
@@ -199,7 +199,7 @@ namespace WalletWasabi.Tests.NodeBuilding
{
foreach (CoreNode node in Nodes)
{
- node?.KillAsync().GetAwaiter().GetResult();
+ node?.TryKillAsync().GetAwaiter().GetResult();
}
foreach (IDisposable disposable in Disposables)

MODIFY
WalletWasabi.Tests/XunitConfiguration/RegTestFixture.cs
WalletWasabi.Tests/XunitConfiguration/RegTestFixture.cs
@@ -56,7 +56,7 @@ namespace WalletWasabi.Tests.XunitConfiguration
BackendHost?.StopAsync().GetAwaiter().GetResult();
BackendHost?.Dispose();
- BackendRegTestNode?.KillAsync(cleanFolder: true).GetAwaiter().GetResult();
+ BackendRegTestNode?.TryKillAsync(cleanFolder: true).GetAwaiter().GetResult();
BackendNodeBuilder?.Dispose();
}
}

MODIFY
WalletWasabi/Stores/IndexStore.cs
WalletWasabi/Stores/IndexStore.cs
@@ -22,7 +22,6 @@ namespace WalletWasabi.Stores
private const string IndexFileName = "Index.dat";
private string WorkFolderPath { get; set; }
private Network Network { get; set; }
- private string IndexFilePath { get; set; }
private IoManager IndexFileManager { get; set; }
private FilterModel StartingFilter { get; set; }
@@ -39,8 +38,8 @@ namespace WalletWasabi.Stores
WorkFolderPath = Guard.NotNullOrEmptyOrWhitespace(nameof(workFolderPath), workFolderPath, trim: true);
Network = Guard.NotNull(nameof(network), network);
- IndexFilePath = Path.Combine(WorkFolderPath, IndexFileName);
- IndexFileManager = new IoManager(IndexFilePath);
+ var indexFilePath = Path.Combine(WorkFolderPath, IndexFileName);
+ IndexFileManager = new IoManager(indexFilePath);
StartingFilter = StartingFilters.GetStartingFilter(Network);
StartingHeight = StartingFilters.GetStartingHeight(Network);
@@ -49,7 +48,7 @@ namespace WalletWasabi.Stores
Semaphore = new SemaphoreSlim(1);
- await IndexFileManager.WrapInSemaphoreAsync(async () =>
+ await IndexFileManager.WrapInMutexAsync(async () =>
{
IoHelpers.EnsureDirectoryExists(WorkFolderPath);
@@ -57,18 +56,18 @@ namespace WalletWasabi.Stores
if (Network == Network.RegTest)
{
- IoHelpers.BetterDelete(IndexFilePath); // RegTest is not a global ledger, better to delete it.
+ IndexFileManager.DeleteMe(); // RegTest is not a global ledger, better to delete it.
}
- if (!File.Exists(IndexFilePath))
+ if (!IndexFileManager.Exists())
{
- await File.WriteAllLinesAsync(IndexFilePath, new[] { StartingFilter.ToHeightlessLine() });
+ await IndexFileManager.WriteAllLinesAsync(new[] { StartingFilter.ToHeightlessLine() });
}
var height = StartingHeight;
try
{
- foreach (var line in await File.ReadAllLinesAsync(IndexFilePath))
+ foreach (var line in await IndexFileManager.ReadAllLinesAsync())
{
var filter = FilterModel.FromHeightlessLine(line, height);
height++;
@@ -79,7 +78,7 @@ namespace WalletWasabi.Stores
{
// We found a corrupted entry. Stop here.
// Fix the currupted file.
- await File.WriteAllLinesAsync(IndexFilePath, Index.Select(x => x.ToHeightlessLine()));
+ await IndexFileManager.WriteAllLinesAsync(Index.Select(x => x.ToHeightlessLine()));
}
});
}
@@ -90,10 +89,7 @@ namespace WalletWasabi.Stores
{
// Before Wasabi 1.1.5
var oldIndexFilepath = Path.Combine(EnvironmentHelpers.GetDataDir(Path.Combine("WalletWasabi", "Client")), $"Index{Network}.dat");
- if (File.Exists(oldIndexFilepath))
- {
- File.Move(oldIndexFilepath, IndexFilePath);
- }
+ IndexFileManager.TryReplaceMeWith(oldIndexFilepath);
}
catch (Exception ex)
{
@@ -108,9 +104,9 @@ namespace WalletWasabi.Stores
await Semaphore.WaitAsync();
Index.Add(filter);
- await IndexFileManager.WrapInSemaphoreAsync(async () =>
+ await IndexFileManager.WrapInMutexAsync(async () =>
{
- await File.AppendAllLinesAsync(IndexFilePath, new[] { filter.ToHeightlessLine() });
+ await IndexFileManager.AppendAllLinesAsync(new[] { filter.ToHeightlessLine() });
});
}
finally
@@ -130,9 +126,9 @@ namespace WalletWasabi.Stores
filter = Index.Last();
Index.RemoveLast();
- await IndexFileManager.WrapInSemaphoreAsync(async () =>
+ await IndexFileManager.WrapInMutexAsync(async () =>
{
- await File.WriteAllLinesAsync(IndexFilePath, Index.Select(x => x.ToHeightlessLine()));
+ await IndexFileManager.WriteAllLinesAsync(Index.Select(x => x.ToHeightlessLine()));
});
}
finally

MODIFY
WalletWasabi/Stores/IoManager.cs
WalletWasabi/Stores/IoManager.cs
@@ -13,16 +13,25 @@ namespace WalletWasabi.Stores
/// </summary>
public class IoManager
{
- public string FilePath { get; }
+ public string OriginalFilePath { get; }
+ public string OldFilePath { get; }
+ public string NewFilePath { get; }
+
public string FileName { get; }
public string FileNameWithoutExtension { get; }
- private string SemaphoreName { get; }
+ private string MutexName { get; }
+
+ private const string OldExtension = ".old";
+ private const string NewExtension = ".new";
public IoManager(string filePath)
{
- FilePath = Guard.NotNullOrEmptyOrWhitespace(nameof(filePath), filePath, trim: true);
- FileName = Path.GetFileName(FilePath);
- FileNameWithoutExtension = Path.GetFileNameWithoutExtension(FilePath);
+ OriginalFilePath = Guard.NotNullOrEmptyOrWhitespace(nameof(filePath), filePath, trim: true);
+ OldFilePath = $"{OriginalFilePath}{OldExtension}";
+ NewFilePath = $"{OriginalFilePath}{NewExtension}";
+
+ FileName = Path.GetFileName(OriginalFilePath);
+ FileNameWithoutExtension = Path.GetFileNameWithoutExtension(OriginalFilePath);
// https://docs.microsoft.com/en-us/dotnet/api/system.threading.mutex?view=netframework-4.8
// On a server that is running Terminal Services, a named system mutex can have two levels of visibility.
@@ -33,14 +42,16 @@ namespace WalletWasabi.Stores
// Within a terminal server session, two mutexes whose names differ only by their prefixes are separate mutexes,
// and both are visible to all processes in the terminal server session.
// That is, the prefix names "Global\" and "Local\" describe the scope of the mutex name relative to terminal server sessions, not relative to processes.
- SemaphoreName = $"Global\\4AA0E5A2-A94F-4B92-B962-F2BBC7A68323-{FileNameWithoutExtension}";
+ MutexName = $"Global\\4AA0E5A2-A94F-4B92-B962-F2BBC7A68323-{FileNameWithoutExtension}";
}
- public async Task WrapInSemaphoreAsync(Func<Task> todo)
+ #region Mutexes
+
+ public async Task WrapInMutexAsync(Func<Task> todo)
{
- using (var semaphore = new Semaphore(1, 1, SemaphoreName))
+ using (var semaphore = new Semaphore(1, 1, MutexName))
{
- await AcquireSemaphoreAsync(semaphore);
+ await AcquireMutexAsync(semaphore);
try
{
@@ -53,7 +64,7 @@ namespace WalletWasabi.Stores
}
}
- private async Task AcquireSemaphoreAsync(Semaphore semaphore)
+ private async Task AcquireMutexAsync(Semaphore semaphore)
{
bool semaphoreAcquired = false;
try
@@ -87,9 +98,9 @@ namespace WalletWasabi.Stores
public async Task WrapInMutexAsync(Action todo)
{
- using (var semaphore = new Semaphore(1, 1, SemaphoreName))
+ using (var semaphore = new Semaphore(1, 1, MutexName))
{
- await AcquireSemaphoreAsync(semaphore);
+ await AcquireMutexAsync(semaphore);
try
{
@@ -101,5 +112,138 @@ namespace WalletWasabi.Stores
}
}
}
+
+ #endregion Mutexes
+
+ #region IoOperations
+
+ public void DeleteMe()
+ {
+ if (File.Exists(OriginalFilePath))
+ {
+ File.Delete(OriginalFilePath);
+ }
+
+ if (File.Exists(NewFilePath))
+ {
+ File.Delete(NewFilePath);
+ }
+
+ if (File.Exists(OldFilePath))
+ {
+ File.Delete(OldFilePath);
+ }
+ }
+
+ public bool TryReplaceMeWith(string sourcePath)
+ {
+ if (!File.Exists(sourcePath))
+ {
+ return false;
+ }
+
+ SafeMoveToOriginal(sourcePath);
+
+ return true;
+ }
+
+ // https://stackoverflow.com/a/7957634/2061103
+ private void SafeMoveNewToOriginal()
+ {
+ SafeMoveToOriginal(NewFilePath);
+ }
+
+ /// <summary>
+ /// Source must exist.
+ /// </summary>
+ private void SafeMoveToOriginal(string source)
+ {
+ if (File.Exists(OriginalFilePath))
+ {
+ if (File.Exists(OldFilePath))
+ {
+ File.Delete(OldFilePath);
+ }
+
+ File.Move(OriginalFilePath, OldFilePath);
+ }
+
+ File.Move(source, OriginalFilePath);
+
+ if (File.Exists(OldFilePath))
+ {
+ File.Delete(OldFilePath);
+ }
+ }
+
+ /// <summary>
+ /// https://stackoverflow.com/questions/7957544/how-to-ensure-that-data-doesnt-get-corrupted-when-saving-to-file/7957634#7957634
+ /// </summary>
+ private bool TryGetSafestFileVersion(out string safestFilePath)
+ {
+ // If foo.data and foo.data.new exist, load foo.data; foo.data.new may be broken (e.g. power off during write).
+ bool newExists = File.Exists(NewFilePath);
+ bool originalExists = File.Exists(OriginalFilePath);
+ if (newExists && originalExists)
+ {
+ safestFilePath = OriginalFilePath;
+ return true;
+ }
+
+ // If foo.data.old and foo.data.new exist, both should be valid, but something died very shortly afterwards
+ // - you may want to load the foo.data.old version anyway
+ bool oldExists = File.Exists(OldFilePath);
+ if (newExists && oldExists)
+ {
+ safestFilePath = OldFilePath;
+ return true;
+ }
+
+ // If foo.data and foo.data.old exist, then foo.data should be fine, but again something went wrong, or possibly the file couldn't be deleted.
+ // if (File.Exists(originalPath) && File.Exists(oldPath))
+ if (originalExists)
+ {
+ safestFilePath = OriginalFilePath;
+ return true;
+ }
+
+ safestFilePath = null;
+ return false;
+ }
+
+ public bool Exists()
+ {
+ return TryGetSafestFileVersion(out _);
+ }
+
+ public async Task WriteAllLinesAsync(IEnumerable<string> contents, CancellationToken cancellationToken = default)
+ {
+ await File.WriteAllLinesAsync(NewFilePath, contents, cancellationToken);
+ SafeMoveNewToOriginal();
+ }
+
+ public async Task AppendAllLinesAsync(IEnumerable<string> contents, CancellationToken cancellationToken = default)
+ {
+ // Make sure the NewFilePath exists.
+ if (TryGetSafestFileVersion(out string safestFilePath) && safestFilePath != NewFilePath)
+ {
+ File.Copy(safestFilePath, NewFilePath, overwrite: true);
+ }
+
+ await File.AppendAllLinesAsync(NewFilePath, contents, cancellationToken);
+ SafeMoveNewToOriginal();
+ }
+
+ public async Task<string[]> ReadAllLinesAsync(CancellationToken cancellationToken = default)
+ {
+ var filePath = OriginalFilePath;
+ if (TryGetSafestFileVersion(out string safestFilePath))
+ {
+ filePath = safestFilePath;
+ }
+ return await File.ReadAllLinesAsync(filePath, cancellationToken);
+ }
+
+ #endregion IoOperations
}
}

Finish safe file operations