mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 17:34:35 +00:00
fix: serialize sandbox registry writes
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { acquireSessionWriteLock } from "../session-write-lock.js";
|
||||
import {
|
||||
SANDBOX_BROWSER_REGISTRY_PATH,
|
||||
SANDBOX_REGISTRY_PATH,
|
||||
@@ -33,86 +36,144 @@ type SandboxBrowserRegistry = {
|
||||
entries: SandboxBrowserRegistryEntry[];
|
||||
};
|
||||
|
||||
export async function readRegistry(): Promise<SandboxRegistry> {
|
||||
type RegistryReadMode = "strict" | "fallback";
|
||||
|
||||
async function withRegistryLock<T>(registryPath: string, fn: () => Promise<T>): Promise<T> {
|
||||
const lock = await acquireSessionWriteLock({ sessionFile: registryPath });
|
||||
try {
|
||||
const raw = await fs.readFile(SANDBOX_REGISTRY_PATH, "utf-8");
|
||||
const parsed = JSON.parse(raw) as SandboxRegistry;
|
||||
if (parsed && Array.isArray(parsed.entries)) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
return await fn();
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
return { entries: [] };
|
||||
}
|
||||
|
||||
async function readRegistryFromFile<T>(
|
||||
registryPath: string,
|
||||
mode: RegistryReadMode,
|
||||
): Promise<{ entries: T[] }> {
|
||||
try {
|
||||
const raw = await fs.readFile(registryPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as { entries?: unknown };
|
||||
if (parsed && Array.isArray(parsed.entries)) {
|
||||
return { entries: parsed.entries as T[] };
|
||||
}
|
||||
if (mode === "fallback") {
|
||||
return { entries: [] };
|
||||
}
|
||||
throw new Error(`Invalid sandbox registry format: ${registryPath}`);
|
||||
} catch (error) {
|
||||
const code = (error as { code?: string } | null)?.code;
|
||||
if (code === "ENOENT") {
|
||||
return { entries: [] };
|
||||
}
|
||||
if (mode === "fallback") {
|
||||
return { entries: [] };
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to read sandbox registry file: ${registryPath}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
async function writeRegistryFile<T>(
|
||||
registryPath: string,
|
||||
registry: { entries: T[] },
|
||||
): Promise<void> {
|
||||
await fs.mkdir(SANDBOX_STATE_DIR, { recursive: true });
|
||||
const payload = `${JSON.stringify(registry, null, 2)}\n`;
|
||||
const registryDir = path.dirname(registryPath);
|
||||
const tempPath = path.join(
|
||||
registryDir,
|
||||
`${path.basename(registryPath)}.${crypto.randomUUID()}.tmp`,
|
||||
);
|
||||
await fs.writeFile(tempPath, payload, "utf-8");
|
||||
try {
|
||||
await fs.rename(tempPath, registryPath);
|
||||
} catch (error) {
|
||||
await fs.rm(tempPath, { force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readRegistry(): Promise<SandboxRegistry> {
|
||||
return await readRegistryFromFile<SandboxRegistryEntry>(SANDBOX_REGISTRY_PATH, "fallback");
|
||||
}
|
||||
|
||||
async function readRegistryForWrite(): Promise<SandboxRegistry> {
|
||||
return await readRegistryFromFile<SandboxRegistryEntry>(SANDBOX_REGISTRY_PATH, "strict");
|
||||
}
|
||||
|
||||
async function writeRegistry(registry: SandboxRegistry) {
|
||||
await fs.mkdir(SANDBOX_STATE_DIR, { recursive: true });
|
||||
await fs.writeFile(SANDBOX_REGISTRY_PATH, `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
|
||||
await writeRegistryFile<SandboxRegistryEntry>(SANDBOX_REGISTRY_PATH, registry);
|
||||
}
|
||||
|
||||
export async function updateRegistry(entry: SandboxRegistryEntry) {
|
||||
const registry = await readRegistry();
|
||||
const existing = registry.entries.find((item) => item.containerName === entry.containerName);
|
||||
const next = registry.entries.filter((item) => item.containerName !== entry.containerName);
|
||||
next.push({
|
||||
...entry,
|
||||
createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
|
||||
image: existing?.image ?? entry.image,
|
||||
configHash: entry.configHash ?? existing?.configHash,
|
||||
await withRegistryLock(SANDBOX_REGISTRY_PATH, async () => {
|
||||
const registry = await readRegistryForWrite();
|
||||
const existing = registry.entries.find((item) => item.containerName === entry.containerName);
|
||||
const next = registry.entries.filter((item) => item.containerName !== entry.containerName);
|
||||
next.push({
|
||||
...entry,
|
||||
createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
|
||||
image: existing?.image ?? entry.image,
|
||||
configHash: entry.configHash ?? existing?.configHash,
|
||||
});
|
||||
await writeRegistry({ entries: next });
|
||||
});
|
||||
await writeRegistry({ entries: next });
|
||||
}
|
||||
|
||||
export async function removeRegistryEntry(containerName: string) {
|
||||
const registry = await readRegistry();
|
||||
const next = registry.entries.filter((item) => item.containerName !== containerName);
|
||||
if (next.length === registry.entries.length) {
|
||||
return;
|
||||
}
|
||||
await writeRegistry({ entries: next });
|
||||
await withRegistryLock(SANDBOX_REGISTRY_PATH, async () => {
|
||||
const registry = await readRegistryForWrite();
|
||||
const next = registry.entries.filter((item) => item.containerName !== containerName);
|
||||
if (next.length === registry.entries.length) {
|
||||
return;
|
||||
}
|
||||
await writeRegistry({ entries: next });
|
||||
});
|
||||
}
|
||||
|
||||
export async function readBrowserRegistry(): Promise<SandboxBrowserRegistry> {
|
||||
try {
|
||||
const raw = await fs.readFile(SANDBOX_BROWSER_REGISTRY_PATH, "utf-8");
|
||||
const parsed = JSON.parse(raw) as SandboxBrowserRegistry;
|
||||
if (parsed && Array.isArray(parsed.entries)) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return { entries: [] };
|
||||
}
|
||||
|
||||
async function writeBrowserRegistry(registry: SandboxBrowserRegistry) {
|
||||
await fs.mkdir(SANDBOX_STATE_DIR, { recursive: true });
|
||||
await fs.writeFile(
|
||||
return await readRegistryFromFile<SandboxBrowserRegistryEntry>(
|
||||
SANDBOX_BROWSER_REGISTRY_PATH,
|
||||
`${JSON.stringify(registry, null, 2)}\n`,
|
||||
"utf-8",
|
||||
"fallback",
|
||||
);
|
||||
}
|
||||
|
||||
async function readBrowserRegistryForWrite(): Promise<SandboxBrowserRegistry> {
|
||||
return await readRegistryFromFile<SandboxBrowserRegistryEntry>(
|
||||
SANDBOX_BROWSER_REGISTRY_PATH,
|
||||
"strict",
|
||||
);
|
||||
}
|
||||
|
||||
async function writeBrowserRegistry(registry: SandboxBrowserRegistry) {
|
||||
await writeRegistryFile<SandboxBrowserRegistryEntry>(SANDBOX_BROWSER_REGISTRY_PATH, registry);
|
||||
}
|
||||
|
||||
export async function updateBrowserRegistry(entry: SandboxBrowserRegistryEntry) {
|
||||
const registry = await readBrowserRegistry();
|
||||
const existing = registry.entries.find((item) => item.containerName === entry.containerName);
|
||||
const next = registry.entries.filter((item) => item.containerName !== entry.containerName);
|
||||
next.push({
|
||||
...entry,
|
||||
createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
|
||||
image: existing?.image ?? entry.image,
|
||||
configHash: entry.configHash ?? existing?.configHash,
|
||||
await withRegistryLock(SANDBOX_BROWSER_REGISTRY_PATH, async () => {
|
||||
const registry = await readBrowserRegistryForWrite();
|
||||
const existing = registry.entries.find((item) => item.containerName === entry.containerName);
|
||||
const next = registry.entries.filter((item) => item.containerName !== entry.containerName);
|
||||
next.push({
|
||||
...entry,
|
||||
createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
|
||||
image: existing?.image ?? entry.image,
|
||||
configHash: entry.configHash ?? existing?.configHash,
|
||||
});
|
||||
await writeBrowserRegistry({ entries: next });
|
||||
});
|
||||
await writeBrowserRegistry({ entries: next });
|
||||
}
|
||||
|
||||
export async function removeBrowserRegistryEntry(containerName: string) {
|
||||
const registry = await readBrowserRegistry();
|
||||
const next = registry.entries.filter((item) => item.containerName !== containerName);
|
||||
if (next.length === registry.entries.length) {
|
||||
return;
|
||||
}
|
||||
await writeBrowserRegistry({ entries: next });
|
||||
await withRegistryLock(SANDBOX_BROWSER_REGISTRY_PATH, async () => {
|
||||
const registry = await readBrowserRegistryForWrite();
|
||||
const next = registry.entries.filter((item) => item.containerName !== containerName);
|
||||
if (next.length === registry.entries.length) {
|
||||
return;
|
||||
}
|
||||
await writeBrowserRegistry({ entries: next });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user