mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix: refresh plugin runtime mirrors in place
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/startup: treat manifestless Claude bundles as valid installed-plugin registry entries instead of stale missing manifests, so workspace bundles no longer force repeated derived registry rebuilds or noisy `plugins.entries.workspace` warnings during Gateway startup. Fixes #73433. Thanks @AnneVoss.
|
||||
- Agents/subagents: preserve `sessions_yield` as a paused subagent state and ignore its wait text while freezing completion output, so parent sessions wait for the final post-compaction answer instead of receiving intermediate progress or `(no output)`. Fixes #73413. Thanks @Ask-sola.
|
||||
- Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock and keep Docker bundled plugin runtime deps/mirrors in a Docker-managed volume instead of the Windows/WSL config bind mount, so cold starts avoid slow host-volume mirror writes. Fixes #73339. Thanks @1yihui.
|
||||
- Plugins/runtime deps: refresh bundled runtime mirrors without deleting active import trees, so config-triggered restarts do not see transient missing plugin files during registration. Thanks @shakkernerd.
|
||||
- Channels/LINE: persist inbound image, video, audio, and file downloads in `~/.openclaw/media/inbound/` instead of temporary files so agents can still read LINE media after `/tmp` cleanup. Fixes #73370. Thanks @hijirii and @wenxu007.
|
||||
- CLI/plugins: keep bundled plugin installs out of `plugins.load.paths` while preserving install records, so install/inspect/doctor loops no longer warn about the current bundled plugin directory. Thanks @vincentkoc.
|
||||
- CLI/plugins: scope `plugins inspect <id>` runtime loading to the matched plugin so single-plugin inspection does not load every plugin before checking the target. Thanks @shakkernerd.
|
||||
|
||||
54
src/plugins/bundled-runtime-mirror.test.ts
Normal file
54
src/plugins/bundled-runtime-mirror.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { refreshBundledPluginRuntimeMirrorRoot } from "./bundled-runtime-mirror.js";
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function makeTempRoot(): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bundled-runtime-mirror-"));
|
||||
tempRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("refreshBundledPluginRuntimeMirrorRoot", () => {
|
||||
it("refreshes stale mirrors without deleting the active target root", () => {
|
||||
const root = makeTempRoot();
|
||||
const sourceRoot = path.join(root, "source");
|
||||
const targetRoot = path.join(root, "target");
|
||||
fs.mkdirSync(sourceRoot, { recursive: true });
|
||||
fs.mkdirSync(targetRoot, { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, "index.js"), "export const value = 'v1';\n", "utf8");
|
||||
|
||||
expect(
|
||||
refreshBundledPluginRuntimeMirrorRoot({
|
||||
pluginId: "demo",
|
||||
sourceRoot,
|
||||
targetRoot,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
fs.writeFileSync(path.join(sourceRoot, "index.js"), "export const value = 'v2';\n", "utf8");
|
||||
fs.writeFileSync(path.join(targetRoot, "inflight-import.js"), "still readable\n", "utf8");
|
||||
|
||||
expect(
|
||||
refreshBundledPluginRuntimeMirrorRoot({
|
||||
pluginId: "demo",
|
||||
sourceRoot,
|
||||
targetRoot,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(fs.readFileSync(path.join(targetRoot, "index.js"), "utf8")).toContain("v2");
|
||||
expect(fs.readFileSync(path.join(targetRoot, "inflight-import.js"), "utf8")).toBe(
|
||||
"still readable\n",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -31,22 +31,9 @@ export function refreshBundledPluginRuntimeMirrorRoot(params: {
|
||||
if (isBundledRuntimeMirrorRootFresh(params.targetRoot, metadata)) {
|
||||
return false;
|
||||
}
|
||||
const tempDir = fs.mkdtempSync(
|
||||
path.join(
|
||||
params.tempDirParent ?? path.dirname(params.targetRoot),
|
||||
`.plugin-${sanitizeBundledRuntimeMirrorTempId(params.pluginId)}-`,
|
||||
),
|
||||
);
|
||||
const stagedRoot = path.join(tempDir, "plugin");
|
||||
try {
|
||||
copyBundledPluginRuntimeRoot(params.sourceRoot, stagedRoot);
|
||||
writeBundledRuntimeMirrorMetadata(stagedRoot, metadata);
|
||||
fs.rmSync(params.targetRoot, { recursive: true, force: true });
|
||||
fs.renameSync(stagedRoot, params.targetRoot);
|
||||
return true;
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
copyBundledPluginRuntimeRoot(params.sourceRoot, params.targetRoot);
|
||||
writeBundledRuntimeMirrorMetadata(params.targetRoot, metadata);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: string): void {
|
||||
@@ -54,24 +41,29 @@ export function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: str
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 });
|
||||
const mirroredNames = new Set<string>();
|
||||
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
||||
if (shouldIgnoreBundledRuntimeMirrorEntry(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
if (!entry.isDirectory() && !entry.isSymbolicLink() && !entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
mirroredNames.add(entry.name);
|
||||
const sourcePath = path.join(sourceRoot, entry.name);
|
||||
const targetPath = path.join(targetRoot, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
removeBundledRuntimeMirrorPathIfTypeChanged(targetPath, "directory");
|
||||
copyBundledPluginRuntimeRoot(sourcePath, targetPath);
|
||||
continue;
|
||||
}
|
||||
if (entry.isSymbolicLink()) {
|
||||
fs.symlinkSync(fs.readlinkSync(sourcePath), targetPath);
|
||||
removeBundledRuntimeMirrorPathIfTypeChanged(targetPath, "symlink");
|
||||
replaceBundledRuntimeMirrorSymlinkAtomic(fs.readlinkSync(sourcePath), targetPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
removeBundledRuntimeMirrorPathIfTypeChanged(targetPath, "file");
|
||||
copyBundledRuntimeMirrorFileAtomic(sourcePath, targetPath);
|
||||
try {
|
||||
const sourceMode = fs.statSync(sourcePath).mode;
|
||||
fs.chmodSync(targetPath, sourceMode | 0o600);
|
||||
@@ -79,6 +71,69 @@ export function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: str
|
||||
// Readable copied files are enough for plugin loading.
|
||||
}
|
||||
}
|
||||
pruneStaleBundledRuntimeMirrorEntries(targetRoot, mirroredNames);
|
||||
}
|
||||
|
||||
function pruneStaleBundledRuntimeMirrorEntries(targetRoot: string, mirroredNames: Set<string>): void {
|
||||
for (const entry of fs.readdirSync(targetRoot, { withFileTypes: true })) {
|
||||
if (shouldIgnoreBundledRuntimeMirrorEntry(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
if (mirroredNames.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
fs.rmSync(path.join(targetRoot, entry.name), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function removeBundledRuntimeMirrorPathIfTypeChanged(
|
||||
targetPath: string,
|
||||
expectedType: "directory" | "file" | "symlink",
|
||||
): void {
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
stat = fs.lstatSync(targetPath);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const matches =
|
||||
expectedType === "directory"
|
||||
? stat.isDirectory()
|
||||
: expectedType === "symlink"
|
||||
? stat.isSymbolicLink()
|
||||
: stat.isFile();
|
||||
if (!matches) {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBundledRuntimeMirrorSymlinkAtomic(linkTarget: string, targetPath: string): void {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o755 });
|
||||
const tempPath = createBundledRuntimeMirrorTempPath(targetPath);
|
||||
try {
|
||||
fs.symlinkSync(linkTarget, tempPath);
|
||||
fs.renameSync(tempPath, targetPath);
|
||||
} finally {
|
||||
fs.rmSync(tempPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function copyBundledRuntimeMirrorFileAtomic(sourcePath: string, targetPath: string): void {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o755 });
|
||||
const tempPath = createBundledRuntimeMirrorTempPath(targetPath);
|
||||
try {
|
||||
fs.copyFileSync(sourcePath, tempPath);
|
||||
fs.renameSync(tempPath, targetPath);
|
||||
} finally {
|
||||
fs.rmSync(tempPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function createBundledRuntimeMirrorTempPath(targetPath: string): string {
|
||||
return path.join(
|
||||
path.dirname(targetPath),
|
||||
`.openclaw-mirror-${process.pid}-${process.hrtime.bigint()}-${path.basename(targetPath)}.tmp`,
|
||||
);
|
||||
}
|
||||
|
||||
export function precomputeBundledRuntimeMirrorMetadata(params: {
|
||||
@@ -235,7 +290,3 @@ function resolveBundledRuntimeMirrorSourceRootId(sourceRoot: string): string {
|
||||
function shouldIgnoreBundledRuntimeMirrorEntry(name: string): boolean {
|
||||
return name === "node_modules" || name === BUNDLED_RUNTIME_MIRROR_METADATA_FILE;
|
||||
}
|
||||
|
||||
function sanitizeBundledRuntimeMirrorTempId(pluginId: string): string {
|
||||
return pluginId.replaceAll(/[^a-zA-Z0-9._-]/g, "_");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user