fix(plugins): serialize bundled runtime mirrors

This commit is contained in:
Peter Steinberger
2026-04-26 11:32:02 +01:00
parent 4506bb2e02
commit 0e490a3c26
4 changed files with 125 additions and 80 deletions

View File

@@ -1526,7 +1526,7 @@ if [ -z "\$dashboard_port" ] || [ "\$dashboard_port" = "\$dashboard_http_url" ];
echo "failed to parse dashboard port from \$dashboard_http_url" >&2
exit 1
fi
deadline=\$((SECONDS + 30))
deadline=\$((SECONDS + 120))
dashboard_ready=0
while [ \$SECONDS -lt \$deadline ]; do
if curl -fsSL --connect-timeout 2 --max-time 5 "\$dashboard_http_url" >/tmp/openclaw-dashboard-smoke.html 2>/dev/null; then

View File

@@ -341,9 +341,13 @@ function removeRuntimeDepsLockIfStale(lockDir: string, nowMs: number): boolean {
}
}
function withBundledRuntimeDepsInstallRootLock<T>(installRoot: string, run: () => T): T {
export function withBundledRuntimeDepsFilesystemLock<T>(
installRoot: string,
lockName: string,
run: () => T,
): T {
fs.mkdirSync(installRoot, { recursive: true });
const lockDir = path.join(installRoot, BUNDLED_RUNTIME_DEPS_LOCK_DIR);
const lockDir = path.join(installRoot, lockName);
const startedAt = Date.now();
let locked = false;
while (!locked) {
@@ -390,6 +394,10 @@ function withBundledRuntimeDepsInstallRootLock<T>(installRoot: string, run: () =
}
}
function withBundledRuntimeDepsInstallRootLock<T>(installRoot: string, run: () => T): T {
return withBundledRuntimeDepsFilesystemLock(installRoot, BUNDLED_RUNTIME_DEPS_LOCK_DIR, run);
}
function collectRuntimeDeps(packageJson: JsonObject): Record<string, unknown> {
return {
...(packageJson.dependencies as Record<string, unknown> | undefined),

View File

@@ -5,9 +5,11 @@ import {
resolveBundledRuntimeDependencyInstallRoot,
resolveBundledRuntimeDependencyPackageRoot,
registerBundledRuntimeDependencyNodePath,
withBundledRuntimeDepsFilesystemLock,
} from "./bundled-runtime-deps.js";
const bundledRuntimeDepsRetainSpecsByInstallRoot = new Map<string, readonly string[]>();
const BUNDLED_RUNTIME_MIRROR_LOCK_DIR = ".openclaw-runtime-mirror.lock";
export function isBuiltBundledPluginRuntimeRoot(pluginRoot: string): boolean {
const extensionsDir = path.dirname(pluginRoot);
@@ -83,34 +85,40 @@ function mirrorBundledPluginRuntimeRoot(params: {
pluginRoot: string;
installRoot: string;
}): string {
const mirrorParent = prepareBundledPluginRuntimeDistMirror({
installRoot: params.installRoot,
pluginRoot: params.pluginRoot,
});
const mirrorRoot = path.join(mirrorParent, params.pluginId);
fs.mkdirSync(params.installRoot, { recursive: true });
try {
fs.chmodSync(params.installRoot, 0o755);
} catch {
// Best-effort only: staged roots may live on filesystems that reject chmod.
}
fs.mkdirSync(mirrorParent, { recursive: true });
try {
fs.chmodSync(mirrorParent, 0o755);
} catch {
// Best-effort only: the access check below will surface non-writable dirs.
}
fs.accessSync(mirrorParent, fs.constants.W_OK);
const tempDir = fs.mkdtempSync(path.join(mirrorParent, `.plugin-${params.pluginId}-`));
const stagedRoot = path.join(tempDir, "plugin");
try {
copyBundledPluginRuntimeRoot(params.pluginRoot, stagedRoot);
fs.rmSync(mirrorRoot, { recursive: true, force: true });
fs.renameSync(stagedRoot, mirrorRoot);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
return mirrorRoot;
return withBundledRuntimeDepsFilesystemLock(
params.installRoot,
BUNDLED_RUNTIME_MIRROR_LOCK_DIR,
() => {
const mirrorParent = prepareBundledPluginRuntimeDistMirror({
installRoot: params.installRoot,
pluginRoot: params.pluginRoot,
});
const mirrorRoot = path.join(mirrorParent, params.pluginId);
fs.mkdirSync(params.installRoot, { recursive: true });
try {
fs.chmodSync(params.installRoot, 0o755);
} catch {
// Best-effort only: staged roots may live on filesystems that reject chmod.
}
fs.mkdirSync(mirrorParent, { recursive: true });
try {
fs.chmodSync(mirrorParent, 0o755);
} catch {
// Best-effort only: the access check below will surface non-writable dirs.
}
fs.accessSync(mirrorParent, fs.constants.W_OK);
const tempDir = fs.mkdtempSync(path.join(mirrorParent, `.plugin-${params.pluginId}-`));
const stagedRoot = path.join(tempDir, "plugin");
try {
copyBundledPluginRuntimeRoot(params.pluginRoot, stagedRoot);
fs.rmSync(mirrorRoot, { recursive: true, force: true });
fs.renameSync(stagedRoot, mirrorRoot);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
return mirrorRoot;
},
);
}
function prepareBundledPluginRuntimeDistMirror(params: {
@@ -135,6 +143,9 @@ function prepareBundledPluginRuntimeDistMirror(params: {
try {
fs.symlinkSync(sourcePath, targetPath, entry.isDirectory() ? "junction" : "file");
} catch {
if (fs.existsSync(targetPath)) {
continue;
}
if (entry.isDirectory()) {
copyBundledPluginRuntimeRoot(sourcePath, targetPath);
} else if (entry.isFile()) {
@@ -211,17 +222,21 @@ function writeRuntimeModuleWrapper(sourcePath: string, targetPath: string): void
` defaultExport = defaultExport.default;`,
`}`,
];
const content = [
`export * from ${JSON.stringify(normalizedSpecifier)};`,
...defaultForwarder,
"export { defaultExport as default };",
"",
].join("\n");
try {
if (fs.readFileSync(targetPath, "utf8") === content) {
return;
}
} catch {
// Missing or unreadable wrapper; rewrite below.
}
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(
targetPath,
[
`export * from ${JSON.stringify(normalizedSpecifier)};`,
...defaultForwarder,
"export { defaultExport as default };",
"",
].join("\n"),
"utf8",
);
fs.writeFileSync(targetPath, content, "utf8");
}
function ensureOpenClawPluginSdkAlias(distRoot: string): void {
@@ -240,7 +255,14 @@ function ensureOpenClawPluginSdkAlias(distRoot: string): void {
"./plugin-sdk/*": "./plugin-sdk/*.js",
},
});
fs.rmSync(pluginSdkAliasDir, { recursive: true, force: true });
try {
if (fs.existsSync(pluginSdkAliasDir) && !fs.lstatSync(pluginSdkAliasDir).isDirectory()) {
fs.rmSync(pluginSdkAliasDir, { recursive: true, force: true });
}
} catch {
// Another process may be creating the alias at the same time; mkdir/write
// below will either converge or surface the real filesystem error.
}
fs.mkdirSync(pluginSdkAliasDir, { recursive: true });
for (const entry of fs.readdirSync(pluginSdkDir, { withFileTypes: true })) {
if (!entry.isFile() || path.extname(entry.name) !== ".js") {

View File

@@ -38,6 +38,7 @@ import {
resolveBundledRuntimeDependencyInstallRoot,
resolveBundledRuntimeDependencyPackageRoot,
registerBundledRuntimeDependencyNodePath,
withBundledRuntimeDepsFilesystemLock,
type BundledRuntimeDepsInstallParams,
} from "./bundled-runtime-deps.js";
import {
@@ -269,6 +270,7 @@ export function clearPluginLoaderCache(): void {
}
const defaultLogger = () => createSubsystemLogger("plugins");
const BUNDLED_RUNTIME_MIRROR_LOCK_DIR = ".openclaw-runtime-mirror.lock";
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
return (
@@ -706,34 +708,40 @@ function mirrorBundledPluginRuntimeRoot(params: {
pluginRoot: string;
installRoot: string;
}): string {
const mirrorParent = prepareBundledPluginRuntimeDistMirror({
installRoot: params.installRoot,
pluginRoot: params.pluginRoot,
});
const mirrorRoot = path.join(mirrorParent, params.pluginId);
fs.mkdirSync(params.installRoot, { recursive: true });
try {
fs.chmodSync(params.installRoot, 0o755);
} catch {
// Best-effort only: staged roots may live on filesystems that reject chmod.
}
fs.mkdirSync(mirrorParent, { recursive: true });
try {
fs.chmodSync(mirrorParent, 0o755);
} catch {
// Best-effort only: the access check below will surface non-writable dirs.
}
fs.accessSync(mirrorParent, fs.constants.W_OK);
const tempDir = fs.mkdtempSync(path.join(mirrorParent, `.plugin-${params.pluginId}-`));
const stagedRoot = path.join(tempDir, "plugin");
try {
copyBundledPluginRuntimeRoot(params.pluginRoot, stagedRoot);
fs.rmSync(mirrorRoot, { recursive: true, force: true });
fs.renameSync(stagedRoot, mirrorRoot);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
return mirrorRoot;
return withBundledRuntimeDepsFilesystemLock(
params.installRoot,
BUNDLED_RUNTIME_MIRROR_LOCK_DIR,
() => {
const mirrorParent = prepareBundledPluginRuntimeDistMirror({
installRoot: params.installRoot,
pluginRoot: params.pluginRoot,
});
const mirrorRoot = path.join(mirrorParent, params.pluginId);
fs.mkdirSync(params.installRoot, { recursive: true });
try {
fs.chmodSync(params.installRoot, 0o755);
} catch {
// Best-effort only: staged roots may live on filesystems that reject chmod.
}
fs.mkdirSync(mirrorParent, { recursive: true });
try {
fs.chmodSync(mirrorParent, 0o755);
} catch {
// Best-effort only: the access check below will surface non-writable dirs.
}
fs.accessSync(mirrorParent, fs.constants.W_OK);
const tempDir = fs.mkdtempSync(path.join(mirrorParent, `.plugin-${params.pluginId}-`));
const stagedRoot = path.join(tempDir, "plugin");
try {
copyBundledPluginRuntimeRoot(params.pluginRoot, stagedRoot);
fs.rmSync(mirrorRoot, { recursive: true, force: true });
fs.renameSync(stagedRoot, mirrorRoot);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
return mirrorRoot;
},
);
}
function prepareBundledPluginRuntimeDistMirror(params: {
@@ -759,6 +767,9 @@ function prepareBundledPluginRuntimeDistMirror(params: {
try {
fs.symlinkSync(sourcePath, targetPath, entry.isDirectory() ? "junction" : "file");
} catch {
if (fs.existsSync(targetPath)) {
continue;
}
if (entry.isDirectory()) {
copyBundledPluginRuntimeRoot(sourcePath, targetPath);
} else if (entry.isFile()) {
@@ -853,17 +864,21 @@ function writeRuntimeModuleWrapper(sourcePath: string, targetPath: string): void
` defaultExport = defaultExport.default;`,
`}`,
];
const content = [
`export * from ${JSON.stringify(normalizedSpecifier)};`,
...defaultForwarder,
"export { defaultExport as default };",
"",
].join("\n");
try {
if (fs.readFileSync(targetPath, "utf8") === content) {
return;
}
} catch {
// Missing or unreadable wrapper; rewrite below.
}
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(
targetPath,
[
`export * from ${JSON.stringify(normalizedSpecifier)};`,
...defaultForwarder,
"export { defaultExport as default };",
"",
].join("\n"),
"utf8",
);
fs.writeFileSync(targetPath, content, "utf8");
}
function ensureOpenClawPluginSdkAlias(distRoot: string): void {