fix(POSTV1-02): 修复便携包首次启动链路
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
This commit is contained in:
@@ -33,6 +33,12 @@ const fixedDirectories = [
|
|||||||
"logs/supervisor",
|
"logs/supervisor",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export function ensureLocalDataRuntimeDirectories(dataRoot: string) {
|
||||||
|
for (const directory of fixedDirectories) {
|
||||||
|
mkdirSync(join(resolve(dataRoot), directory), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const DATA_TRANSFER_POLICY = {
|
export const DATA_TRANSFER_POLICY = {
|
||||||
allowed_downloads: ["original_generation", "jpg", "png"],
|
allowed_downloads: ["original_generation", "jpg", "png"],
|
||||||
application_backup: false,
|
application_backup: false,
|
||||||
@@ -219,9 +225,7 @@ export function initializeLocalDataRoot(input: {
|
|||||||
|
|
||||||
const createdRoot = !existsSync(validation.normalized_path);
|
const createdRoot = !existsSync(validation.normalized_path);
|
||||||
try {
|
try {
|
||||||
for (const directory of fixedDirectories) {
|
ensureLocalDataRuntimeDirectories(validation.normalized_path);
|
||||||
mkdirSync(join(validation.normalized_path, directory), { recursive: true });
|
|
||||||
}
|
|
||||||
openInstanceDatabase(join(validation.normalized_path, "db", "dada.sqlite3"));
|
openInstanceDatabase(join(validation.normalized_path, "db", "dada.sqlite3"));
|
||||||
const configuration: InstanceConfiguration = {
|
const configuration: InstanceConfiguration = {
|
||||||
data_root: validation.normalized_path,
|
data_root: validation.normalized_path,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { registrationNotice } from "@dada/shared-contracts";
|
|||||||
|
|
||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { readBrowserSupportRelease } from "./browser-support.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 { ManagedStorage } from "./managed-storage.js";
|
||||||
import { LatestExportService } from "./latest-exports.js";
|
import { LatestExportService } from "./latest-exports.js";
|
||||||
import { CreditService } from "./credits.js";
|
import { CreditService } from "./credits.js";
|
||||||
@@ -40,6 +40,7 @@ if (credentialChannelEnabled) {
|
|||||||
.update(`Dada/P0A/${purpose}/v1`, "utf8")
|
.update(`Dada/P0A/${purpose}/v1`, "utf8")
|
||||||
.digest();
|
.digest();
|
||||||
const dataRoot = readConfiguredLocalDataRoot(instanceConfigPath);
|
const dataRoot = readConfiguredLocalDataRoot(instanceConfigPath);
|
||||||
|
ensureLocalDataRuntimeDirectories(dataRoot);
|
||||||
const databasePath = join(dataRoot, "db", "dada.sqlite3");
|
const databasePath = join(dataRoot, "db", "dada.sqlite3");
|
||||||
registration = new RegistrationService({
|
registration = new RegistrationService({
|
||||||
adminAllowlistPepper: Buffer.from(clients.adminAllowlistPepper),
|
adminAllowlistPepper: Buffer.from(clients.adminAllowlistPepper),
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ internal static class Program
|
|||||||
var supervisor = await TestSupervisorLifecycleAsync();
|
var supervisor = await TestSupervisorLifecycleAsync();
|
||||||
await TestAmapProbeSecurityAsync();
|
await TestAmapProbeSecurityAsync();
|
||||||
TestSecureConfigurationPersistence();
|
TestSecureConfigurationPersistence();
|
||||||
|
TestRuntimeDirectoryBootstrap();
|
||||||
TestStructuredLogging();
|
TestStructuredLogging();
|
||||||
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security);
|
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SEC"), security);
|
||||||
WriteEvidence(Environment.GetEnvironmentVariable("DADA_EVIDENCE_DIR_SUP"), supervisor);
|
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()
|
private static async Task<object> TestCredentialBoundaryAsync()
|
||||||
{
|
{
|
||||||
var store = new TestCredentialStore();
|
var store = new TestCredentialStore();
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ internal static class CredentialProcessLauncher
|
|||||||
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
|
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
foreach (var target in CredentialCatalog.RequiredFor(role))
|
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;
|
startInfo.UseShellExecute = false;
|
||||||
@@ -98,7 +100,9 @@ internal static class CredentialProcessLauncher
|
|||||||
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
|
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
foreach (var target in CredentialCatalog.RequiredFor(role))
|
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;
|
startInfo.UseShellExecute = false;
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ namespace Dada.Supervisor;
|
|||||||
|
|
||||||
internal sealed class SupervisorRuntime : IAsyncDisposable
|
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 readonly ICredentialStore credentials;
|
||||||
private ManagedComponentSupervisor? api;
|
private ManagedComponentSupervisor? api;
|
||||||
private ManagedComponentSupervisor? worker;
|
private ManagedComponentSupervisor? worker;
|
||||||
@@ -26,6 +39,7 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
return SupervisorState.StartupFailed;
|
return SupervisorState.StartupFailed;
|
||||||
}
|
}
|
||||||
|
EnsureRuntimeDirectories(configuration.LocalDataRoot);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger = new StructuredJsonlLogger(Path.Combine(configuration.LocalDataRoot, "logs", "supervisor"), "supervisor");
|
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");
|
var workerEntry = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs");
|
||||||
if (!File.Exists(node) || !File.Exists(apiEntry) || !File.Exists(workerEntry)) return SupervisorState.StartupFailed;
|
if (!File.Exists(node) || !File.Exists(apiEntry) || !File.Exists(workerEntry)) return SupervisorState.StartupFailed;
|
||||||
|
|
||||||
api = CreateComponent(node, apiEntry, ChildRole.Api, SupervisorState.ApiDegraded);
|
try
|
||||||
await api.StartAsync(cancellationToken);
|
{
|
||||||
|
api = CreateComponent(node, apiEntry, ChildRole.Api, SupervisorState.ApiDegraded);
|
||||||
|
await api.StartAsync(cancellationToken);
|
||||||
|
|
||||||
worker = CreateComponent(node, workerEntry, ChildRole.Worker, SupervisorState.WorkerDegraded);
|
worker = CreateComponent(node, workerEntry, ChildRole.Worker, SupervisorState.WorkerDegraded);
|
||||||
await worker.StartAsync(cancellationToken);
|
await worker.StartAsync(cancellationToken);
|
||||||
return TryLog(new StructuredLogEvent("ready", ErrorCategory: "none")) ? SupervisorState.Ready : SupervisorState.StorageUnavailable;
|
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()
|
private void EnsureAdminPepper()
|
||||||
@@ -102,10 +134,26 @@ internal sealed class SupervisorRuntime : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await StopComponentsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopComponentsAsync()
|
||||||
{
|
{
|
||||||
var stops = new List<Task>();
|
var stops = new List<Task>();
|
||||||
if (worker is not null) stops.Add(worker.DisposeAsync().AsTask());
|
if (worker is not null) stops.Add(worker.DisposeAsync().AsTask());
|
||||||
if (api is not null) stops.Add(api.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,6 +1,6 @@
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
import assert from "node:assert/strict";
|
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 { existsSync } from "node:fs";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
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/);
|
assert.match(await readFile(join(packageRoot, "server", "worker", "dist", "worker.js"), "utf8"), /GenerationProcessor/);
|
||||||
const root = await mkdtemp(join(tmpdir(), "dada-postv1-"));
|
const root = await mkdtemp(join(tmpdir(), "dada-postv1-"));
|
||||||
const dataRoot = join(root, "data");
|
const dataRoot = join(root, "data");
|
||||||
await mkdir(join(dataRoot, "db"), { recursive: true });
|
|
||||||
const configPath = join(root, "instance.json");
|
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: [] }));
|
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);
|
let api = startApi(configPath, dataRoot);
|
||||||
|
|||||||
Reference in New Issue
Block a user