using AcDream.Headless.Configuration; using AcDream.Headless.Credentials; using AcDream.Headless.Diagnostics; using AcDream.Headless.Platform; using AcDream.Runtime; using AcDream.Runtime.Session; namespace AcDream.Headless.Hosting; internal sealed class HeadlessProcessHost : IDisposable { private readonly HeadlessSessionHost[] _sessions; private readonly HeadlessProcessScheduler _scheduler; private readonly HeadlessDiagnosticWriter _diagnostics; private readonly HeadlessProcessContentOwner? _content; private readonly HeadlessProcessResourceSampler _resources; private int _disposeIndex; private bool _disposed; internal HeadlessProcessHost( HeadlessConfiguration configuration, HeadlessPathSet paths, TextReader standardInput, TextWriter diagnostics, ILiveSessionOperations? sessionOperations = null, TimeProvider? timeProvider = null, IHeadlessProcessContentFactory? contentFactory = null, HeadlessDirectCredentials? directCredentials = null) { ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(paths); ArgumentNullException.ThrowIfNull(standardInput); ArgumentNullException.ThrowIfNull(diagnostics); if (configuration.Sessions.Count == 0) { throw new HeadlessConfigurationException( "Headless run mode requires at least one session."); } if (directCredentials is not null && configuration.Sessions.Count != 1) { throw new HeadlessConfigurationException( "Direct credentials require exactly one configured session."); } HeadlessStaticStateAudit.ValidateProcessIsolation( configuration.Sessions.Count); _diagnostics = new HeadlessDiagnosticWriter(diagnostics); var credentials = new HeadlessCredentialResolver( standardInput, paths.ConfigDirectory); var sessions = new List( configuration.Sessions.Count); HeadlessProcessContentOwner? content = null; HeadlessProcessResourceSampler? resources = null; try { if (configuration.Process?.Content is { } contentDescriptor) { content = new HeadlessProcessContentOwner( contentDescriptor, message => _diagnostics.Message( "process-content", message), contentFactory); } foreach (HeadlessSessionDescriptor? candidate in configuration.Sessions) { HeadlessSessionDescriptor descriptor = candidate ?? throw new HeadlessConfigurationException( "Headless run mode cannot contain a null session."); if (directCredentials is not null) { descriptor = WithAccount( descriptor, directCredentials.User); } HeadlessCredentialSecret secret = directCredentials is null ? credentials.Resolve( descriptor.Id, descriptor.Credential) : new HeadlessCredentialSecret( "command-line", directCredentials.Password); HeadlessProcessContentOwner.HeadlessProcessContentLease? contentLease = content?.AcquireLease(descriptor.Id); try { sessions.Add(new HeadlessSessionHost( descriptor, secret, _diagnostics, sessionOperations, timeProvider, contentLease: contentLease)); } catch { contentLease?.Dispose(); secret.Dispose(); throw; } } _sessions = sessions.ToArray(); resources = new HeadlessProcessResourceSampler(timeProvider); _scheduler = new HeadlessProcessScheduler( _sessions, timeProvider, observation: snapshot => CaptureResources( "periodic", resources, snapshot, content)); _resources = resources; _content = content; _disposeIndex = _sessions.Length - 1; } catch { resources?.Dispose(); for (int index = sessions.Count - 1; index >= 0; index--) sessions[index].Dispose(); content?.Dispose(); throw; } } internal HeadlessSessionHost Session => _sessions.Length == 1 ? _sessions[0] : throw new InvalidOperationException( "The process owns more than one headless session."); internal IReadOnlyList Sessions => _sessions; internal HeadlessSchedulerSnapshot Scheduler => _scheduler.CaptureSnapshot(); internal HeadlessProcessContentSnapshot? Content => _content?.CaptureSnapshot(); private static HeadlessSessionDescriptor WithAccount( HeadlessSessionDescriptor source, string account) => new() { Id = source.Id, Endpoint = source.Endpoint, Account = account, Character = source.Character, Policy = source.Policy, Credential = source.Credential, }; internal async Task RunAsync( CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); foreach (HeadlessSessionHost session in _sessions) { RuntimeSessionStartResult started = session.Start(); if (started.Status != RuntimeSessionStartStatus.Connected) { if (started.Error is { } error) { _diagnostics.Failure( session.SessionId, "start", error); } return HeadlessExitCode.ConnectionError; } _diagnostics.Lifecycle( session.SessionId, "running", session.Runtime); } _scheduler.RebaseDeadlinesAfterSessionStart(); CaptureResources( "running-start", _resources, _scheduler.CaptureSnapshot(), _content); try { await _scheduler.RunAsync(cancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception error) { _diagnostics.Failure("process", "tick", error); return HeadlessExitCode.RuntimeError; } CaptureResources( "running-stop", _resources, _scheduler.CaptureSnapshot(), _content); return _scheduler.CaptureSnapshot().FaultedSessionCount > 0 ? HeadlessExitCode.RuntimeError : HeadlessExitCode.Success; } public void Dispose() { if (_disposed) return; while (_disposeIndex >= 0) { _sessions[_disposeIndex].Dispose(); _disposeIndex--; } _content?.Dispose(); CaptureResources( "disposed", _resources, _scheduler.CaptureSnapshot(), _content); _resources.Dispose(); _disposed = true; } private void CaptureResources( string state, HeadlessProcessResourceSampler resources, HeadlessSchedulerSnapshot scheduler, HeadlessProcessContentOwner? content) { HeadlessProcessResourceSnapshot snapshot = resources.Capture( state, _sessions, scheduler, content?.CaptureSnapshot()); _diagnostics.Resources(state, snapshot); } }