fix(auth): preserve non-signalable lock owners · openclaw/openclaw@c499ef1
steipete
·
2026-05-14
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { isPidDefinitelyDead, shouldRemoveDeadOwnerOrExpiredLock } from "./stale-lock-file.js"; |
| 3 | + |
| 4 | +describe("stale lock file ownership", () => { |
| 5 | +it("treats permission-denied process probes as not definitely dead", () => { |
| 6 | +expect( |
| 7 | +shouldRemoveDeadOwnerOrExpiredLock({ |
| 8 | +payload: { |
| 9 | +pid: 123, |
| 10 | +createdAt: new Date(Date.now() - 60_000).toISOString(), |
| 11 | +}, |
| 12 | +staleMs: 10, |
| 13 | +isPidDefinitelyDead: () => false, |
| 14 | +}), |
| 15 | +).toBe(false); |
| 16 | +}); |
| 17 | + |
| 18 | +it("only removes pid-owned locks when the owner is definitely dead", () => { |
| 19 | +expect( |
| 20 | +shouldRemoveDeadOwnerOrExpiredLock({ |
| 21 | +payload: { |
| 22 | +pid: 123, |
| 23 | +createdAt: new Date(Date.now() - 60_000).toISOString(), |
| 24 | +}, |
| 25 | +staleMs: 10, |
| 26 | +isPidDefinitelyDead: () => true, |
| 27 | +}), |
| 28 | +).toBe(true); |
| 29 | +}); |
| 30 | + |
| 31 | +it("treats invalid pids as definitely dead", () => { |
| 32 | +expect(isPidDefinitelyDead(-1)).toBe(true); |
| 33 | +}); |
| 34 | +}); |
| Original file line number | Diff line number | Diff line change |
|---|
|
1 | 1 | import fs from "node:fs/promises"; |
2 | | -import { isPidAlive as defaultIsPidAlive } from "../shared/pid-alive.js"; |
3 | 2 | |
4 | 3 | export type LockFileSnapshot = { |
5 | 4 | raw: string; |
@@ -52,11 +51,11 @@ export function shouldRemoveDeadOwnerOrExpiredLock(params: {
|
52 | 51 | payload: Record<string, unknown> | null; |
53 | 52 | staleMs: number; |
54 | 53 | nowMs?: number; |
55 | | -isPidAlive?: (pid: number) => boolean; |
| 54 | +isPidDefinitelyDead?: (pid: number) => boolean; |
56 | 55 | }): boolean { |
57 | 56 | const payload = readLockFileOwnerPayload(params.payload); |
58 | 57 | if (payload?.pid) { |
59 | | -return !(params.isPidAlive ?? defaultIsPidAlive)(payload.pid); |
| 58 | +return (params.isPidDefinitelyDead ?? isPidDefinitelyDead)(payload.pid); |
60 | 59 | } |
61 | 60 | if (payload?.createdAt) { |
62 | 61 | const createdAt = Date.parse(payload.createdAt); |
@@ -65,6 +64,18 @@ export function shouldRemoveDeadOwnerOrExpiredLock(params: {
|
65 | 64 | return false; |
66 | 65 | } |
67 | 66 | |
| 67 | +export function isPidDefinitelyDead(pid: number): boolean { |
| 68 | +if (!Number.isInteger(pid) || pid <= 0) { |
| 69 | +return true; |
| 70 | +} |
| 71 | +try { |
| 72 | +process.kill(pid, 0); |
| 73 | +return false; |
| 74 | +} catch (err) { |
| 75 | +return (err as NodeJS.ErrnoException).code === "ESRCH"; |
| 76 | +} |
| 77 | +} |
| 78 | + |
68 | 79 | export async function removeLockFileIfSnapshotMatches(params: { |
69 | 80 | lockPath: string; |
70 | 81 | snapshot: LockFileSnapshot; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -69,7 +69,7 @@ describe("acquireFileLock", () => {
|
69 | 69 | stale: 10, |
70 | 70 | } as const; |
71 | 71 | |
72 | | -const deadPid = Number.MAX_SAFE_INTEGER; |
| 72 | +const deadPid = -1; |
73 | 73 | await fs.writeFile( |
74 | 74 | lockPath, |
75 | 75 | JSON.stringify({ pid: deadPid, createdAt: new Date(Date.now() - 60_000).toISOString() }), |
@@ -147,7 +147,7 @@ describe("acquireFileLock", () => {
|
147 | 147 | } |
148 | 148 | })(), |
149 | 149 | ).rejects.toMatchObject({ |
150 | | -code: FILE_LOCK_STALE_ERROR_CODE, |
| 150 | +code: FILE_LOCK_TIMEOUT_ERROR_CODE, |
151 | 151 | }); |
152 | 152 | await expect(fs.realpath(caught?.lockPath ?? "")).resolves.toBe(await fs.realpath(lockPath)); |
153 | 153 | await expect(fs.readFile(lockPath, "utf8")).resolves.toContain(`"pid":${process.pid}`); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,11 +5,9 @@ import {
|
5 | 5 | resetFileLockManagerForTest, |
6 | 6 | } from "@openclaw/fs-safe/file-lock"; |
7 | 7 | import { |
8 | | -readLockFileOwnerPayload, |
9 | 8 | removeReportedStaleLockIfStillStale, |
10 | 9 | shouldRemoveDeadOwnerOrExpiredLock, |
11 | 10 | } from "../infra/stale-lock-file.js"; |
12 | | -import { isPidAlive } from "../shared/pid-alive.js"; |
13 | 11 | |
14 | 12 | export type FileLockOptions = { |
15 | 13 | retries: { |
@@ -48,15 +46,11 @@ async function shouldReclaimPluginLock(params: {
|
48 | 46 | staleMs: number; |
49 | 47 | nowMs: number; |
50 | 48 | }): Promise<boolean> { |
51 | | -const payload = readLockFileOwnerPayload(params.payload); |
52 | | -if (payload?.pid && !isPidAlive(payload.pid)) { |
53 | | -return true; |
54 | | -} |
55 | | -if (payload?.createdAt) { |
56 | | -const createdAt = Date.parse(payload.createdAt); |
57 | | -return !Number.isFinite(createdAt) || params.nowMs - createdAt > params.staleMs; |
58 | | -} |
59 | | -return false; |
| 49 | +return shouldRemoveDeadOwnerOrExpiredLock({ |
| 50 | +payload: params.payload, |
| 51 | +staleMs: params.staleMs, |
| 52 | +nowMs: params.nowMs, |
| 53 | +}); |
60 | 54 | } |
61 | 55 | |
62 | 56 | function isFileLockError(error: unknown, code: string): boolean { |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。