acdream/src/AcDream.App/UI/RetailUiRuntimeLease.cs
Erik 31e6e192b3 refactor(app): extract focused window lifetime
Move the exact retryable shutdown manifest, typed root snapshot, terminal reporting, and native-window-last release out of GameWindow. Keep session and GPU convergence as hard barriers while reporting persistent physical callback cleanup without stranding dependent owners.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 19:43:52 +02:00

265 lines
8.2 KiB
C#

namespace AcDream.App.UI;
/// <summary>
/// Retains one UiHost/runtime ownership chain across partial construction,
/// initialization cleanup, lifetime retry, and explicit terminal abandonment.
/// </summary>
internal sealed class RetailUiRuntimeLease : IDisposable
{
private object? _host;
private Action? _quiesceInput;
private Action? _deactivateInput;
private Action? _disposeHost;
private Func<bool>? _hostDisposalComplete;
private object? _runtime;
private Action? _disposeRuntime;
private Func<bool>? _runtimeDisposalComplete;
private bool _disposalFailed;
private bool _inputQuiesced;
private bool _inputDeactivated;
private bool _operationActive;
private bool _disposed;
private bool _abandoned;
public bool IsDisposalComplete => _disposed;
public bool IsAbandoned => _abandoned;
internal bool HasDisposalFailure => _disposalFailed;
internal bool RetainsResources => _host is not null || _runtime is not null;
public UiHost AcquireHost(Func<UiHost> factory) => AcquireHostCore(
factory,
static host => host.QuiesceInput(),
static host => host.DeactivateInput(),
static host => host.Dispose(),
static host => host.IsDisposalComplete);
internal T AcquireHostCore<T>(
Func<T> factory,
Action<T> quiesceInput,
Action<T> deactivateInput,
Action<T> dispose,
Func<T, bool> disposalComplete)
where T : class
{
ArgumentNullException.ThrowIfNull(factory);
ArgumentNullException.ThrowIfNull(quiesceInput);
ArgumentNullException.ThrowIfNull(deactivateInput);
ArgumentNullException.ThrowIfNull(dispose);
ArgumentNullException.ThrowIfNull(disposalComplete);
ThrowIfUnavailable();
if (_operationActive || _host is not null)
throw new InvalidOperationException("A retained UI host is already owned.");
_operationActive = true;
try
{
T host = factory()
?? throw new InvalidOperationException("The retained UI host factory returned null.");
_host = host;
_quiesceInput = () => quiesceInput(host);
_deactivateInput = () => deactivateInput(host);
_disposeHost = () => dispose(host);
_hostDisposalComplete = () => disposalComplete(host);
return host;
}
finally
{
_operationActive = false;
}
}
public RetailUiRuntime Mount(Func<RetailUiRuntime> factory) => MountCore(
factory,
static runtime => runtime.InitializeForLease(),
static runtime => runtime.Dispose(),
static runtime => runtime.IsDisposalComplete);
internal T MountCore<T>(
Func<T> factory,
Action<T> initialize,
Action<T> dispose,
Func<T, bool> disposalComplete)
where T : class
{
ArgumentNullException.ThrowIfNull(factory);
ArgumentNullException.ThrowIfNull(initialize);
ArgumentNullException.ThrowIfNull(dispose);
ArgumentNullException.ThrowIfNull(disposalComplete);
ThrowIfUnavailable();
if (_operationActive)
throw new InvalidOperationException("The retained UI lease is changing state.");
if (_host is null)
throw new InvalidOperationException("The retained UI host must be acquired first.");
if (_runtime is not null)
throw new InvalidOperationException("A retained UI runtime is already owned.");
// Construction may throw while the lease still owns the host directly.
_operationActive = true;
T runtime;
try
{
runtime = factory()
?? throw new InvalidOperationException("The retained UI runtime factory returned null.");
}
finally
{
_operationActive = false;
}
// Publish the partial runtime before any Initialize side effect.
_runtime = runtime;
_disposeRuntime = () => dispose(runtime);
_runtimeDisposalComplete = () => disposalComplete(runtime);
_operationActive = true;
try
{
initialize(runtime);
_operationActive = false;
return runtime;
}
catch (Exception initializationFailure)
{
_operationActive = false;
try
{
Dispose();
}
catch (Exception cleanupFailure)
{
throw new AggregateException(
"Retail UI initialization failed and its published partial runtime did not cleanly retire.",
initializationFailure,
cleanupFailure);
}
throw;
}
}
public void QuiesceInput()
{
if (_disposed || _abandoned || _inputQuiesced)
return;
if (_operationActive)
throw new InvalidOperationException("The retained UI lease is changing state.");
_operationActive = true;
_inputQuiesced = true;
try
{
_quiesceInput?.Invoke();
}
catch
{
_inputQuiesced = false;
throw;
}
finally
{
_operationActive = false;
}
}
public void DeactivateInput()
{
if (_disposed || _abandoned || _inputDeactivated)
return;
if (_operationActive)
throw new InvalidOperationException("The retained UI lease is changing state.");
_operationActive = true;
_inputDeactivated = true;
try
{
_deactivateInput?.Invoke();
}
catch
{
_inputDeactivated = false;
throw;
}
finally
{
_operationActive = false;
}
}
public void Dispose()
{
if (_disposed || _abandoned)
return;
if (_operationActive)
throw new InvalidOperationException("The retained UI lease is changing state.");
_operationActive = true;
try
{
if (_runtime is not null)
{
_disposeRuntime!();
if (_runtimeDisposalComplete?.Invoke() != true)
throw new InvalidOperationException(
"The retained UI runtime returned without completing disposal.");
if (_hostDisposalComplete?.Invoke() != true)
throw new InvalidOperationException(
"The retained UI runtime completed without retiring its host.");
}
else if (_host is not null)
{
_disposeHost!();
if (_hostDisposalComplete?.Invoke() != true)
throw new InvalidOperationException(
"The retained UI host returned without completing disposal.");
}
_runtime = null;
_disposeRuntime = null;
_runtimeDisposalComplete = null;
_host = null;
_quiesceInput = null;
_deactivateInput = null;
_disposeHost = null;
_hostDisposalComplete = null;
_disposalFailed = false;
_disposed = true;
}
catch
{
_disposalFailed = true;
throw;
}
finally
{
_operationActive = false;
}
}
public void AbandonAfterTerminalFailure()
{
if (_disposed || _abandoned)
return;
if (_operationActive)
throw new InvalidOperationException("The retained UI lease is changing state.");
if (!_disposalFailed)
throw new InvalidOperationException(
"Retained UI resources may be abandoned only after disposal failed.");
// Keep the exact references rooted. Checkpoint J owns the final native
// fallback/reporting policy; abandonment must not turn this into an
// unowned local resource.
_abandoned = true;
}
private void ThrowIfUnavailable()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_abandoned)
throw new InvalidOperationException("The retained UI lease was abandoned.");
}
}