168 lines
6.4 KiB
C#
168 lines
6.4 KiB
C#
#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);
|
|
}
|
|
}
|
|
}
|