mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix: stabilize gateway watch runtime deps
This commit is contained in:
@@ -11,6 +11,9 @@
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
"bundle": {
|
||||
"stageRuntimeDependencies": true
|
||||
},
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
|
||||
@@ -6,6 +6,9 @@ import { pathToFileURL } from "node:url";
|
||||
import semverSatisfies from "semver/functions/satisfies.js";
|
||||
import { resolveNpmRunner } from "./npm-runner.mjs";
|
||||
|
||||
const TRANSIENT_TEMP_REMOVE_ERROR_CODES = new Set(["EBUSY", "ENOTEMPTY", "EPERM"]);
|
||||
const TEMP_REMOVE_RETRY_DELAYS_MS = [10, 25, 50];
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
}
|
||||
@@ -25,6 +28,22 @@ function removePathIfExists(targetPath) {
|
||||
fs.rmSync(targetPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function isTransientTempRemoveError(error) {
|
||||
return (
|
||||
!!error &&
|
||||
typeof error === "object" &&
|
||||
typeof error.code === "string" &&
|
||||
TRANSIENT_TEMP_REMOVE_ERROR_CODES.has(error.code)
|
||||
);
|
||||
}
|
||||
|
||||
function sleepSync(ms) {
|
||||
if (!Number.isFinite(ms) || ms <= 0) {
|
||||
return;
|
||||
}
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||||
}
|
||||
|
||||
function makeTempDir(parentDir, prefix) {
|
||||
return fs.mkdtempSync(path.join(parentDir, prefix));
|
||||
}
|
||||
@@ -907,7 +926,22 @@ function removeStaleRuntimeDepsTempDirs(pluginDir) {
|
||||
}
|
||||
for (const entry of fs.readdirSync(pluginDir, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith(".openclaw-runtime-deps-")) {
|
||||
removePathIfExists(path.join(pluginDir, entry.name));
|
||||
const targetPath = path.join(pluginDir, entry.name);
|
||||
for (let attempt = 0; attempt <= TEMP_REMOVE_RETRY_DELAYS_MS.length; attempt += 1) {
|
||||
try {
|
||||
removePathIfExists(targetPath);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (!isTransientTempRemoveError(error)) {
|
||||
throw error;
|
||||
}
|
||||
const delay = TEMP_REMOVE_RETRY_DELAYS_MS[attempt];
|
||||
if (delay === undefined) {
|
||||
break;
|
||||
}
|
||||
sleepSync(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
collectRuntimeDependencyInstallManifest,
|
||||
collectRuntimeDependencyInstallSpecs,
|
||||
@@ -16,6 +16,10 @@ type RuntimeDepsStampParams = {
|
||||
};
|
||||
|
||||
describe("stageBundledPluginRuntimeDeps", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function createBundledPluginFixture(params: {
|
||||
packageJson: Record<string, unknown>;
|
||||
pluginId?: string;
|
||||
@@ -253,6 +257,50 @@ describe("stageBundledPluginRuntimeDeps", () => {
|
||||
expect(fs.readFileSync(path.join(pluginDir, "node_modules", "marker.txt"), "utf8")).toBe("2\n");
|
||||
});
|
||||
|
||||
it("retries stale temp dir cleanup races before staging runtime deps", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
name: "@openclaw/fixture-plugin",
|
||||
version: "1.0.0",
|
||||
dependencies: { "left-pad": "1.3.0" },
|
||||
openclaw: { bundle: { stageRuntimeDependencies: true } },
|
||||
},
|
||||
});
|
||||
const staleTempDir = path.join(pluginDir, ".openclaw-runtime-deps-copy-stale");
|
||||
fs.mkdirSync(staleTempDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(staleTempDir, "marker.txt"), "stale\n", "utf8");
|
||||
const realRmSync = fs.rmSync.bind(fs);
|
||||
let cleanupAttempts = 0;
|
||||
vi.spyOn(fs, "rmSync").mockImplementation((target, options) => {
|
||||
if (String(target) === staleTempDir && cleanupAttempts === 0) {
|
||||
cleanupAttempts += 1;
|
||||
const error = new Error("Directory not empty") as NodeJS.ErrnoException;
|
||||
error.code = "ENOTEMPTY";
|
||||
throw error;
|
||||
}
|
||||
if (String(target) === staleTempDir) {
|
||||
cleanupAttempts += 1;
|
||||
}
|
||||
return realRmSync(target, options);
|
||||
});
|
||||
|
||||
stageBundledPluginRuntimeDeps({
|
||||
cwd: repoRoot,
|
||||
installPluginRuntimeDepsImpl: ({ fingerprint, stampPath }: RuntimeDepsStampParams) => {
|
||||
const nodeModulesDir = path.join(pluginDir, "node_modules");
|
||||
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(nodeModulesDir, "marker.txt"), "installed\n", "utf8");
|
||||
writeRuntimeDepsStamp(stampPath, fingerprint);
|
||||
},
|
||||
});
|
||||
|
||||
expect(cleanupAttempts).toBe(2);
|
||||
expect(fs.existsSync(staleTempDir)).toBe(false);
|
||||
expect(fs.readFileSync(path.join(pluginDir, "node_modules", "marker.txt"), "utf8")).toBe(
|
||||
"installed\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("restages when installed root runtime dependency contents change", () => {
|
||||
const { pluginDir, repoRoot } = createBundledPluginFixture({
|
||||
packageJson: {
|
||||
|
||||
Reference in New Issue
Block a user