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,121 @@
namespace AcDream.Launcher.Core.Installation;
/// <summary>
/// Strict state machine for known v1 bake events. Human output, unknown v1
/// events, and future versions are deliberately transparent; known v1 events
/// cannot be reordered, duplicated, or appended after the first terminal.
/// </summary>
internal sealed class BakeProgressProtocol
{
private BakeProgressProtocolState _state;
internal BakeStartedEvent? Started { get; private set; }
internal BakeCompletedEvent? Completed { get; private set; }
internal BakeErrorEvent? Error { get; private set; }
internal string? Violation { get; private set; }
internal bool Observe(BakeProgressEvent progressEvent)
{
ArgumentNullException.ThrowIfNull(progressEvent);
if (Violation is not null)
{
return false;
}
switch (progressEvent)
{
case BakeHumanOutputEvent:
case UnknownBakeProgressEvent:
case FutureBakeProgressEvent:
return true;
case MalformedBakeProgressEvent malformed:
Reject($"Malformed bake progress: {malformed.Reason}");
return false;
case BakeStartedEvent started:
if (_state != BakeProgressProtocolState.AwaitingStarted)
{
Reject(_state == BakeProgressProtocolState.Running
? "The bake protocol emitted more than one v1 started event."
: "The bake protocol emitted a known event after its terminal event.");
return false;
}
Started = started;
_state = BakeProgressProtocolState.Running;
return true;
case BakeWorkProgressEvent:
if (_state != BakeProgressProtocolState.Running)
{
Reject(KnownEventStateViolation("progress"));
return false;
}
return true;
case BakeCompletedEvent completed:
if (_state != BakeProgressProtocolState.Running)
{
Reject(KnownEventStateViolation("completed"));
return false;
}
Completed = completed;
_state = BakeProgressProtocolState.Completed;
return true;
case BakeErrorEvent error:
if (_state != BakeProgressProtocolState.Running)
{
Reject(KnownEventStateViolation("error"));
return false;
}
Error = error;
_state = BakeProgressProtocolState.Error;
return true;
default:
Reject("The bake protocol emitted an unsupported known event.");
return false;
}
}
internal void CompleteInput()
{
if (Violation is not null)
{
return;
}
if (_state == BakeProgressProtocolState.AwaitingStarted)
{
Reject("The bake protocol did not emit a v1 started event first.");
}
else if (_state == BakeProgressProtocolState.Running)
{
Reject("The bake protocol ended without exactly one terminal event.");
}
}
private string KnownEventStateViolation(string eventName) => _state switch
{
BakeProgressProtocolState.AwaitingStarted =>
$"The bake protocol emitted v1 {eventName} before v1 started.",
BakeProgressProtocolState.Running =>
$"The bake protocol emitted an invalid v1 {eventName} event.",
_ => "The bake protocol emitted a known event after its terminal event.",
};
private void Reject(string message)
{
Violation ??= message;
}
private enum BakeProgressProtocolState
{
AwaitingStarted,
Running,
Completed,
Error,
}
}