mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:40:43 +00:00
perf(config): reuse prepared snapshots for daemon token writes
This commit is contained in:
@@ -67,11 +67,15 @@ vi.mock("../../bootstrap/node-startup-env.js", () => ({
|
||||
|
||||
vi.mock("../../config/io.js", () => ({
|
||||
loadConfig: loadConfigMock,
|
||||
readBestEffortConfig: loadConfigMock,
|
||||
readConfigFileSnapshotForWrite: vi.fn(async () => ({
|
||||
snapshot: await readConfigFileSnapshotMock(),
|
||||
writeOptions: { expectedConfigPath: "/tmp/openclaw.json" },
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/paths.js", () => ({
|
||||
resolveGatewayPort: resolveGatewayPortMock,
|
||||
resolveIsNixMode: resolveIsNixModeMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../commands/gateway-install-token.persist.runtime.js", () => ({
|
||||
@@ -79,10 +83,6 @@ vi.mock("../../commands/gateway-install-token.persist.runtime.js", () => ({
|
||||
replaceConfigFile: replaceConfigFileMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../config/paths.js", () => ({
|
||||
resolveIsNixMode: resolveIsNixModeMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../config/types.secrets.js", () => ({
|
||||
hasConfiguredSecretInput: hasConfiguredSecretInputMock,
|
||||
resolveSecretInputRef: resolveSecretInputRefMock,
|
||||
@@ -190,7 +190,12 @@ describe("runDaemonInstall", () => {
|
||||
actionState.failed.length = 0;
|
||||
|
||||
loadConfigMock.mockReturnValue({ gateway: { auth: { mode: "token" } } });
|
||||
readConfigFileSnapshotMock.mockResolvedValue({ exists: false, valid: true, config: {} });
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
exists: false,
|
||||
valid: true,
|
||||
config: {},
|
||||
sourceConfig: { gateway: { auth: { mode: "token" } } },
|
||||
});
|
||||
resolveGatewayPortMock.mockReturnValue(18789);
|
||||
resolveIsNixModeMock.mockReturnValue(false);
|
||||
resolveSecretInputRefMock.mockReturnValue({ ref: undefined });
|
||||
@@ -274,6 +279,7 @@ describe("runDaemonInstall", () => {
|
||||
exists: true,
|
||||
valid: true,
|
||||
config: { gateway: { auth: { mode: "token" } } },
|
||||
sourceConfig: { gateway: { auth: { mode: "token" } } },
|
||||
});
|
||||
|
||||
await runDaemonInstall({ json: true });
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
isGatewayDaemonRuntime,
|
||||
} from "../../commands/daemon-runtime.js";
|
||||
import { resolveGatewayInstallToken } from "../../commands/gateway-install-token.js";
|
||||
import { readBestEffortConfig } from "../../config/io.js";
|
||||
import { readConfigFileSnapshotForWrite } from "../../config/io.js";
|
||||
import { resolveGatewayPort } from "../../config/paths.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { isNonFatalSystemdInstallProbeError } from "../../daemon/systemd.js";
|
||||
@@ -38,7 +38,9 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = await readBestEffortConfig();
|
||||
const { snapshot: configSnapshot, writeOptions: configWriteOptions } =
|
||||
await readConfigFileSnapshotForWrite();
|
||||
const cfg = configSnapshot.valid ? configSnapshot.sourceConfig : configSnapshot.config;
|
||||
const portOverride = parsePort(opts.port);
|
||||
if (opts.port !== undefined && portOverride === null) {
|
||||
fail("Invalid port");
|
||||
@@ -104,6 +106,8 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
|
||||
const tokenResolution = await resolveGatewayInstallToken({
|
||||
config: cfg,
|
||||
configSnapshot,
|
||||
configWriteOptions,
|
||||
env: installEnv,
|
||||
explicitToken: opts.token,
|
||||
autoGenerateWhenMissing: true,
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { ConfigWriteOptions } from "../config/io.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import type { ConfigFileSnapshot } from "../config/types.openclaw.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
|
||||
import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
|
||||
import { resolveGatewayAuthToken } from "../gateway/auth-token-resolution.js";
|
||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import {
|
||||
readConfigFileSnapshot,
|
||||
replaceConfigFile,
|
||||
} from "./gateway-install-token.persist.runtime.js";
|
||||
import { randomToken } from "./random-token.js";
|
||||
|
||||
type GatewayInstallTokenOptions = {
|
||||
config: OpenClawConfig;
|
||||
configSnapshot?: ConfigFileSnapshot;
|
||||
configWriteOptions?: ConfigWriteOptions;
|
||||
env: NodeJS.ProcessEnv;
|
||||
explicitToken?: string;
|
||||
autoGenerateWhenMissing?: boolean;
|
||||
@@ -23,24 +31,15 @@ export type GatewayInstallTokenResolution = {
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
let gatewayInstallTokenPersistRuntimePromise:
|
||||
| Promise<typeof import("./gateway-install-token.persist.runtime.js")>
|
||||
| undefined;
|
||||
|
||||
async function loadGatewayInstallTokenPersistRuntime() {
|
||||
gatewayInstallTokenPersistRuntimePromise ??= import("./gateway-install-token.persist.runtime.js");
|
||||
return gatewayInstallTokenPersistRuntimePromise;
|
||||
}
|
||||
|
||||
async function maybePersistAutoGeneratedGatewayInstallToken(params: {
|
||||
token: string;
|
||||
config: OpenClawConfig;
|
||||
configSnapshot?: ConfigFileSnapshot;
|
||||
configWriteOptions?: ConfigWriteOptions;
|
||||
warnings: string[];
|
||||
}): Promise<string | undefined> {
|
||||
try {
|
||||
const { readConfigFileSnapshot, replaceConfigFile } =
|
||||
await loadGatewayInstallTokenPersistRuntime();
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const snapshot = params.configSnapshot ?? (await readConfigFileSnapshot());
|
||||
if (snapshot.exists && !snapshot.valid) {
|
||||
params.warnings.push(
|
||||
"Warning: config file exists but is invalid; skipping token persistence.",
|
||||
@@ -71,6 +70,8 @@ async function maybePersistAutoGeneratedGatewayInstallToken(params: {
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshot: params.configSnapshot,
|
||||
writeOptions: params.configWriteOptions,
|
||||
});
|
||||
return params.token;
|
||||
}
|
||||
@@ -170,6 +171,8 @@ export async function resolveGatewayInstallToken(
|
||||
token = await maybePersistAutoGeneratedGatewayInstallToken({
|
||||
token,
|
||||
config: cfg,
|
||||
configSnapshot: options.configSnapshot,
|
||||
configWriteOptions: options.configWriteOptions,
|
||||
warnings,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,6 +149,11 @@ export type ConfigWriteOptions = {
|
||||
* even if schema/default normalization reintroduces them.
|
||||
*/
|
||||
unsetPaths?: string[][];
|
||||
/**
|
||||
* Internal fast path for callers that already hold a fresh config snapshot.
|
||||
* Avoids rereading the full config just to prepare an immediate write.
|
||||
*/
|
||||
baseSnapshot?: ConfigFileSnapshot;
|
||||
};
|
||||
|
||||
export type ReadConfigFileSnapshotForWriteResult = {
|
||||
@@ -1408,7 +1413,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
): Promise<{ persistedHash: string }> {
|
||||
clearConfigCache();
|
||||
let persistCandidate: unknown = cfg;
|
||||
const { snapshot } = await readConfigFileSnapshotInternal();
|
||||
const snapshot = options.baseSnapshot ?? (await readConfigFileSnapshotInternal()).snapshot;
|
||||
let envRefMap: Map<string, string> | null = null;
|
||||
let changedPaths: Set<string> | null = null;
|
||||
if (snapshot.valid && snapshot.exists) {
|
||||
|
||||
@@ -99,4 +99,27 @@ describe("config mutate helpers", () => {
|
||||
).rejects.toBeInstanceOf(ConfigMutationConflictError);
|
||||
expect(ioMocks.writeConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses a provided snapshot and write options for replace", async () => {
|
||||
const snapshot = createSnapshot({
|
||||
hash: "hash-1",
|
||||
sourceConfig: { gateway: { auth: { mode: "token" } } },
|
||||
});
|
||||
|
||||
await replaceConfigFile({
|
||||
baseHash: snapshot.hash,
|
||||
nextConfig: { gateway: { auth: { mode: "token", token: "minted" } } },
|
||||
snapshot,
|
||||
writeOptions: { expectedConfigPath: snapshot.path },
|
||||
});
|
||||
|
||||
expect(ioMocks.readConfigFileSnapshotForWrite).not.toHaveBeenCalled();
|
||||
expect(ioMocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
{ gateway: { auth: { mode: "token", token: "minted" } } },
|
||||
{
|
||||
baseSnapshot: snapshot,
|
||||
expectedConfigPath: snapshot.path,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,11 +38,17 @@ function assertBaseHashMatches(snapshot: ConfigFileSnapshot, expectedHash?: stri
|
||||
export async function replaceConfigFile(params: {
|
||||
nextConfig: OpenClawConfig;
|
||||
baseHash?: string;
|
||||
snapshot?: ConfigFileSnapshot;
|
||||
writeOptions?: ConfigWriteOptions;
|
||||
}): Promise<ConfigReplaceResult> {
|
||||
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
|
||||
const prepared =
|
||||
params.snapshot && params.writeOptions
|
||||
? { snapshot: params.snapshot, writeOptions: params.writeOptions }
|
||||
: await readConfigFileSnapshotForWrite();
|
||||
const { snapshot, writeOptions } = prepared;
|
||||
const previousHash = assertBaseHashMatches(snapshot, params.baseHash);
|
||||
await writeConfigFile(params.nextConfig, {
|
||||
baseSnapshot: snapshot,
|
||||
...writeOptions,
|
||||
...params.writeOptions,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user