fix(launcher): harden installer transactions

This commit is contained in:
Erik 2026-08-14 20:36:11 +02:00
parent ff6ebb6a6a
commit 3f68895120
21 changed files with 1164 additions and 61 deletions

View file

@ -0,0 +1,60 @@
namespace AcDream.Launcher.Core.Installation;
/// <summary>
/// Cross-process ownership for every mutation or recovery of one launcher
/// DataDirectory. The persistent lock pathname is harmless; exclusivity is
/// owned by the open OS handle and therefore disappears if the process dies.
/// </summary>
internal sealed class InstallerTransactionLease : IAsyncDisposable
{
internal const string LockFileName = ".install.lock";
private static readonly TimeSpan RetryDelay = TimeSpan.FromMilliseconds(50);
private readonly FileStream _stream;
private InstallerTransactionLease(FileStream stream)
{
_stream = stream;
}
internal static string GetLockPath(string dataDirectory) =>
Path.Combine(Path.GetFullPath(dataDirectory), LockFileName);
internal static async ValueTask<InstallerTransactionLease> AcquireAsync(
string dataDirectory,
CancellationToken cancellationToken = default)
{
string lockPath = GetLockPath(dataDirectory);
Directory.CreateDirectory(
Path.GetDirectoryName(lockPath)
?? throw new InvalidOperationException(
"The installer lock path has no parent directory."));
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var stream = new FileStream(
lockPath,
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None,
bufferSize: 1,
FileOptions.None);
return new InstallerTransactionLease(stream);
}
catch (IOException)
{
await Task.Delay(RetryDelay, cancellationToken)
.ConfigureAwait(false);
}
}
}
public ValueTask DisposeAsync()
{
_stream.Dispose();
return ValueTask.CompletedTask;
}
}