

























@@ -49,6 +49,11 @@ const RETAINED_RUNTIME_DEPS_MANIFEST = ".openclaw-runtime-deps.json";
4949// to the plugin root. Source-checkout installs already have their own cache
5050// path and keep using it.
5151const PLUGIN_ROOT_INSTALL_STAGE_DIR = ".openclaw-install-stage";
52+const BUNDLED_RUNTIME_DEPS_LOCK_DIR = ".openclaw-runtime-deps.lock";
53+const BUNDLED_RUNTIME_DEPS_LOCK_OWNER_FILE = "owner.json";
54+const BUNDLED_RUNTIME_DEPS_LOCK_WAIT_MS = 100;
55+const BUNDLED_RUNTIME_DEPS_LOCK_TIMEOUT_MS = 5 * 60_000;
56+const BUNDLED_RUNTIME_DEPS_LOCK_STALE_MS = 10 * 60_000;
52575358export type BundledRuntimeDepsNpmRunner = {
5459command: string;
@@ -170,6 +175,82 @@ function readJsonObject(filePath: string): JsonObject | null {
170175}
171176}
172177178+function sleepSync(ms: number): void {
179+Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
180+}
181+182+function isProcessAlive(pid: number): boolean {
183+if (!Number.isInteger(pid) || pid <= 0) {
184+return false;
185+}
186+try {
187+process.kill(pid, 0);
188+return true;
189+} catch {
190+return false;
191+}
192+}
193+194+function readRuntimeDepsLockOwner(lockDir: string): { pid?: number; createdAtMs?: number } {
195+const owner = readJsonObject(path.join(lockDir, BUNDLED_RUNTIME_DEPS_LOCK_OWNER_FILE));
196+return {
197+pid: typeof owner?.pid === "number" ? owner.pid : undefined,
198+createdAtMs: typeof owner?.createdAtMs === "number" ? owner.createdAtMs : undefined,
199+};
200+}
201+202+function removeRuntimeDepsLockIfStale(lockDir: string, nowMs: number): boolean {
203+const owner = readRuntimeDepsLockOwner(lockDir);
204+const createdAtMs = owner.createdAtMs;
205+const staleByTime =
206+typeof createdAtMs === "number" && nowMs - createdAtMs > BUNDLED_RUNTIME_DEPS_LOCK_STALE_MS;
207+const staleByPid = typeof owner.pid === "number" && !isProcessAlive(owner.pid);
208+if (!staleByTime && !staleByPid) {
209+return false;
210+}
211+try {
212+fs.rmSync(lockDir, { recursive: true, force: true });
213+return true;
214+} catch {
215+return false;
216+}
217+}
218+219+function withBundledRuntimeDepsInstallRootLock<T>(installRoot: string, run: () => T): T {
220+fs.mkdirSync(installRoot, { recursive: true });
221+const lockDir = path.join(installRoot, BUNDLED_RUNTIME_DEPS_LOCK_DIR);
222+const startedAt = Date.now();
223+let locked = false;
224+while (!locked) {
225+try {
226+fs.mkdirSync(lockDir);
227+fs.writeFileSync(
228+path.join(lockDir, BUNDLED_RUNTIME_DEPS_LOCK_OWNER_FILE),
229+`${JSON.stringify({ pid: process.pid, createdAtMs: Date.now() }, null, 2)}\n`,
230+"utf8",
231+);
232+locked = true;
233+} catch (error) {
234+const code = (error as NodeJS.ErrnoException).code;
235+if (code !== "EEXIST") {
236+throw error;
237+}
238+removeRuntimeDepsLockIfStale(lockDir, Date.now());
239+if (Date.now() - startedAt > BUNDLED_RUNTIME_DEPS_LOCK_TIMEOUT_MS) {
240+throw new Error(`Timed out waiting for bundled runtime deps lock at ${lockDir}`, {
241+cause: error,
242+});
243+}
244+sleepSync(BUNDLED_RUNTIME_DEPS_LOCK_WAIT_MS);
245+}
246+}
247+try {
248+return run();
249+} finally {
250+fs.rmSync(lockDir, { recursive: true, force: true });
251+}
252+}
253+173254function collectRuntimeDeps(packageJson: JsonObject): Record<string, unknown> {
174255return {
175256 ...(packageJson.dependencies as Record<string, unknown> | undefined),
@@ -935,67 +1016,69 @@ export function ensureBundledPluginRuntimeDeps(params: {
9351016const installRoot = resolveBundledRuntimeDependencyInstallRoot(params.pluginRoot, {
9361017env: params.env,
9371018});
938-const persistRetainedManifest = shouldPersistRetainedRuntimeDepsManifest({
939-pluginRoot: params.pluginRoot,
940- installRoot,
941-});
942-if (!persistRetainedManifest) {
943-removeRetainedRuntimeDepsManifest(installRoot);
944-}
945-const dependencySpecs = deps
946-.map((dep) => `${dep.name}@${dep.version}`)
947-.toSorted((left, right) => left.localeCompare(right));
948-const missingSpecs = deps
949-.filter((dep) => !hasDependencySentinel([installRoot], dep))
950-.map((dep) => `${dep.name}@${dep.version}`)
951-.toSorted((left, right) => left.localeCompare(right));
952-if (missingSpecs.length === 0) {
953-return { installedSpecs: [], retainSpecs: [] };
954-}
955-const retainedManifestSpecs = persistRetainedManifest
956- ? readRetainedRuntimeDepsManifest(installRoot)
957- : [];
958-const installSpecs = [
959- ...new Set([...(params.retainSpecs ?? []), ...retainedManifestSpecs, ...dependencySpecs]),
960-].toSorted((left, right) => left.localeCompare(right));
961-const cacheDir = resolveSourceCheckoutRuntimeDepsCacheDir({
962-pluginId: params.pluginId,
963-pluginRoot: params.pluginRoot,
964- installSpecs,
965-});
966-const isPluginRootInstall = path.resolve(installRoot) === path.resolve(params.pluginRoot);
967-const sourceCheckoutCacheStage =
968-cacheDir &&
969-isPluginRootInstall &&
970-resolveSourceCheckoutBundledPluginPackageRoot(params.pluginRoot)
971- ? cacheDir
972- : undefined;
973-const installExecutionRoot =
974-sourceCheckoutCacheStage ??
975-(isPluginRootInstall ? path.join(installRoot, PLUGIN_ROOT_INSTALL_STAGE_DIR) : undefined);
976-if (
977-restoreSourceCheckoutRuntimeDepsFromCache({
978- cacheDir,
979- deps,
1019+return withBundledRuntimeDepsInstallRootLock(installRoot, () => {
1020+const persistRetainedManifest = shouldPersistRetainedRuntimeDepsManifest({
1021+pluginRoot: params.pluginRoot,
9801022 installRoot,
981-})
982-) {
983-return { installedSpecs: [], retainSpecs: [] };
984-}
1023+});
1024+if (!persistRetainedManifest) {
1025+removeRetainedRuntimeDepsManifest(installRoot);
1026+}
1027+const dependencySpecs = deps
1028+.map((dep) => `${dep.name}@${dep.version}`)
1029+.toSorted((left, right) => left.localeCompare(right));
1030+const missingSpecs = deps
1031+.filter((dep) => !hasDependencySentinel([installRoot], dep))
1032+.map((dep) => `${dep.name}@${dep.version}`)
1033+.toSorted((left, right) => left.localeCompare(right));
1034+if (missingSpecs.length === 0) {
1035+return { installedSpecs: [], retainSpecs: [] };
1036+}
1037+const retainedManifestSpecs = persistRetainedManifest
1038+ ? readRetainedRuntimeDepsManifest(installRoot)
1039+ : [];
1040+const installSpecs = [
1041+ ...new Set([...(params.retainSpecs ?? []), ...retainedManifestSpecs, ...dependencySpecs]),
1042+].toSorted((left, right) => left.localeCompare(right));
1043+const cacheDir = resolveSourceCheckoutRuntimeDepsCacheDir({
1044+pluginId: params.pluginId,
1045+pluginRoot: params.pluginRoot,
1046+ installSpecs,
1047+});
1048+const isPluginRootInstall = path.resolve(installRoot) === path.resolve(params.pluginRoot);
1049+const sourceCheckoutCacheStage =
1050+cacheDir &&
1051+isPluginRootInstall &&
1052+resolveSourceCheckoutBundledPluginPackageRoot(params.pluginRoot)
1053+ ? cacheDir
1054+ : undefined;
1055+const installExecutionRoot =
1056+sourceCheckoutCacheStage ??
1057+(isPluginRootInstall ? path.join(installRoot, PLUGIN_ROOT_INSTALL_STAGE_DIR) : undefined);
1058+if (
1059+restoreSourceCheckoutRuntimeDepsFromCache({
1060+ cacheDir,
1061+ deps,
1062+ installRoot,
1063+})
1064+) {
1065+return { installedSpecs: [], retainSpecs: [] };
1066+}
9851067986-const install =
987-params.installDeps ??
988-((installParams) =>
989-installBundledRuntimeDeps({
990-installRoot: installParams.installRoot,
991-installExecutionRoot: installParams.installExecutionRoot,
992-missingSpecs: installParams.installSpecs ?? installParams.missingSpecs,
993-env: params.env,
994-}));
995-install({ installRoot, installExecutionRoot, missingSpecs, installSpecs });
996-if (persistRetainedManifest) {
997-writeRetainedRuntimeDepsManifest(installRoot, installSpecs);
998-}
999-storeSourceCheckoutRuntimeDepsCache({ cacheDir, installRoot });
1000-return { installedSpecs: missingSpecs, retainSpecs: installSpecs };
1068+const install =
1069+params.installDeps ??
1070+((installParams) =>
1071+installBundledRuntimeDeps({
1072+installRoot: installParams.installRoot,
1073+installExecutionRoot: installParams.installExecutionRoot,
1074+missingSpecs: installParams.installSpecs ?? installParams.missingSpecs,
1075+env: params.env,
1076+}));
1077+install({ installRoot, installExecutionRoot, missingSpecs, installSpecs });
1078+if (persistRetainedManifest) {
1079+writeRetainedRuntimeDepsManifest(installRoot, installSpecs);
1080+}
1081+storeSourceCheckoutRuntimeDepsCache({ cacheDir, installRoot });
1082+return { installedSpecs: missingSpecs, retainSpecs: installSpecs };
1083+});
10011084}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。