diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 96010437510..221b17fbffe 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { + __testing as bundledRuntimeDepsTesting, createBundledRuntimeDependencyAliasMap, createBundledRuntimeDepsInstallArgs, createBundledRuntimeDepsInstallEnv, @@ -656,6 +657,16 @@ describe("ensureBundledPluginRuntimeDeps", () => { ]); }); + it("does not expire active runtime-deps install locks by age alone", () => { + expect( + bundledRuntimeDepsTesting.shouldRemoveRuntimeDepsLock( + { pid: 123, createdAtMs: 0 }, + Number.MAX_SAFE_INTEGER, + () => true, + ), + ).toBe(false); + }); + it("removes stale runtime-deps install locks before repairing deps", () => { const packageRoot = makeTempDir(); const pluginRoot = path.join(packageRoot, "dist", "extensions", "openai"); @@ -670,10 +681,7 @@ describe("ensureBundledPluginRuntimeDeps", () => { ); const lockDir = path.join(pluginRoot, ".openclaw-runtime-deps.lock"); fs.mkdirSync(lockDir, { recursive: true }); - fs.writeFileSync( - path.join(lockDir, "owner.json"), - JSON.stringify({ pid: process.pid, createdAtMs: 0 }), - ); + fs.writeFileSync(path.join(lockDir, "owner.json"), JSON.stringify({ pid: 0, createdAtMs: 0 })); const calls: BundledRuntimeDepsInstallParams[] = []; const result = ensureBundledPluginRuntimeDeps({ diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index 2e20afeb1d1..817c1db18b2 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -199,15 +199,31 @@ function readRuntimeDepsLockOwner(lockDir: string): { pid?: number; createdAtMs? }; } +function shouldRemoveRuntimeDepsLock( + owner: { pid?: number; createdAtMs?: number }, + nowMs: number, + isAlive: (pid: number) => boolean = isProcessAlive, +): boolean { + if (typeof owner.pid === "number") { + return !isAlive(owner.pid); + } + + return ( + typeof owner.createdAtMs === "number" && + nowMs - owner.createdAtMs > BUNDLED_RUNTIME_DEPS_LOCK_STALE_MS + ); +} + +export const __testing = { + shouldRemoveRuntimeDepsLock, +}; + function removeRuntimeDepsLockIfStale(lockDir: string, nowMs: number): boolean { const owner = readRuntimeDepsLockOwner(lockDir); - const createdAtMs = owner.createdAtMs; - const staleByTime = - typeof createdAtMs === "number" && nowMs - createdAtMs > BUNDLED_RUNTIME_DEPS_LOCK_STALE_MS; - const staleByPid = typeof owner.pid === "number" && !isProcessAlive(owner.pid); - if (!staleByTime && !staleByPid) { + if (!shouldRemoveRuntimeDepsLock(owner, nowMs)) { return false; } + try { fs.rmSync(lockDir, { recursive: true, force: true }); return true;