mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
Fix/issue 98958 gateway lock fd leak (#99291)
* fix(infra): close fd and remove lock file on writeFile failure (#98958)
When fs.open(lockPath, "wx") succeeds but handle.writeFile() fails
(e.g. disk full / ENOSPC), close the file handle and remove the
partially-written lock file before re-throwing to avoid a file
descriptor leak and stale lock artifact.
Changes:
- src/infra/gateway-lock.ts: nested try-catch around writeFile
- src/infra/gateway-lock.test.ts: test for fd close + lock cleanup
- scripts/verify-gateway-lock-fd-leak*.mjs: fault-injection proof
Co-Authored-By: Claude <noreply@anthropic.com>
* test(infra): strengthen gateway lock cleanup proof
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
(cherry picked from commit 36dd9ee3c3)
This commit is contained in:
committed by
Dallin Romney
parent
3faf92d6c7
commit
c510b7623f
@@ -377,6 +377,36 @@ describe("gateway lock", () => {
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("closes handle and removes lock file when writeFile fails after open succeeds", async () => {
|
||||
vi.useRealTimers();
|
||||
const env = await makeEnv();
|
||||
const { lockPath } = resolveLockPath(env);
|
||||
|
||||
const writeError = Object.assign(new Error("ENOSPC: no space left on device"), {
|
||||
code: "ENOSPC",
|
||||
});
|
||||
const close = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
|
||||
const mockHandle = {
|
||||
writeFile: vi.fn().mockImplementation(async () => {
|
||||
await fs.writeFile(lockPath, "partial", "utf8");
|
||||
throw writeError;
|
||||
}),
|
||||
close,
|
||||
};
|
||||
|
||||
const openSpy = vi.spyOn(fs, "open").mockResolvedValueOnce(mockHandle as never);
|
||||
|
||||
await expect(acquireForTest(env)).rejects.toMatchObject({
|
||||
name: "GatewayLockError",
|
||||
cause: writeError,
|
||||
});
|
||||
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
await expect(fs.access(lockPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("clears stale lock on win32 when process cmdline is not a gateway", async () => {
|
||||
vi.useRealTimers();
|
||||
const env = await makeEnv();
|
||||
|
||||
@@ -262,16 +262,24 @@ export async function acquireGatewayLock(
|
||||
while (now() - startedAt < timeoutMs) {
|
||||
try {
|
||||
const handle = await fs.open(lockPath, "wx");
|
||||
const startTime = platform === "linux" ? readLinuxStartTime(process.pid) : null;
|
||||
const payload: LockPayload = {
|
||||
pid: process.pid,
|
||||
createdAt: resolveTimestampMsToIsoString(now()),
|
||||
configPath,
|
||||
};
|
||||
if (typeof startTime === "number" && Number.isFinite(startTime)) {
|
||||
payload.startTime = startTime;
|
||||
try {
|
||||
const startTime = platform === "linux" ? readLinuxStartTime(process.pid) : null;
|
||||
const payload: LockPayload = {
|
||||
pid: process.pid,
|
||||
createdAt: resolveTimestampMsToIsoString(now()),
|
||||
configPath,
|
||||
};
|
||||
if (typeof startTime === "number" && Number.isFinite(startTime)) {
|
||||
payload.startTime = startTime;
|
||||
}
|
||||
await handle.writeFile(JSON.stringify(payload), "utf8");
|
||||
} catch (error) {
|
||||
// Acquisition owns both resources until the release callback exists.
|
||||
// Unwind them if payload preparation fails before ownership transfers.
|
||||
await handle.close().catch(() => undefined);
|
||||
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
await handle.writeFile(JSON.stringify(payload), "utf8");
|
||||
return {
|
||||
lockPath,
|
||||
configPath,
|
||||
|
||||
Reference in New Issue
Block a user