62 lines
2 KiB
C#
62 lines
2 KiB
C#
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,
|
|
Action? contentionObserved = null)
|
|
{
|
|
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)
|
|
{
|
|
contentionObserved?.Invoke();
|
|
await Task.Delay(RetryDelay, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
_stream.Dispose();
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
}
|