fix(gateway): derive lock and coordinator paths from the resolved state dir (#120110)

This commit is contained in:
Peter Steinberger
2026-08-06 19:16:13 -07:00
committed by GitHub
parent 2013123472
commit 70abd88e03
14 changed files with 193 additions and 108 deletions
+7
View File
@@ -24,6 +24,9 @@ Each layer can fail independently and throws its own `GatewayLockError`.
### State and config locks
- Lock files, SQLite coordinators, and transient reclaim guards live under
`$OPENCLAW_STATE_DIR/tmp/openclaw-<uid>` (or `openclaw` on platforms without
a user ID). An overridden state directory therefore owns its complete lock tree.
- Lock liveness comes from the recorded PID, platform process start identity when available, and Gateway process identity. A verified owner remains authoritative during startup before its port begins listening.
- A dedicated SQLite coordinator serializes metadata inspection, stale-owner reclamation, and lock replacement. Its exclusive transaction is released automatically if the owning process crashes.
- If a lock file is missing or the recorded owner process is gone, startup reclaims the lock and continues.
@@ -51,6 +54,10 @@ Each layer can fail independently and throws its own `GatewayLockError`.
On shutdown, the gateway closes the HTTP/WebSocket server and removes its state
and config lock files.
The state-local layout is a clean version boundary. Binaries from before this
change use the process temp directory, so an old and new binary sharing one state
directory during an upgrade do not exclude each other through these locks.
## Operational notes
- If the port is occupied by a different, non-gateway process, the error is the same; free the port or choose another with `openclaw gateway --port <port>`.
+7 -12
View File
@@ -275,18 +275,13 @@ fly machine update <machine-id> --vm-memory 2048 -y
Gateway refuses to start with "already running" errors after a container restart.
The runtime lock files live at `<tmpdir>/openclaw-<uid>/gateway.<hash>.lock`
and `gateway.state.<hash>.lock` (Linux:
`/tmp/openclaw-<uid>/gateway.*.lock`), not on the persistent `/data` volume, so
a full container restart normally clears them along with the rest of the
container filesystem. If a lock survives (for example a `fly machine restart`
that preserves the container filesystem) and blocks startup, remove it
manually:
```bash
fly ssh console --command "rm -f /tmp/openclaw-*/gateway.*.lock"
fly machine restart <machine-id>
```
With `OPENCLAW_STATE_DIR=/data`, the lock tree lives under
`/data/tmp/openclaw-<uid>` and persists with the volume. OpenClaw normally
reclaims stale owners automatically. If startup continues to report an owner,
first use `fly status` and `fly logs` to verify that no other machine or Gateway
process is using the volume. Do not delete the lock tree while an owner may
still be running; see [Gateway lock](/gateway/gateway-lock) for the ownership
and stale-recovery contract.
### Config not being read
+6 -5
View File
@@ -402,14 +402,15 @@ export function resolveDefaultConfigCandidates(
export const DEFAULT_GATEWAY_PORT = 18789;
/**
* Gateway lock directory (ephemeral).
* Default: os.tmpdir()/openclaw-<uid> (uid suffix when available).
* Gateway lock directory inside the selected state tree.
* Default: $OPENCLAW_STATE_DIR/tmp/openclaw-<uid> (uid suffix when available).
*/
export function resolveGatewayLockDir(tmpdir: () => string = os.tmpdir): string {
const base = tmpdir();
export function resolveGatewayLockDir(stateDir: string = resolveStateDir()): string {
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
const suffix = uid != null ? `openclaw-${uid}` : "openclaw";
return path.join(base, suffix);
// Clean break: older binaries still use process temp and do not exclude a
// state-local binary during a mixed-version upgrade.
return path.join(normalizePathForComparison(stateDir), "tmp", suffix);
}
/**
+5 -8
View File
@@ -8,7 +8,6 @@ import * as tar from "tar";
import { describe, expect, it, vi } from "vitest";
import { saveAuthProfileStore } from "../agents/auth-profiles/store.js";
import { backupVerifyCommand } from "../commands/backup-verify.js";
import { isPathWithin } from "../commands/cleanup-utils.js";
import { CONFIG_AUDIT_MAX_ENTRIES, CONFIG_AUDIT_SCOPE } from "../config/io.audit.js";
import { resolveGatewayLockDir } from "../config/paths.js";
import type { RuntimeEnv } from "../runtime.js";
@@ -2028,7 +2027,7 @@ describe("createBackupArchive", () => {
);
});
it("backs up durable SQLite while live gateway coordinators remain held under state", async () => {
it("excludes the state-local gateway lock tree while backing up durable SQLite", async () => {
await withOpenClawTestState(
{
layout: "state-only",
@@ -2038,7 +2037,7 @@ describe("createBackupArchive", () => {
async (state) => {
const outputDir = state.path("backups");
const extractDir = state.path("extract");
const lockDir = resolveGatewayLockDir(() => state.statePath("tmp"));
const lockDir = resolveGatewayLockDir(state.stateDir);
const pluginDbPath = state.statePath("plugins", "dedicated", "durable.sqlite");
const producerShapedDbPath = state.statePath(
"plugins",
@@ -2075,7 +2074,6 @@ describe("createBackupArchive", () => {
if (!gatewayLock) {
throw new Error("expected test gateway lock");
}
expect(isPathWithin(resolveGatewayLockDir(), state.stateDir)).toBe(false);
const gatewayCoordinatorPaths = [
`${gatewayLock.lockPath}.sqlite`,
`${gatewayLock.stateLockPath}.sqlite`,
@@ -2122,9 +2120,9 @@ describe("createBackupArchive", () => {
entry.endsWith("/state/plugins/dedicated/gateway.12345678.lock.sqlite"),
),
).toBe(true);
expect(
entries.some((entry) => entry.endsWith(`/${path.basename(lockDir)}/retained.sqlite`)),
).toBe(true);
expect(entries.some((entry) => entry.includes(`/${path.basename(lockDir)}/`))).toBe(
false,
);
const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
await expect(
@@ -2135,7 +2133,6 @@ describe("createBackupArchive", () => {
for (const [entrySuffix, value] of [
["/state/plugins/dedicated/durable.sqlite", "plugin-state"],
["/state/plugins/dedicated/gateway.12345678.lock.sqlite", "producer-shaped-state"],
[`/${path.basename(lockDir)}/retained.sqlite`, "colocated-state"],
] as const) {
const archivedEntry = expectDefined(
entries.find((entry) => entry.endsWith(entrySuffix)),
+13 -20
View File
@@ -446,7 +446,6 @@ function resolveSqliteBackupDatabasePath(sourcePath: string): string | undefined
function classifyStateSqliteBackupSourcePath(
sourcePath: string,
stateDir: string,
gatewayLockDirs: readonly string[],
): "excluded" | "sqlite" | undefined {
const resolvedSourcePath = path.resolve(sourcePath);
if (!isPathWithin(resolvedSourcePath, stateDir)) {
@@ -455,7 +454,7 @@ function classifyStateSqliteBackupSourcePath(
if (isStatePackageContentPath(resolvedSourcePath, stateDir)) {
return undefined;
}
if (isTransientSqliteBackupPath(resolvedSourcePath, gatewayLockDirs)) {
if (isTransientSqliteBackupPath(resolvedSourcePath)) {
return "excluded";
}
const databasePath = resolveSqliteBackupDatabasePath(resolvedSourcePath);
@@ -472,7 +471,7 @@ function isBackupTarFilterFile(entry: import("node:fs").Stats | import("tar").Re
async function listStateSqlitePaths(params: {
stateDir: string;
globalStateSqlitePath: string;
gatewayLockDirs: readonly string[];
gatewayLockDir: string;
preservedStatePaths?: readonly string[];
}): Promise<{ snapshotPaths: string[]; discoveredSourcePaths: Set<string> }> {
const snapshotPaths = new Set<string>();
@@ -512,7 +511,11 @@ async function listStateSqlitePaths(params: {
continue;
}
if (entry.isDirectory()) {
if (stateFilter(entryPath) && !isStatePackageContentPath(entryPath, params.stateDir)) {
if (
stateFilter(entryPath) &&
!isPathWithin(entryPath, params.gatewayLockDir) &&
!isStatePackageContentPath(entryPath, params.stateDir)
) {
await visit(entryPath);
}
} else if (
@@ -524,7 +527,6 @@ async function listStateSqlitePaths(params: {
const sqliteSourceKind = classifyStateSqliteBackupSourcePath(
resolvedEntryPath,
params.stateDir,
params.gatewayLockDirs,
);
if (sqliteSourceKind === "sqlite") {
discoveredSourcePaths.add(resolvedEntryPath);
@@ -596,12 +598,7 @@ async function createStateSqliteBackupPlan(params: {
const discovery = await listStateSqlitePaths({
stateDir: params.stateDir,
globalStateSqlitePath,
// CLI and managed services use different temp roots for the same
// disposable gateway/device coordination databases.
gatewayLockDirs: [
resolveGatewayLockDir(),
resolveGatewayLockDir(() => path.join(params.stateDir, "tmp")),
],
gatewayLockDir: resolveGatewayLockDir(params.stateDir),
preservedStatePaths: params.preservedStatePaths,
});
const globalStateIdentity = await fs.stat(globalStateSqlitePath).catch((error: unknown) => {
@@ -866,10 +863,7 @@ export async function createBackupArchive(
const stateFilter = stateAsset
? buildStateBackupFilter(stateAsset.sourcePath, preservedStatePaths)
: undefined;
const gatewayLockDirs = [
resolveGatewayLockDir(),
resolveGatewayLockDir(() => path.join(plan.stateDir, "tmp")),
];
const gatewayLockDir = resolveGatewayLockDir(plan.stateDir);
const volatilePlan = { stateDirs: [stateAsset?.sourcePath ?? plan.stateDir] };
let skippedVolatileCount = 0;
// node-tar invokes filters from async stat callbacks, so throwing inside
@@ -888,6 +882,9 @@ export async function createBackupArchive(
if (stateFilter && !stateFilter(entryPath)) {
return false;
}
if (isPathWithin(resolvedEntryPath, gatewayLockDir)) {
return false;
}
if (
stateAsset &&
isLegacyAuditMigrationBackupPath(resolvedEntryPath, stateAsset.sourcePath)
@@ -895,11 +892,7 @@ export async function createBackupArchive(
return false;
}
const sqliteSourceKind = stateAsset
? classifyStateSqliteBackupSourcePath(
resolvedEntryPath,
stateAsset.sourcePath,
gatewayLockDirs,
)
? classifyStateSqliteBackupSourcePath(resolvedEntryPath, stateAsset.sourcePath)
: undefined;
if (sqliteSourceKind === "excluded") {
return false;
+4 -16
View File
@@ -128,14 +128,6 @@ describe("isVolatileBackupPath", () => {
});
describe("isTransientSqliteBackupPath", () => {
it.each([
"tmp/openclaw-502/gateway.12345678.lock.sqlite",
"tmp/openclaw-502/gateway.12345678.lock.sqlite-wal",
"tmp/openclaw-502/device-identity.12345678.lock.sqlite-journal",
])("classifies transient coordinator state: %s", (filePath) => {
expect(isTransientSqliteBackupPath(filePath, ["tmp/openclaw-502"])).toBe(true);
});
it.each([
"memory/main.sqlite.reindex-lock.sqlite",
"memory/main.sqlite.reindex-lock.sqlite-shm",
@@ -145,19 +137,15 @@ describe("isTransientSqliteBackupPath", () => {
});
it.each([
"tmp/openclaw-502/gateway.state.lock.sqlite",
"tmp/openclaw-502/gateway.12345678.lock.sqlite-wal",
"tmp/openclaw-502/device-identity.12345678.lock.sqlite-journal",
"tmp/openclaw-502/retained.sqlite",
"plugins/dedicated/durable.sqlite",
"plugins/dedicated/cache.lock.sqlite",
"plugins/dedicated/durable.locked.sqlite",
"plugins/dedicated/lock.sqlite",
])("preserves durable SQLite state: %s", (filePath) => {
expect(isTransientSqliteBackupPath(filePath, ["tmp/openclaw-502"])).toBe(false);
});
it.each([
"plugins/dedicated/gateway.12345678.lock.sqlite",
"plugins/dedicated/device-identity.12345678.lock.sqlite",
])("preserves coordinator-shaped databases outside the lock directory: %s", (filePath) => {
expect(isTransientSqliteBackupPath(filePath, ["tmp/openclaw-502"])).toBe(false);
expect(isTransientSqliteBackupPath(filePath)).toBe(false);
});
});
+2 -14
View File
@@ -1,6 +1,5 @@
// Filters volatile files from backup manifests.
import path from "node:path";
import { isPathInside } from "./path-guards.js";
/**
* Paths that are known to change during a live backup and commonly trigger
@@ -14,8 +13,6 @@ import { isPathInside } from "./path-guards.js";
*/
const STATE_TRANSIENT_EXTENSIONS = new Set([".sock", ".pid", ".tmp"]);
const SQLITE_COORDINATOR_BASENAME_PATTERN =
/^(?:gateway(?:\.state)?|device-identity)\.[0-9a-f]{8}\.lock\.sqlite(?:-wal|-shm|-journal)?$/iu;
const SQLITE_REINDEX_TRANSIENT_PATH_PATTERN =
/(?:^|\/)(?:[^/]+\.sqlite\.reindex-lock\.sqlite|[^/]+\.sqlite\.(?:backup|memory-reindex|tmp)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:-wal|-shm|-journal)?$/iu;
@@ -45,18 +42,9 @@ function hasExtensionInSet(filePosix: string, extensions: ReadonlySet<string>):
return extensions.has(path.posix.extname(filePosix).toLowerCase());
}
export function isTransientSqliteBackupPath(
filePath: string,
coordinatorDirs: readonly string[] = [],
): boolean {
export function isTransientSqliteBackupPath(filePath: string): boolean {
const normalizedPath = normalizePosix(filePath);
if (SQLITE_REINDEX_TRANSIENT_PATH_PATTERN.test(normalizedPath)) {
return true;
}
if (!SQLITE_COORDINATOR_BASENAME_PATTERN.test(path.posix.basename(normalizedPath))) {
return false;
}
return coordinatorDirs.some((coordinatorDir) => isPathInside(coordinatorDir, filePath));
return SQLITE_REINDEX_TRANSIENT_PATH_PATTERN.test(normalizedPath);
}
function isAgentSessionTranscriptPath(filePosix: string, stateDirPosix: string): boolean {
+7 -7
View File
@@ -1,7 +1,7 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { resolveGatewayLockDir } from "../config/paths.js";
import { resolveGatewayLockDir, resolveStateDir } from "../config/paths.js";
import { openNodeSqliteDatabase } from "./node-sqlite.js";
const DEFAULT_BUSY_TIMEOUT_MS = 5000;
@@ -39,10 +39,7 @@ function canonicalizeDatabasePath(databasePath: string): string {
}
}
function resolveDeviceIdentityCoordinatorPath(
databasePath: string,
lockDir = resolveGatewayLockDir(),
): string {
function resolveDeviceIdentityCoordinatorPath(databasePath: string, lockDir: string): string {
const canonicalPath = canonicalizeDatabasePath(databasePath);
const databaseHash = crypto.createHash("sha256").update(canonicalPath).digest("hex").slice(0, 8);
return path.join(lockDir, `device-identity.${databaseHash}.lock.sqlite`);
@@ -57,7 +54,7 @@ function ensurePrivateCoordinatorDirectory(lockDir: string): void {
throw error;
}
try {
fs.mkdirSync(lockDir, { mode: 0o700 });
fs.mkdirSync(lockDir, { mode: 0o700, recursive: true });
} catch (mkdirError) {
if ((mkdirError as NodeJS.ErrnoException).code !== "EEXIST") {
throw mkdirError;
@@ -90,9 +87,12 @@ function ensurePrivateCoordinatorDirectory(lockDir: string): void {
export function acquireDeviceIdentityCoordinator(params: {
databasePath: string;
busyTimeoutMs?: number;
env?: NodeJS.ProcessEnv;
lockDir?: string;
}): { release: () => void } {
const coordinatorPath = resolveDeviceIdentityCoordinatorPath(params.databasePath, params.lockDir);
const lockDir =
params.lockDir ?? resolveGatewayLockDir(resolveStateDir(params.env ?? process.env));
const coordinatorPath = resolveDeviceIdentityCoordinatorPath(params.databasePath, lockDir);
ensurePrivateCoordinatorDirectory(path.dirname(coordinatorPath));
const database = openNodeSqliteDatabase(coordinatorPath);
try {
+35 -1
View File
@@ -1,13 +1,17 @@
// Covers default device identity SQLite path under the state dir.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveGatewayLockDir } from "../config/paths.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { withStateDirEnv } from "../test-helpers/state-dir-env.js";
import { withTempDir } from "../test-utils/temp-dir.js";
import { loadDeviceIdentityIfPresent, loadOrCreateDeviceIdentity } from "./device-identity.js";
afterEach(() => {
closeOpenClawStateDatabaseForTest();
vi.restoreAllMocks();
});
describe("device identity state dir defaults", () => {
@@ -15,9 +19,13 @@ describe("device identity state dir defaults", () => {
await withStateDirEnv("openclaw-identity-state-", async ({ stateDir }) => {
const identity = loadOrCreateDeviceIdentity();
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
const lockDir = resolveGatewayLockDir(stateDir);
expect(loadDeviceIdentityIfPresent()).toEqual(identity);
expect(fs.existsSync(databasePath)).toBe(true);
expect(fs.readdirSync(lockDir)).toContainEqual(
expect.stringMatching(/^device-identity\.[0-9a-f]{8}\.lock\.sqlite$/u),
);
expect(fs.existsSync(path.join(stateDir, "identity", "device.json"))).toBe(false);
});
});
@@ -31,6 +39,32 @@ describe("device identity state dir defaults", () => {
});
});
it("uses the supplied state environment for its coordinator", async () => {
await withTempDir("openclaw-identity-env-state-", async (rootDir) => {
const stateDir = path.join(rootDir, "selected-state");
const fakeHome = path.join(rootDir, "home");
const legacyTmpDir = path.join(rootDir, "legacy-process-tmp");
fs.mkdirSync(stateDir, { recursive: true });
fs.mkdirSync(fakeHome, { recursive: true });
fs.mkdirSync(legacyTmpDir, { recursive: true });
vi.spyOn(os, "tmpdir").mockReturnValue(legacyTmpDir);
const env = {
...process.env,
HOME: fakeHome,
OPENCLAW_HOME: fakeHome,
OPENCLAW_STATE_DIR: stateDir,
};
loadOrCreateDeviceIdentity({ env });
expect(fs.readdirSync(resolveGatewayLockDir(stateDir))).toContainEqual(
expect.stringMatching(/^device-identity\.[0-9a-f]{8}\.lock\.sqlite$/u),
);
expect(fs.readdirSync(legacyTmpDir)).toEqual([]);
expect(fs.existsSync(path.join(fakeHome, ".openclaw"))).toBe(false);
});
});
it("keeps read-only lookup non-creating when the default database is absent", async () => {
await withStateDirEnv("openclaw-identity-state-", async ({ stateDir }) => {
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
+4 -1
View File
@@ -100,7 +100,10 @@ function withDeviceIdentityCoordinator<T>(
path: resolved.databasePath,
identityKey: resolved.identityKey,
};
const coordinator = acquireDeviceIdentityCoordinator({ databasePath: resolved.databasePath });
const coordinator = acquireDeviceIdentityCoordinator({
databasePath: resolved.databasePath,
env: options.env,
});
let result: T;
try {
result = operation(resolved, resolvedOptions);
+81
View File
@@ -0,0 +1,81 @@
// Covers the production Gateway lock layout under an overridden state directory.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveGatewayLockDir } from "../config/paths.js";
import { withTempDir } from "../test-utils/temp-dir.js";
import { acquireGatewayLock, GatewayLockError } from "./gateway-lock.js";
type GatewayLock = NonNullable<Awaited<ReturnType<typeof acquireGatewayLock>>>;
function expectGatewayLock(lock: Awaited<ReturnType<typeof acquireGatewayLock>>): GatewayLock {
if (!lock) {
throw new Error("Expected gateway lock");
}
return lock;
}
afterEach(() => {
vi.restoreAllMocks();
});
describe("gateway lock state directory", () => {
it("keeps lock, coordinator, and reclaim paths inside the selected state", async () => {
await withTempDir("openclaw-gateway-lock-state-", async (root) => {
const canonicalRoot = await fs.realpath(root);
const stateDir = path.join(canonicalRoot, "selected-state");
const fakeHome = path.join(canonicalRoot, "home");
const legacyTmpDir = path.join(canonicalRoot, "legacy-process-tmp");
await fs.mkdir(stateDir, { recursive: true });
await fs.mkdir(fakeHome, { recursive: true });
await fs.mkdir(legacyTmpDir, { recursive: true });
const configPath = path.join(stateDir, "openclaw.json");
await fs.writeFile(configPath, "{}", "utf8");
vi.spyOn(os, "tmpdir").mockReturnValue(legacyTmpDir);
const env = {
...process.env,
HOME: fakeHome,
OPENCLAW_HOME: fakeHome,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
};
const lock = expectGatewayLock(
await acquireGatewayLock({ allowInTests: true, env, timeoutMs: 30 }),
);
const lockDir = resolveGatewayLockDir(stateDir);
const stateLockPath = path.join(lockDir, "gateway.state.lock");
try {
expect(lock.stateLockPath).toBe(stateLockPath);
expect(path.dirname(lock.lockPath)).toBe(lockDir);
expect(path.basename(lock.lockPath)).toMatch(/^gateway\.[0-9a-f]{8}\.lock$/u);
await expect(fs.access(`${lock.lockPath}.sqlite`)).resolves.toBeUndefined();
await expect(fs.access(`${lock.stateLockPath}.sqlite`)).resolves.toBeUndefined();
} finally {
await lock.release();
}
const reclaimPath = `${stateLockPath}.reclaim`;
await fs.mkdir(reclaimPath);
try {
await expect(
acquireGatewayLock({
allowInTests: true,
env,
pollIntervalMs: 2,
timeoutMs: 10,
}),
).rejects.toBeInstanceOf(GatewayLockError);
expect(reclaimPath.startsWith(`${stateDir}${path.sep}`)).toBe(true);
} finally {
await fs.rmdir(reclaimPath);
}
await expect(fs.readdir(legacyTmpDir)).resolves.toEqual([]);
await expect(fs.access(path.join(fakeHome, ".openclaw"))).rejects.toMatchObject({
code: "ENOENT",
});
});
});
});
+18 -21
View File
@@ -21,11 +21,10 @@ type GatewayLock = NonNullable<Awaited<ReturnType<typeof acquireGatewayLock>>>;
type GatewayLockOptions = NonNullable<Parameters<typeof acquireGatewayLock>[0]>;
const fixtureRootTracker = createSuiteTempRootTracker({ prefix: "openclaw-gateway-lock-" });
let fixtureRoot = "";
const realNow = Date.now.bind(Date);
function resolveTestLockDir() {
return path.join(fixtureRoot, "__locks");
function resolveTestLockDir(env: NodeJS.ProcessEnv) {
return path.join(resolveStateDir(env), "__locks");
}
async function makeEnv() {
@@ -52,7 +51,7 @@ async function acquireForTest(
sleep: async (ms) => {
await nativeSleep(ms);
},
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
...opts,
});
}
@@ -69,13 +68,12 @@ function resolveLockPath(env: NodeJS.ProcessEnv) {
const stateDir = resolveStateDir(env);
const configPath = resolveConfigPath(env, stateDir);
const configHash = createHash("sha256").update(configPath).digest("hex").slice(0, 8);
const canonicalStateDir = fsSync.realpathSync.native(path.resolve(stateDir));
const stateHash = createHash("sha256").update(canonicalStateDir).digest("hex").slice(0, 8);
const lockDir = resolveTestLockDir();
const lockDir = resolveTestLockDir(env);
fsSync.mkdirSync(lockDir, { recursive: true });
return {
lockPath: path.join(lockDir, `gateway.${configHash}.lock`),
configPath,
stateLockPath: path.join(lockDir, `gateway.state.${stateHash}.lock`),
stateLockPath: path.join(lockDir, "gateway.state.lock"),
};
}
@@ -152,7 +150,7 @@ async function writeRecentLockFile(env: NodeJS.ProcessEnv, startTime = 111) {
describe("gateway lock", () => {
beforeAll(async () => {
fixtureRoot = await fixtureRootTracker.setup();
await fixtureRootTracker.setup();
});
beforeEach(() => {
@@ -165,7 +163,6 @@ describe("gateway lock", () => {
afterAll(async () => {
await fixtureRootTracker.cleanup();
fixtureRoot = "";
});
afterEach(() => {
@@ -278,7 +275,7 @@ describe("gateway lock", () => {
await expect(
readActiveGatewayLockPort({
env,
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
platform: "darwin",
readProcessCmdline: () => ["openclaw-gateway"],
}),
@@ -304,7 +301,7 @@ describe("gateway lock", () => {
};
const firstIdentity = await readActiveGatewayLockIdentity({
env,
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
platform: "darwin",
readProcessCmdline: options.readProcessCmdline,
});
@@ -315,7 +312,7 @@ describe("gateway lock", () => {
try {
const secondIdentity = await readActiveGatewayLockIdentity({
env,
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
platform: "darwin",
readProcessCmdline: options.readProcessCmdline,
});
@@ -355,7 +352,7 @@ describe("gateway lock", () => {
await expect(
readActiveGatewayLockPort({
env,
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
platform: "darwin",
readProcessCmdline: () => ["openclaw-gateway"],
}),
@@ -384,7 +381,7 @@ describe("gateway lock", () => {
await expect(
readActiveGatewayLockPort({
env: envB,
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(envB),
platform: "darwin",
readProcessCmdline: () => ["openclaw-gateway"],
}),
@@ -475,7 +472,7 @@ describe("gateway lock", () => {
await expect(
readActiveGatewayLockPort({
env,
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
platform: "darwin",
readProcessCmdline: () => null,
}),
@@ -872,7 +869,7 @@ describe("gateway lock", () => {
sleepDelays.push(ms);
now = 10;
},
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
readProcessCmdline: () => ["/usr/local/bin/openclaw", "gateway", "run"],
readProcessStartTime: () => 111,
}),
@@ -888,7 +885,7 @@ describe("gateway lock", () => {
await acquireGatewayLock({
allowInTests: true,
env: { ...env, OPENCLAW_ALLOW_MULTI_GATEWAY: "1", VITEST: "" },
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
}),
);
@@ -900,7 +897,7 @@ describe("gateway lock", () => {
acquireGatewayLock({
allowInTests: true,
env,
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
platform: "darwin",
readProcessCmdline: () => ["openclaw-gateway"],
timeoutMs: 15,
@@ -915,7 +912,7 @@ describe("gateway lock", () => {
const env = await makeEnv();
const lock = await acquireGatewayLock({
env: { ...env, VITEST: "1" },
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
});
expect(lock).toBeNull();
});
@@ -931,7 +928,7 @@ describe("gateway lock", () => {
pollIntervalMs: 2,
now: () => 8_640_000_000_000_001,
sleep: async () => {},
lockDir: resolveTestLockDir(),
lockDir: resolveTestLockDir(env),
}),
);
+3 -3
View File
@@ -293,17 +293,17 @@ function canonicalizeStateDir(stateDir: string): string {
}
}
function resolveGatewayLockPaths(env: NodeJS.ProcessEnv, lockDir = resolveGatewayLockDir()) {
function resolveGatewayLockPaths(env: NodeJS.ProcessEnv, suppliedLockDir?: string) {
const resolvedStateDir = resolveStateDir(env);
const stateDir = canonicalizeStateDir(resolvedStateDir);
const lockDir = suppliedLockDir ?? resolveGatewayLockDir(stateDir);
const configPath = resolveConfigPath(env, resolvedStateDir);
const configHash = sha256HexPrefix(configPath, 8);
const stateHash = sha256HexPrefix(stateDir, 8);
return {
configLockPath: path.join(lockDir, `gateway.${configHash}.lock`),
configPath,
stateDir,
stateLockPath: path.join(lockDir, `gateway.state.${stateHash}.lock`),
stateLockPath: path.join(lockDir, "gateway.state.lock"),
};
}
@@ -535,6 +535,7 @@ export async function migrateLegacyDeviceIdentity(params: {
try {
identityCoordinator = acquireDeviceIdentityCoordinator({
databasePath: resolveDeviceIdentityStore({ env, identityKey: IDENTITY_KEY }).databasePath,
env,
});
} catch (error) {
return {