Files
cneicy ee385a4107
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:48:49 +08:00

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);
}
}
}