Manager for admin modules. It stores module entries, validates and installs modules, computes load order, and handles enable/disable operations with authorization, deadlines and a busy flag.
using SboxSuperuser.Core.Modules;
using SboxSuperuser.Engine.Authority;
using SboxSuperuser.Engine.Commands;
namespace SboxSuperuser.Engine.Modules;
internal sealed partial class ModuleManager : IModuleCatalog, IDisposable
{
private readonly Dictionary<string, ModuleEntry> _entries = new( StringComparer.Ordinal );
private readonly CommandRegistry _registry;
private readonly HostPolicyService _policy;
private readonly Func<bool> _available;
private readonly Action<Exception> _report;
private readonly Action<CommandRegistrationScope> _discover;
private readonly TimeSpan _deadline;
private IReadOnlyList<string> _order = Array.Empty<string>();
private bool _busy;
private bool _disposed;
internal ModuleManager( CommandRegistry registry, HostPolicyService policy, Func<bool> available, Action<Exception> report,
Action<CommandRegistrationScope> discover = null, TimeSpan? deadline = null )
{
_registry = registry;
_policy = policy;
_available = available;
_report = report;
_discover = discover;
_deadline = deadline ?? TimeSpan.FromSeconds( 5 );
foreach ( var id in new[] { "superuser.core", "superuser.administration" } )
{
_entries.Add( id, new ModuleEntry( null, new ModuleDescriptor( id, "Required framework security service", 1, isRequiredSecurityModule: true ) ) );
_registry.ReserveModule( id );
}
_order = _entries.Keys.ToArray();
}
public IReadOnlyList<ModuleStatus> Statuses
{
get
{
foreach ( var entry in _entries.Values )
{
Refresh( entry );
}
return Array.AsReadOnly( _entries.Values.OrderBy( entry => entry.Descriptor.Id, StringComparer.Ordinal )
.Select( entry => new ModuleStatus( entry.Descriptor, entry.State, entry.Message ) ).ToArray() );
}
}
internal void Install( IReadOnlyList<IAdminModule> modules )
{
if ( _disposed || !_available() || _busy )
{
throw new InvalidOperationException( "Module installation requires an idle authoritative runtime on its main thread." );
}
if ( modules is null || modules.Count > 126 || modules.Any( module => module is null ) )
{
throw new ArgumentException( "Supply at most 126 non-null modules.", nameof( modules ) );
}
var additions = modules.Select( module => new ModuleEntry( module, module.Descriptor
?? throw new ArgumentException( "A module descriptor is required." ) ) ).ToArray();
foreach ( var entry in additions )
{
if ( entry.Descriptor.Id.Length > 96 || entry.Descriptor.Description.Length > 2048
|| entry.Descriptor.Dependencies.Count > 128 || _registry.HasModule( entry.Descriptor.Id ) )
{
throw new ArgumentException( $"Module '{entry.Descriptor.Id}' is already owned or its metadata exceeds limits." );
}
}
var candidate = _entries.Values.Concat( additions ).ToArray();
var order = ModuleGraph.Order( candidate.Select( entry => new ModuleNode( entry.Descriptor.Id, entry.Descriptor.ApiVersion,
entry.Descriptor.Dependencies.Select( dependency => new ModuleRequirement( dependency.ModuleId, dependency.RequiredApiVersion, dependency.IsOptional ) ).ToArray() ) ).ToArray() );
// The complete candidate validates before catalog ownership changes.
foreach ( var entry in additions )
{
_entries.Add( entry.Descriptor.Id, entry );
_registry.ReserveModule( entry.Descriptor.Id );
}
_order = order;
}
internal Task<CommandResult> StartAllAsync( CancellationToken cancellation ) => ChangeAsync( () => true, null, true, cancellation );
internal Task<CommandResult> SetEnabledAsync( Func<ActorIdentity> authenticate, string id, bool enabled, CancellationToken cancellation )
=> ChangeAsync( () => CanManage( authenticate() ), id, enabled, cancellation );
private bool CanManage( ActorIdentity actor ) => _policy.IsUsable && actor is not null
&& (actor.Kind == ActorKind.DedicatedConsole || actor.Kind == ActorKind.Player && _policy.IsOwner( actor.SubjectId ));
private async Task<CommandResult> ChangeAsync( Func<bool> authorized, string id, bool enabled, CancellationToken cancellation )
{
if ( _disposed || !_available() )
{
return Result( CommandStatus.Unsupported, "An eligible authoritative runtime is required." );
}
if ( !authorized() )
{
return Result( CommandStatus.Denied, "Current owner or dedicated local console authority is required." );
}
if ( _busy )
{
return Result( CommandStatus.Conflict, "A module lifecycle operation is already in progress." );
}
if ( id is not null && !_entries.ContainsKey( id ) )
{
return Result( CommandStatus.NotFound, "Module is not installed." );
}
_busy = true;
try
{
if ( !enabled )
{
return await DisableAsync( _entries[ id ], cancellation );
}
var selected = id is null ? _order : RequiredOrder( id );
foreach ( var key in selected )
{
var result = await EnableAsync( _entries[ key ], authorized, cancellation );
if ( result.Status != CommandStatus.Success )
{
return result;
}
}
return Result( CommandStatus.Success, "Modules enabled." );
}
finally
{
_busy = false;
}
}
private IReadOnlyList<string> RequiredOrder( string id )
{
var wanted = new HashSet<string>( StringComparer.Ordinal ) { id };
foreach ( var key in _order.Reverse() )
{
if ( wanted.Contains( key ) )
{
foreach ( var dependency in _entries[ key ].Descriptor.Dependencies.Where( dependency => !dependency.IsOptional ) )
{
wanted.Add( dependency.ModuleId );
}
}
}
return _order.Where( wanted.Contains ).ToArray();
}
private static CommandResult Result( CommandStatus status, string message ) => new( status, message );
}