From 0e490a3c26f25254c44a593f398c481eb8189169 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 11:32:02 +0100 Subject: [PATCH] fix(plugins): serialize bundled runtime mirrors --- scripts/e2e/parallels-macos-smoke.sh | 2 +- src/plugins/bundled-runtime-deps.ts | 12 +++- src/plugins/bundled-runtime-root.ts | 100 ++++++++++++++++----------- src/plugins/loader.ts | 91 ++++++++++++++---------- 4 files changed, 125 insertions(+), 80 deletions(-) diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index 6d3db3c33a0..ca34d0945e0 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -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 diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index a1384c428b8..2e6c69adcd0 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -341,9 +341,13 @@ function removeRuntimeDepsLockIfStale(lockDir: string, nowMs: number): boolean { } } -function withBundledRuntimeDepsInstallRootLock(installRoot: string, run: () => T): T { +export function withBundledRuntimeDepsFilesystemLock( + 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(installRoot: string, run: () = } } +function withBundledRuntimeDepsInstallRootLock(installRoot: string, run: () => T): T { + return withBundledRuntimeDepsFilesystemLock(installRoot, BUNDLED_RUNTIME_DEPS_LOCK_DIR, run); +} + function collectRuntimeDeps(packageJson: JsonObject): Record { return { ...(packageJson.dependencies as Record | undefined), diff --git a/src/plugins/bundled-runtime-root.ts b/src/plugins/bundled-runtime-root.ts index 60918cf0521..a042744d33e 100644 --- a/src/plugins/bundled-runtime-root.ts +++ b/src/plugins/bundled-runtime-root.ts @@ -5,9 +5,11 @@ import { resolveBundledRuntimeDependencyInstallRoot, resolveBundledRuntimeDependencyPackageRoot, registerBundledRuntimeDependencyNodePath, + withBundledRuntimeDepsFilesystemLock, } from "./bundled-runtime-deps.js"; const bundledRuntimeDepsRetainSpecsByInstallRoot = new Map(); +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") { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 31f11343d09..9309467947f 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -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 { 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 {