fix(launcher): guard orphan bake publication

This commit is contained in:
Erik 2026-08-14 21:02:27 +02:00
parent 3f68895120
commit 208a70ac83
14 changed files with 829 additions and 55 deletions

View file

@ -120,10 +120,9 @@ public sealed class LauncherInstaller : ILauncherInstaller
_recordStore.DataDirectory,
cancellationToken)
.ConfigureAwait(false);
BakeOutputStagingContract.DeleteOwnedStagingFiles(
_recordStore.PreparedAssetPath);
InstallRecordVerification verification = await _recordStore
.LoadAndVerifyUnderLeaseAsync(cancellationToken)
InstallRecordVerification verification =
await RecoverExistingUnderPublicationGuardAsync(
cancellationToken)
.ConfigureAwait(false);
_verifiedRecord = verification.Record;
return verification;
@ -197,9 +196,8 @@ public sealed class LauncherInstaller : ILauncherInstaller
string outputPath = _recordStore.PreparedAssetPath;
string backupPath = LauncherInstallRecordStore.GetBackupPath(outputPath);
BakeOutputStagingContract.DeleteOwnedStagingFiles(outputPath);
InstallRecordVerification existing = await _recordStore
.LoadAndVerifyUnderLeaseAsync(cancellationToken)
InstallRecordVerification existing =
await RecoverExistingUnderPublicationGuardAsync(cancellationToken)
.ConfigureAwait(false);
_verifiedRecord = existing.Record;
@ -220,6 +218,7 @@ public sealed class LauncherInstaller : ILauncherInstaller
var parser = new BakeProgressJsonlParser();
var protocol = new BakeProgressProtocol();
string? publicationNonce = null;
void Observe(BakeProgressEvent progressEvent)
{
@ -265,11 +264,26 @@ public sealed class LauncherInstaller : ILauncherInstaller
try
{
cancellationToken.ThrowIfCancellationRequested();
publicationNonce = BakePublicationGuardPaths.CreateNonce();
await using (
BakePublicationGuardContract.PublicationLease publication =
await BakePublicationGuardContract.AcquireAsync(
outputPath,
cancellationToken)
.ConfigureAwait(false))
{
BakePublicationGuardContract.Authorize(
outputPath,
publicationNonce,
publication);
}
var request = new BakeProcessRequest(
_bakeExecutablePath,
validation.Directory,
outputPath,
threads);
threads,
publicationNonce);
BakeProcessResult processResult = await _processRunner.RunAsync(
request,
chunk =>
@ -370,7 +384,11 @@ public sealed class LauncherInstaller : ILauncherInstaller
.ConfigureAwait(false);
_verifiedRecord = record;
LauncherInstallRecordStore.TryDelete(backupPath);
await FinalizeSuccessfulPublicationAsync(
outputPath,
backupPath,
publicationNonce)
.ConfigureAwait(false);
Report(
progress,
LauncherInstallPhase.Completed,
@ -381,7 +399,12 @@ public sealed class LauncherInstaller : ILauncherInstaller
}
catch (OperationCanceledException)
{
RestorePreviousPackage(outputPath, backupPath, previousPreserved);
await FinalizeFailedPublicationAsync(
outputPath,
backupPath,
previousPreserved,
publicationNonce)
.ConfigureAwait(false);
Report(
progress,
LauncherInstallPhase.Cancelled,
@ -390,7 +413,12 @@ public sealed class LauncherInstaller : ILauncherInstaller
}
catch (Exception ex)
{
RestorePreviousPackage(outputPath, backupPath, previousPreserved);
await FinalizeFailedPublicationAsync(
outputPath,
backupPath,
previousPreserved,
publicationNonce)
.ConfigureAwait(false);
Report(
progress,
LauncherInstallPhase.Failed,
@ -402,10 +430,62 @@ public sealed class LauncherInstaller : ILauncherInstaller
throw new LauncherInstallException("Installation failed.", ex);
}
finally
{
BakeOutputStagingContract.DeleteOwnedStagingFiles(outputPath);
}
}
private async Task<InstallRecordVerification>
RecoverExistingUnderPublicationGuardAsync(
CancellationToken cancellationToken)
{
string outputPath = _recordStore.PreparedAssetPath;
await using BakePublicationGuardContract.PublicationLease publication =
await BakePublicationGuardContract.AcquireAsync(
outputPath,
cancellationToken)
.ConfigureAwait(false);
// Any child whose parent died before it acquired this lock is now
// irrevocably stale. A child already holding the lock must finish its
// promotion before recovery reaches this invalidation point.
BakePublicationGuardContract.Invalidate(outputPath, publication);
BakeOutputStagingContract.DeleteOwnedStagingFiles(outputPath);
return await _recordStore.LoadAndVerifyUnderLeaseAsync(cancellationToken)
.ConfigureAwait(false);
}
private static async Task FinalizeSuccessfulPublicationAsync(
string outputPath,
string backupPath,
string publicationNonce)
{
await using BakePublicationGuardContract.PublicationLease publication =
await BakePublicationGuardContract.AcquireAsync(
outputPath,
CancellationToken.None)
.ConfigureAwait(false);
BakePublicationGuardContract.Invalidate(
outputPath,
publication,
publicationNonce);
LauncherInstallRecordStore.TryDelete(backupPath);
BakeOutputStagingContract.DeleteOwnedStagingFiles(outputPath);
}
private static async Task FinalizeFailedPublicationAsync(
string outputPath,
string backupPath,
bool previousPreserved,
string? publicationNonce)
{
await using BakePublicationGuardContract.PublicationLease publication =
await BakePublicationGuardContract.AcquireAsync(
outputPath,
CancellationToken.None)
.ConfigureAwait(false);
BakePublicationGuardContract.Invalidate(
outputPath,
publication,
publicationNonce);
RestorePreviousPackage(outputPath, backupPath, previousPreserved);
BakeOutputStagingContract.DeleteOwnedStagingFiles(outputPath);
}
private bool PreservePreviousPackage(string outputPath, string backupPath)