Files
tyx_AI_xhs/supervisor/Dada.Supervisor/SupervisorRuntime.cs
T
suyx 43d946bb5c
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
fix(POSTV1-02): 修复便携包首次启动链路
2026-08-05 12:00:23 +08:00

160 lines
6.0 KiB
C#

using System.Diagnostics;
using System.Security.Cryptography;
namespace Dada.Supervisor;
internal sealed class SupervisorRuntime : IAsyncDisposable
{
private static readonly string[] RequiredDataDirectories =
[
"db",
Path.Combine("content", "references"),
Path.Combine("content", "generated"),
Path.Combine("content", "exports"),
"managed-assets",
"derived-assets",
"staging",
Path.Combine("logs", "api"),
Path.Combine("logs", "worker"),
Path.Combine("logs", "supervisor"),
];
private readonly ICredentialStore credentials;
private ManagedComponentSupervisor? api;
private ManagedComponentSupervisor? worker;
private StructuredJsonlLogger? logger;
internal SupervisorRuntime(ICredentialStore credentials)
{
this.credentials = credentials;
}
internal event Action<SupervisorState>? StateChanged;
internal async Task<SupervisorState> 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;
}
EnsureRuntimeDirectories(configuration.LocalDataRoot);
try
{
logger = new StructuredJsonlLogger(Path.Combine(configuration.LocalDataRoot, "logs", "supervisor"), "supervisor");
logger.Write(new StructuredLogEvent("starting", ErrorCategory: "none"));
}
catch (LogWriteException)
{
return SupervisorState.StorageUnavailable;
}
EnsureAdminPepper();
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;
try
{
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;
}
catch
{
await StopComponentsAsync();
return TryLog(new StructuredLogEvent("failed", ErrorCategory: "service_unavailable"))
? SupervisorState.StartupFailed
: SupervisorState.StorageUnavailable;
}
}
internal static void EnsureRuntimeDirectories(string dataRoot)
{
foreach (var directory in RequiredDataDirectories)
{
Directory.CreateDirectory(Path.Combine(dataRoot, directory));
}
}
private void EnsureAdminPepper()
{
if (credentials.IsConfigured(CredentialCatalog.AdminPepper)) return;
var pepper = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
credentials.Write(CredentialCatalog.AdminPepper, pepper);
Array.Clear(System.Text.Encoding.UTF8.GetBytes(pepper));
}
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.Environment["DADA_WEB_ROOT"] = Path.Combine(AppContext.BaseDirectory, "web");
startInfo.Environment["DADA_INSTANCE_CONFIG_PATH"] = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Dada", "P0A", "config", "instance.json");
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()
{
await StopComponentsAsync();
}
private async Task StopComponentsAsync()
{
var stops = new List<Task>();
if (worker is not null) stops.Add(worker.DisposeAsync().AsTask());
if (api is not null) stops.Add(api.DisposeAsync().AsTask());
try
{
await Task.WhenAll(stops);
}
catch
{
}
finally
{
worker = null;
api = null;
}
}
}