fix: preserve live runtime deps temp dirs

This commit is contained in:
Peter Steinberger
2026-04-27 08:39:30 +01:00
parent da55212c6e
commit 4514a73170
3 changed files with 120 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ 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];
const TEMP_OWNER_FILE = "owner.json";
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
@@ -48,13 +49,26 @@ function makeTempDir(parentDir, prefix) {
return fs.mkdtempSync(path.join(parentDir, prefix));
}
function writeRuntimeDepsTempOwner(tempDir) {
writeJson(path.join(tempDir, TEMP_OWNER_FILE), {
pid: process.pid,
createdAtMs: Date.now(),
});
}
function makeOwnedTempDir(parentDir, prefix) {
const tempDir = makeTempDir(parentDir, prefix);
writeRuntimeDepsTempOwner(tempDir);
return tempDir;
}
function sanitizeTempPrefixSegment(value) {
const normalized = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-");
return normalized.length > 0 ? normalized : "plugin";
}
function makePluginOwnedTempDir(pluginDir, label) {
return makeTempDir(pluginDir, `.openclaw-runtime-deps-${label}-`);
return makeOwnedTempDir(pluginDir, `.openclaw-runtime-deps-${label}-`);
}
function assertPathIsNotSymlink(targetPath, label) {
@@ -84,6 +98,7 @@ function replaceDirAtomically(targetPath, sourcePath) {
try {
if (fs.existsSync(targetPath)) {
fs.renameSync(targetPath, backupPath);
writeRuntimeDepsTempOwner(backupPath);
movedExistingTarget = true;
}
fs.renameSync(sourcePath, targetPath);
@@ -100,7 +115,7 @@ function writeJsonAtomically(targetPath, value) {
assertPathIsNotSymlink(targetPath, "write runtime deps stamp");
const targetParentDir = path.dirname(targetPath);
fs.mkdirSync(targetParentDir, { recursive: true });
const tempDir = makeTempDir(
const tempDir = makeOwnedTempDir(
targetParentDir,
`.openclaw-runtime-deps-stamp-${sanitizeTempPrefixSegment(path.basename(targetPath))}-`,
);
@@ -954,6 +969,35 @@ function readRuntimeDepsStamp(stampPath) {
}
}
function readRuntimeDepsTempOwner(tempDir) {
try {
const owner = readJson(path.join(tempDir, TEMP_OWNER_FILE));
return owner && typeof owner === "object" ? owner : null;
} catch {
return null;
}
}
function isLiveProcess(pid) {
if (!Number.isInteger(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error?.code === "EPERM";
}
}
function shouldRemoveRuntimeDepsTempDir(tempDir) {
const owner = readRuntimeDepsTempOwner(tempDir);
if (!owner || typeof owner.pid !== "number") {
return true;
}
return !isLiveProcess(owner.pid);
}
function removeStaleRuntimeDepsTempDirs(pluginDir) {
if (!fs.existsSync(pluginDir)) {
return;
@@ -961,6 +1005,9 @@ function removeStaleRuntimeDepsTempDirs(pluginDir) {
for (const entry of fs.readdirSync(pluginDir, { withFileTypes: true })) {
if (entry.name.startsWith(".openclaw-runtime-deps-")) {
const targetPath = path.join(pluginDir, entry.name);
if (!shouldRemoveRuntimeDepsTempDir(targetPath)) {
continue;
}
for (let attempt = 0; attempt <= TEMP_REMOVE_RETRY_DELAYS_MS.length; attempt += 1) {
try {
removePathIfExists(targetPath);
@@ -1243,7 +1290,10 @@ export function stageBundledPluginRuntimeDeps(params = {}) {
}
export const __testing = {
removeStaleRuntimeDepsTempDirs,
replaceDirAtomically,
runNpmInstall,
writeRuntimeDepsTempOwner,
};
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {