using System.Diagnostics; namespace Dada.Supervisor; internal sealed class SupervisorRuntime : IAsyncDisposable { private readonly ICredentialStore credentials; private ManagedComponentSupervisor? api; private ManagedComponentSupervisor? worker; private StructuredJsonlLogger? logger; internal SupervisorRuntime(ICredentialStore credentials) { this.credentials = credentials; } internal event Action? StateChanged; internal async Task StartAsync(CancellationToken cancellationToken = default) { if (LoopbackPortGuard.Check() == SupervisorState.PortInUse) return SupervisorState.PortInUse; var configuration = new InstanceConfigurationStore().Load(); if (configuration.LocalDataRoot is null || configuration.AssetRoot is null || !Directory.Exists(configuration.LocalDataRoot) || !Directory.Exists(configuration.AssetRoot)) { return SupervisorState.StartupFailed; } try { logger = new StructuredJsonlLogger(Path.Combine(configuration.LocalDataRoot, "logs", "supervisor"), "supervisor"); logger.Write(new StructuredLogEvent("starting", ErrorCategory: "none")); } catch (LogWriteException) { return SupervisorState.StorageUnavailable; } if (CredentialCatalog.RequiredFor(ChildRole.Api).Concat(CredentialCatalog.RequiredFor(ChildRole.Worker)).Any(target => !credentials.IsConfigured(target))) { return SupervisorState.StartupFailed; } var node = Path.Combine(AppContext.BaseDirectory, "runtime", "node.exe"); var apiEntry = Path.Combine(AppContext.BaseDirectory, "server", "api.mjs"); var workerEntry = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs"); if (!File.Exists(node) || !File.Exists(apiEntry) || !File.Exists(workerEntry)) return SupervisorState.StartupFailed; api = CreateComponent(node, apiEntry, ChildRole.Api, SupervisorState.ApiDegraded); await api.StartAsync(cancellationToken); worker = CreateComponent(node, workerEntry, ChildRole.Worker, SupervisorState.WorkerDegraded); await worker.StartAsync(cancellationToken); return TryLog(new StructuredLogEvent("ready", ErrorCategory: "none")) ? SupervisorState.Ready : SupervisorState.StorageUnavailable; } private ManagedComponentSupervisor CreateComponent(string node, string entry, ChildRole role, SupervisorState degradedState) { var component = new ManagedComponentSupervisor(async cancellationToken => { var startInfo = new ProcessStartInfo(node); startInfo.WorkingDirectory = AppContext.BaseDirectory; startInfo.Environment["DADA_SQLITE_NATIVE_BINDING"] = Path.Combine(AppContext.BaseDirectory, "server", "native", "better_sqlite3.node"); startInfo.Environment["DADA_SUPPORT_GATE_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web", "support-gate"); startInfo.ArgumentList.Add(entry); var child = await ManagedChildProcess.StartAsync(startInfo, role, credentials, cancellationToken); child.StatusReceived += status => { if (status == "storage_unavailable") { TryLog(new StructuredLogEvent("unavailable", ErrorCategory: "log_write_failed")); StateChanged?.Invoke(SupervisorState.StorageUnavailable); } }; return child; }, degradedState); component.StateChanged += state => { if (TryLog(new StructuredLogEvent("degraded", ErrorCategory: "service_unavailable"))) StateChanged?.Invoke(state); }; return component; } private bool TryLog(StructuredLogEvent entry) { try { logger?.Write(entry); return true; } catch (LogWriteException) { StateChanged?.Invoke(SupervisorState.StorageUnavailable); return false; } } public async ValueTask DisposeAsync() { var stops = new List(); if (worker is not null) stops.Add(worker.DisposeAsync().AsTask()); if (api is not null) stops.Add(api.DisposeAsync().AsTask()); await Task.WhenAll(stops); } }