This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkApp
|
||||
{
|
||||
public sealed class ShrinkAppHost : MonoBehaviour
|
||||
{
|
||||
private readonly List<string> _startedModules = new();
|
||||
private ShrinkAppContext? _context;
|
||||
private bool _unityStarted;
|
||||
private bool _publishedStartedEvent;
|
||||
|
||||
public bool IsRunning { get; private set; }
|
||||
public bool HasFailed { get; private set; }
|
||||
public string LastError { get; private set; } = string.Empty;
|
||||
public IReadOnlyList<string> StartedModules => _startedModules;
|
||||
|
||||
internal void Configure(ShrinkAppContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
internal async UniTask StartAppAsync()
|
||||
{
|
||||
if (IsRunning || HasFailed)
|
||||
return;
|
||||
if (_context == null)
|
||||
throw new InvalidOperationException("ShrinkAppHost is not configured.");
|
||||
|
||||
var installers = CreateInstallerInstances();
|
||||
var orderedInstallers = SortInstallers(installers, _context.Settings);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var installer in orderedInstallers)
|
||||
installer.RegisterServices(_context);
|
||||
|
||||
foreach (var installer in orderedInstallers)
|
||||
{
|
||||
await installer.InitializeAsync(_context);
|
||||
_startedModules.Add(installer.ModuleId);
|
||||
}
|
||||
|
||||
IsRunning = true;
|
||||
PublishStartedEventIfReady();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HasFailed = true;
|
||||
LastError = ex.Message;
|
||||
EventBus.Post(new ShrinkAppStartFailedEvent
|
||||
{
|
||||
ErrorMessage = ex.Message,
|
||||
FailedModuleId = _startedModules.LastOrDefault() ?? string.Empty
|
||||
});
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_unityStarted = true;
|
||||
PublishStartedEventIfReady();
|
||||
}
|
||||
|
||||
private void PublishStartedEventIfReady()
|
||||
{
|
||||
if (_publishedStartedEvent || !_unityStarted || !IsRunning)
|
||||
return;
|
||||
|
||||
_publishedStartedEvent = true;
|
||||
EventBus.Post(new ShrinkAppStartedEvent
|
||||
{
|
||||
ModuleIds = _startedModules.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IShrinkAppModuleInstaller> CreateInstallerInstances()
|
||||
{
|
||||
var installers = new List<IShrinkAppModuleInstaller>();
|
||||
foreach (var installerType in ShrinkAppGeneratedRegistry.GetInstallerTypes())
|
||||
{
|
||||
if (installerType == null)
|
||||
continue;
|
||||
if (!typeof(IShrinkAppModuleInstaller).IsAssignableFrom(installerType))
|
||||
throw new InvalidOperationException($"Installer type does not implement IShrinkAppModuleInstaller: {installerType.FullName}");
|
||||
if (installerType.IsAbstract)
|
||||
throw new InvalidOperationException($"Installer type cannot be abstract: {installerType.FullName}");
|
||||
|
||||
if (Activator.CreateInstance(installerType) is not IShrinkAppModuleInstaller installer)
|
||||
throw new InvalidOperationException($"Failed to create installer: {installerType.FullName}");
|
||||
|
||||
installers.Add(installer);
|
||||
}
|
||||
|
||||
return installers;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IShrinkAppModuleInstaller> SortInstallers(
|
||||
IReadOnlyList<IShrinkAppModuleInstaller> installers,
|
||||
ShrinkAppSettings settings)
|
||||
{
|
||||
var disabled = new HashSet<string>(
|
||||
settings.disabledModuleIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()) ??
|
||||
Enumerable.Empty<string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var enabledInstallers = installers
|
||||
.Where(installer => !disabled.Contains(installer.ModuleId))
|
||||
.ToArray();
|
||||
|
||||
var byId = new Dictionary<string, IShrinkAppModuleInstaller>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var installer in enabledInstallers)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(installer.ModuleId))
|
||||
throw new InvalidOperationException($"Installer {installer.GetType().FullName} has an empty ModuleId.");
|
||||
if (!byId.TryAdd(installer.ModuleId.Trim(), installer))
|
||||
throw new InvalidOperationException($"Duplicate installer ModuleId: {installer.ModuleId}");
|
||||
}
|
||||
|
||||
var visitState = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var ordered = new List<IShrinkAppModuleInstaller>();
|
||||
foreach (var installer in enabledInstallers.OrderBy(item => item.Order).ThenBy(item => item.ModuleId, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
Visit(installer, byId, visitState, ordered);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private static void Visit(
|
||||
IShrinkAppModuleInstaller installer,
|
||||
IReadOnlyDictionary<string, IShrinkAppModuleInstaller> byId,
|
||||
IDictionary<string, int> visitState,
|
||||
IList<IShrinkAppModuleInstaller> ordered)
|
||||
{
|
||||
var state = visitState.TryGetValue(installer.ModuleId, out var existingState) ? existingState : 0;
|
||||
if (state == 2)
|
||||
return;
|
||||
if (state == 1)
|
||||
throw new InvalidOperationException($"Circular installer dependency detected at module: {installer.ModuleId}");
|
||||
|
||||
visitState[installer.ModuleId] = 1;
|
||||
foreach (var dependencyId in installer.DependsOn ?? Array.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dependencyId))
|
||||
continue;
|
||||
|
||||
if (!byId.TryGetValue(dependencyId.Trim(), out var dependency))
|
||||
throw new InvalidOperationException(
|
||||
$"Installer dependency missing. Module={installer.ModuleId}, DependsOn={dependencyId}");
|
||||
|
||||
Visit(dependency, byId, visitState, ordered);
|
||||
}
|
||||
|
||||
visitState[installer.ModuleId] = 2;
|
||||
if (!ordered.Contains(installer))
|
||||
ordered.Add(installer);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user