openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
115 lines (114 loc) • 5.72 kB
JavaScript
import { a as resolveRootPathSync, n as resolvePathViaExistingAncestorSync } from "./boundary-path-DMNeww4q.js";
import { r as isPathInside } from "./path-guards-Cp-mGr3-.js";
import { w as resolveStateDir } from "./paths-D2sRr1a_.js";
import { n as acquireGatewayLock, t as GatewayLockError } from "./gateway-lock-B0QIxQaj.js";
import fs from "node:fs";
import path from "node:path";
//#region src/commands/doctor-sqlite-maintenance-lock.ts
/** Serializes offline SQLite maintenance against the Gateway state owner. */
const MAINTENANCE_LOCK_TIMEOUT_MS = 250;
const MAINTENANCE_LOCK_POLL_INTERVAL_MS = 25;
var DoctorSqliteMaintenanceLockUnavailableError = class extends Error {
constructor(operation, cause) {
super(`Cannot run ${operation} while the Gateway or another SQLite maintenance command owns this OpenClaw state directory. Stop the Gateway and retry.`);
this.cause = cause;
this.name = "DoctorSqliteMaintenanceLockUnavailableError";
}
};
async function assertMaintenancePathsOwnedByStateDir(env, operation, protectedPaths, reconcileHardlink) {
if (protectedPaths.length === 0) return;
const stateDir = path.resolve(resolveStateDir(env));
const stateCanonicalDir = resolvePathViaExistingAncestorSync(stateDir);
for (const protectedPath of protectedPaths) {
const absolutePath = path.resolve(protectedPath);
try {
if (!isPathInside(stateDir, absolutePath) && !isPathInside(stateCanonicalDir, absolutePath)) throw new Error("path is not lexically owned by the active state directory");
resolveRootPathSync({
absolutePath,
boundaryLabel: "OpenClaw state directory",
rootCanonicalPath: stateCanonicalDir,
rootPath: stateDir
});
} catch (error) {
throw new Error(`Cannot run ${operation} for a path outside the active OpenClaw state directory: ${protectedPath}. Set OPENCLAW_STATE_DIR to the owning state directory and retry.`, { cause: error });
}
}
if (reconcileHardlink) for (const protectedPath of new Set(protectedPaths.map((candidate) => path.resolve(candidate)))) {
const stat = inspectMaintenancePath(operation, protectedPath, [stateDir]);
if (stat?.isFile() && stat.nlink > 1) await reconcileHardlink(protectedPath);
}
assertDoctorSqliteMaintenancePathsNotAliased(operation, protectedPaths, [stateDir]);
}
/** Reject file aliases that destructive SQLite maintenance would mutate in place. */
function assertDoctorSqliteMaintenancePathsNotAliased(operation, protectedPaths, ownershipRoots = []) {
const resolvedRoots = ownershipRoots.map((candidate) => path.resolve(candidate));
for (const protectedPath of new Set(protectedPaths.map((candidate) => path.resolve(candidate)))) {
const stat = inspectMaintenancePath(operation, protectedPath, resolvedRoots);
if (stat?.isFile() && stat.nlink > 1) throw new Error(`Cannot run ${operation} for a hard-linked path: ${protectedPath}. Remove the additional hard link and retry.`);
}
}
function inspectMaintenancePath(operation, protectedPath, ownershipRoots) {
assertPathComponentsNotSymbolicLinks(operation, protectedPath, ownershipRoots);
let stat;
try {
stat = fs.lstatSync(protectedPath);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
if (stat.isSymbolicLink()) throw new Error(`Cannot run ${operation} for a symbolic-link path: ${protectedPath}. Replace the symbolic link with an owned regular file and retry.`);
return stat;
}
function assertPathComponentsNotSymbolicLinks(operation, protectedPath, ownershipRoots) {
const rootPath = ownershipRoots.find((candidate) => isPathInside(candidate, protectedPath));
if (!rootPath) return;
const relativePath = path.relative(rootPath, protectedPath);
let currentPath = rootPath;
for (const segment of relativePath.split(path.sep).filter(Boolean)) {
currentPath = path.join(currentPath, segment);
let stat;
try {
stat = fs.lstatSync(currentPath);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
if (stat.isSymbolicLink()) throw new Error(`Cannot run ${operation} through a symbolic-link path component: ${currentPath}. Replace the symbolic link with an owned directory or regular file and retry.`);
}
}
function isDestructiveDoctorSessionSqliteMode(mode) {
return mode === "import" || mode === "compact" || mode === "restore" || mode === "recover";
}
/** Run one destructive doctor operation while excluding Gateway startup and peer maintenance. */
async function withDoctorSqliteMaintenanceLock(params, deps = {}) {
const env = params.env ?? process.env;
const acquireLock = deps.acquireLock ?? acquireGatewayLock;
const lockOptions = deps.lockOptions;
let lock;
try {
lock = await acquireLock({
...lockOptions,
allowInTests: true,
env,
pollIntervalMs: lockOptions?.pollIntervalMs ?? MAINTENANCE_LOCK_POLL_INTERVAL_MS,
role: "sqlite-maintenance",
timeoutMs: lockOptions?.timeoutMs ?? MAINTENANCE_LOCK_TIMEOUT_MS
});
} catch (error) {
if (error instanceof GatewayLockError) throw new DoctorSqliteMaintenanceLockUnavailableError(params.operation, error);
throw error;
}
if (!lock) throw new Error(`Cannot run ${params.operation} without exclusive OpenClaw state ownership.`);
let active = true;
try {
await assertMaintenancePathsOwnedByStateDir(env, params.operation, params.protectedPaths ?? [], params.reconcileHardlink);
return await params.run({ assertCurrent() {
if (!active) throw new Error("Doctor SQLite maintenance authority has expired.");
} });
} finally {
active = false;
await lock.release();
}
}
//#endregion
export { withDoctorSqliteMaintenanceLock as i, assertDoctorSqliteMaintenancePathsNotAliased as n, isDestructiveDoctorSessionSqliteMode as r, DoctorSqliteMaintenanceLockUnavailableError as t };