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