using System.Diagnostics.CodeAnalysis; using DotPython.Contracts; using DotPython.Runtime.Managed; namespace DotPython.Hosting; /// /// Owns a managed DotPython runtime and typed module clients for small applications. /// public sealed class DotPythonHost : IAsyncDisposable { private readonly object _gate = new(); private readonly DotPythonModuleProvider _provider; private readonly IDotPythonModuleRuntime _runtime; private readonly List _sessions = []; private Task? _disposeTask; private bool _disposed; internal DotPythonHost(IDotPythonModuleRuntime runtime) { _runtime = runtime; _provider = new DotPythonModuleProvider(runtime); } /// Creates a host that owns a managed DotPython runtime. [SuppressMessage( "Reliability", "CA2000:Dispose objects before losing scope", Justification = "The returned host takes ownership of the managed runtime." )] public static DotPythonHost CreateManaged() => new(new ManagedPythonModuleRuntime()); /// /// Creates a host that takes ownership of a backend-independent module runtime. /// public static DotPythonHost Create(IDotPythonModuleRuntime runtime) { ArgumentNullException.ThrowIfNull(runtime); return new DotPythonHost(runtime); } /// Creates a typed client for a per-runtime module. public TService GetModule(PythonModuleRegistration registration) where TService : class => GetModule(registration, static _ => { }); /// Creates a configured typed client for a per-runtime module. public TService GetModule( PythonModuleRegistration registration, Action configure ) where TService : class { ArgumentNullException.ThrowIfNull(registration); ArgumentNullException.ThrowIfNull(configure); if (registration.StatePolicy != PythonModuleStatePolicy.PerRuntime) { throw new InvalidOperationException( "Per-session modules must be resolved from a DotPythonModuleSession." ); } lock (_gate) { ObjectDisposedException.ThrowIf(_disposed, this); ConfigureProvider(_provider, registration.Definition, configure); return registration.CreateClient(_provider); } } /// Loads and validates a per-runtime module before its first invocation. public ValueTask WarmUpAsync( PythonModuleRegistration registration, CancellationToken cancellationToken = default ) where TService : class => WarmUpAsync(registration, static _ => { }, cancellationToken); /// Loads and validates a configured per-runtime module. public ValueTask WarmUpAsync( PythonModuleRegistration registration, Action configure, CancellationToken cancellationToken = default ) where TService : class { ArgumentNullException.ThrowIfNull(registration); ArgumentNullException.ThrowIfNull(configure); if (registration.StatePolicy != PythonModuleStatePolicy.PerRuntime) { throw new InvalidOperationException( "Per-session modules must be warmed within a DotPythonModuleSession." ); } lock (_gate) { ObjectDisposedException.ThrowIf(_disposed, this); ConfigureProvider(_provider, registration.Definition, configure); return _provider.WarmUpAsync(registration.Definition, cancellationToken); } } /// /// Creates a logical state scope for per-session modules. This is not a security boundary. /// public DotPythonModuleSession CreateSession() { lock (_gate) { ObjectDisposedException.ThrowIf(_disposed, this); var session = new DotPythonModuleSession(_runtime, RemoveSession); _sessions.Add(session); return session; } } /// public ValueTask DisposeAsync() { lock (_gate) { if (_disposeTask is not null) { return new ValueTask(_disposeTask); } _disposed = true; _disposeTask = DisposeCoreAsync(_sessions.AsEnumerable().Reverse().ToArray()); return new ValueTask(_disposeTask); } } private async Task DisposeCoreAsync(IReadOnlyList sessions) { try { foreach (var session in sessions) { await session.DisposeFromHostAsync().ConfigureAwait(false); } } finally { try { await _provider.DisposeAsync().ConfigureAwait(false); } finally { await _runtime.DisposeAsync().ConfigureAwait(false); } } } private void RemoveSession(DotPythonModuleSession session) { lock (_gate) { _sessions.Remove(session); } } internal static void ConfigureProvider( DotPythonModuleProvider provider, PythonModuleDefinition definition, Action configure ) { var options = new DotPythonModuleHostingOptions(); configure(options); options.Validate(); provider.ConfigureInitialization(definition, options.MaximumInitializationAttempts); } }