fix(POSTV1-02): 修复便携包首次启动链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run

This commit is contained in:
suyx
2026-08-05 12:00:23 +08:00
parent 79b01ebc81
commit 43d946bb5c
6 changed files with 94 additions and 14 deletions
+7 -3
View File
@@ -33,6 +33,12 @@ const fixedDirectories = [
"logs/supervisor",
] as const;
export function ensureLocalDataRuntimeDirectories(dataRoot: string) {
for (const directory of fixedDirectories) {
mkdirSync(join(resolve(dataRoot), directory), { recursive: true });
}
}
export const DATA_TRANSFER_POLICY = {
allowed_downloads: ["original_generation", "jpg", "png"],
application_backup: false,
@@ -219,9 +225,7 @@ export function initializeLocalDataRoot(input: {
const createdRoot = !existsSync(validation.normalized_path);
try {
for (const directory of fixedDirectories) {
mkdirSync(join(validation.normalized_path, directory), { recursive: true });
}
ensureLocalDataRuntimeDirectories(validation.normalized_path);
openInstanceDatabase(join(validation.normalized_path, "db", "dada.sqlite3"));
const configuration: InstanceConfiguration = {
data_root: validation.normalized_path,
+2 -1
View File
@@ -5,7 +5,7 @@ import { registrationNotice } from "@dada/shared-contracts";
import { createApp } from "./app.js";
import { readBrowserSupportRelease } from "./browser-support.js";
import { defaultInstanceConfigPath, readConfiguredLocalDataRoot } from "./local-data-root.js";
import { defaultInstanceConfigPath, ensureLocalDataRuntimeDirectories, readConfiguredLocalDataRoot } from "./local-data-root.js";
import { ManagedStorage } from "./managed-storage.js";
import { LatestExportService } from "./latest-exports.js";
import { CreditService } from "./credits.js";
@@ -40,6 +40,7 @@ if (credentialChannelEnabled) {
.update(`Dada/P0A/${purpose}/v1`, "utf8")
.digest();
const dataRoot = readConfiguredLocalDataRoot(instanceConfigPath);
ensureLocalDataRuntimeDirectories(dataRoot);
const databasePath = join(dataRoot, "db", "dada.sqlite3");
registration = new RegistrationService({
adminAllowlistPepper: Buffer.from(clients.adminAllowlistPepper),
@@ -40,6 +40,7 @@ internal static class Program
var supervisor = await TestSupervisorLifecycleAsync();
await TestAmapProbeSecurityAsync();
TestSecureConfigurationPersistence();
TestRuntimeDirectoryBootstrap();
TestStructuredLogging();
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security);
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SUP"), supervisor);
@@ -125,6 +126,29 @@ internal static class Program
}
}
private static void TestRuntimeDirectoryBootstrap()
{
var root = Path.Combine(Path.GetTempPath(), $"dada-runtime-root-{Guid.NewGuid():N}");
try
{
Directory.CreateDirectory(root);
SupervisorRuntime.EnsureRuntimeDirectories(root);
foreach (var relativePath in new[]
{
"db", "content/references", "content/generated", "content/exports",
"managed-assets", "derived-assets", "staging",
"logs/api", "logs/worker", "logs/supervisor",
})
{
True(Directory.Exists(Path.Combine(root, relativePath)), $"runtime directory missing: {relativePath}");
}
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
private static async Task<object> TestCredentialBoundaryAsync()
{
var store = new TestCredentialStore();
+6 -2
View File
@@ -50,7 +50,9 @@ internal static class CredentialProcessLauncher
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var target in CredentialCatalog.RequiredFor(role))
{
credentials[target] = store.Read(target) ?? string.Empty;
var value = store.Read(target);
if (role == ChildRole.Worker && string.IsNullOrWhiteSpace(value)) throw new MissingCredentialException(target);
credentials[target] = value ?? string.Empty;
}
startInfo.UseShellExecute = false;
@@ -98,7 +100,9 @@ internal static class CredentialProcessLauncher
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var target in CredentialCatalog.RequiredFor(role))
{
credentials[target] = store.Read(target) ?? string.Empty;
var value = store.Read(target);
if (role == ChildRole.Worker && string.IsNullOrWhiteSpace(value)) throw new MissingCredentialException(target);
credentials[target] = value ?? string.Empty;
}
startInfo.UseShellExecute = false;
@@ -5,6 +5,19 @@ 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;
@@ -26,6 +39,7 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
{
return SupervisorState.StartupFailed;
}
EnsureRuntimeDirectories(configuration.LocalDataRoot);
try
{
logger = new StructuredJsonlLogger(Path.Combine(configuration.LocalDataRoot, "logs", "supervisor"), "supervisor");
@@ -42,12 +56,30 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
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);
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;
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()
@@ -102,10 +134,26 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
}
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());
await Task.WhenAll(stops);
try
{
await Task.WhenAll(stops);
}
catch
{
}
finally
{
worker = null;
api = null;
}
}
}
+1 -2
View File
@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
@@ -49,7 +49,6 @@ test("portable package serves the product and keeps SQLite data across API resta
assert.match(await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8"), /GenerationProcessor/);
const root = await mkdtemp(join(tmpdir(), "dada-postv1-"));
const dataRoot = join(root, "data");
await mkdir(join(dataRoot, "db"), { recursive: true });
const configPath = join(root, "instance.json");
await writeFile(configPath, JSON.stringify({ data_root: dataRoot, initialized: true, instance_id: "portable-test", schema_version: 1, secure_config_revision: 1, admin_allowlist_hashes: [], admin_recovery_hashes: [] }));
let api = startApi(configPath, dataRoot);