fix: normalize session lock path

This commit is contained in:
Peter Steinberger
2026-01-23 18:30:11 +00:00
parent a1413a011e
commit fdc50a0feb
2 changed files with 51 additions and 9 deletions

View File

@@ -0,0 +1,34 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { acquireSessionWriteLock } from "./session-write-lock.js";
describe("acquireSessionWriteLock", () => {
it("reuses locks across symlinked session paths", async () => {
if (process.platform === "win32") {
expect(true).toBe(true);
return;
}
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-"));
try {
const realDir = path.join(root, "real");
const linkDir = path.join(root, "link");
await fs.mkdir(realDir, { recursive: true });
await fs.symlink(realDir, linkDir);
const sessionReal = path.join(realDir, "sessions.json");
const sessionLink = path.join(linkDir, "sessions.json");
const lockA = await acquireSessionWriteLock({ sessionFile: sessionReal, timeoutMs: 500 });
const lockB = await acquireSessionWriteLock({ sessionFile: sessionLink, timeoutMs: 500 });
await lockB.release();
await lockA.release();
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
});

View File

@@ -45,20 +45,28 @@ export async function acquireSessionWriteLock(params: {
}> { }> {
const timeoutMs = params.timeoutMs ?? 10_000; const timeoutMs = params.timeoutMs ?? 10_000;
const staleMs = params.staleMs ?? 30 * 60 * 1000; const staleMs = params.staleMs ?? 30 * 60 * 1000;
const sessionFile = params.sessionFile; const sessionFile = path.resolve(params.sessionFile);
const lockPath = `${sessionFile}.lock`; const sessionDir = path.dirname(sessionFile);
await fs.mkdir(path.dirname(lockPath), { recursive: true }); await fs.mkdir(sessionDir, { recursive: true });
let normalizedDir = sessionDir;
try {
normalizedDir = await fs.realpath(sessionDir);
} catch {
// Fall back to the resolved path if realpath fails (permissions, transient FS).
}
const normalizedSessionFile = path.join(normalizedDir, path.basename(sessionFile));
const lockPath = `${normalizedSessionFile}.lock`;
const held = HELD_LOCKS.get(sessionFile); const held = HELD_LOCKS.get(normalizedSessionFile);
if (held) { if (held) {
held.count += 1; held.count += 1;
return { return {
release: async () => { release: async () => {
const current = HELD_LOCKS.get(sessionFile); const current = HELD_LOCKS.get(normalizedSessionFile);
if (!current) return; if (!current) return;
current.count -= 1; current.count -= 1;
if (current.count > 0) return; if (current.count > 0) return;
HELD_LOCKS.delete(sessionFile); HELD_LOCKS.delete(normalizedSessionFile);
await current.handle.close(); await current.handle.close();
await fs.rm(current.lockPath, { force: true }); await fs.rm(current.lockPath, { force: true });
}, },
@@ -75,14 +83,14 @@ export async function acquireSessionWriteLock(params: {
JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2), JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
"utf8", "utf8",
); );
HELD_LOCKS.set(sessionFile, { count: 1, handle, lockPath }); HELD_LOCKS.set(normalizedSessionFile, { count: 1, handle, lockPath });
return { return {
release: async () => { release: async () => {
const current = HELD_LOCKS.get(sessionFile); const current = HELD_LOCKS.get(normalizedSessionFile);
if (!current) return; if (!current) return;
current.count -= 1; current.count -= 1;
if (current.count > 0) return; if (current.count > 0) return;
HELD_LOCKS.delete(sessionFile); HELD_LOCKS.delete(normalizedSessionFile);
await current.handle.close(); await current.handle.close();
await fs.rm(current.lockPath, { force: true }); await fs.rm(current.lockPath, { force: true });
}, },