fix(launcher): guard orphan bake publication
This commit is contained in:
parent
3f68895120
commit
208a70ac83
14 changed files with 829 additions and 55 deletions
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="AcDream.Bake.Tests" />
|
||||
<InternalsVisibleTo Include="AcDream.Launcher.Core.Tests.Fixtures.InstallLeaseHolder" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AcDream.Content\AcDream.Content.csproj" />
|
||||
<ProjectReference Include="..\AcDream.Platform\AcDream.Platform.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,21 @@ public static class BakeOutputTransaction
|
|||
Func<string, TResult> writeTemporary,
|
||||
Action<string, TResult> validateTemporary,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> WriteValidateAndPublish(
|
||||
destinationPath,
|
||||
writeTemporary,
|
||||
validateTemporary,
|
||||
beforePublicationLock: null,
|
||||
beforePromotion: null,
|
||||
cancellationToken);
|
||||
|
||||
internal static TResult WriteValidateAndPublish<TResult>(
|
||||
string destinationPath,
|
||||
Func<string, TResult> writeTemporary,
|
||||
Action<string, TResult> validateTemporary,
|
||||
Action? beforePublicationLock,
|
||||
Action? beforePromotion,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(destinationPath);
|
||||
ArgumentNullException.ThrowIfNull(writeTemporary);
|
||||
|
|
@ -36,6 +51,14 @@ public static class BakeOutputTransaction
|
|||
cancellationToken.ThrowIfCancellationRequested();
|
||||
validateTemporary(temporaryPath, result);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
beforePublicationLock?.Invoke();
|
||||
using IDisposable? publication =
|
||||
BakePublicationGuard.AcquireIfRequested(
|
||||
fullDestination,
|
||||
cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
beforePromotion?.Invoke();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Same-volume MoveFileEx/rename is the publication primitive.
|
||||
// File.Replace additionally performs destination metadata/backup
|
||||
|
|
|
|||
80
src/AcDream.Bake/BakePublicationGuard.cs
Normal file
80
src/AcDream.Bake/BakePublicationGuard.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using AcDream.Platform;
|
||||
|
||||
namespace AcDream.Bake;
|
||||
|
||||
/// <summary>
|
||||
/// Optional launcher authorization checked immediately before atomic
|
||||
/// publication. Standalone Bake runs have no nonce environment variable and
|
||||
/// retain the original unguarded behavior.
|
||||
/// </summary>
|
||||
internal static class BakePublicationGuard
|
||||
{
|
||||
private static readonly TimeSpan RetryDelay = TimeSpan.FromMilliseconds(50);
|
||||
|
||||
internal static IDisposable? AcquireIfRequested(
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
string? nonce = Environment.GetEnvironmentVariable(
|
||||
BakePublicationGuardPaths.NonceEnvironmentVariable);
|
||||
if (nonce is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!BakePublicationGuardPaths.IsValidNonce(nonce))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The launcher bake publication nonce is invalid.");
|
||||
}
|
||||
|
||||
string lockPath = BakePublicationGuardPaths.GetPublishLockPath(
|
||||
outputPath);
|
||||
Directory.CreateDirectory(
|
||||
Path.GetDirectoryName(lockPath)
|
||||
?? throw new InvalidOperationException(
|
||||
"The bake publication lock has no parent directory."));
|
||||
|
||||
FileStream? lease = null;
|
||||
while (lease is null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
lease = new FileStream(
|
||||
lockPath,
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.None,
|
||||
bufferSize: 1,
|
||||
options: FileOptions.None);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
cancellationToken.WaitHandle.WaitOne(RetryDelay);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string authorizationPath =
|
||||
BakePublicationGuardPaths.GetAuthorizationPath(outputPath);
|
||||
string authorized = File.Exists(authorizationPath)
|
||||
? File.ReadAllText(authorizationPath)
|
||||
: string.Empty;
|
||||
if (!string.Equals(authorized, nonce, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"This bake process is no longer authorized to publish its output.");
|
||||
}
|
||||
|
||||
return lease;
|
||||
}
|
||||
catch
|
||||
{
|
||||
lease.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue