Implements acquireLock() in src/runtime/lockfile.ts that writes a PID-stamped lock file under workspaces_root, detects live vs stale locks via process.kill(pid, 0), and returns a release function. src/index.ts acquires the lock before DB init and releases it in the graceful-shutdown handler. Four unit tests cover acquire, duplicate- acquire rejection, stale-lock recovery, and release.
88 lines
2.4 KiB
TypeScript
88 lines
2.4 KiB
TypeScript
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
import { describe, it, expect, afterEach } from "vitest";
|
|
import { acquireLock } from "../../src/runtime/lockfile";
|
|
|
|
function makeTmpDir(): string {
|
|
const suffix = Math.random().toString(36).slice(2);
|
|
const dir = path.join(os.tmpdir(), `disclaw-test-${suffix}`);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
describe("acquireLock", () => {
|
|
const dirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const dir of dirs) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
dirs.length = 0;
|
|
});
|
|
|
|
it("(a) first acquire writes lock file with correct PID", async () => {
|
|
const dir = makeTmpDir();
|
|
dirs.push(dir);
|
|
const lockPath = path.join(dir, ".disclaw.lock");
|
|
|
|
const release = await acquireLock(lockPath);
|
|
|
|
expect(fs.existsSync(lockPath)).toBe(true);
|
|
const data = JSON.parse(fs.readFileSync(lockPath, "utf-8")) as {
|
|
pid: number;
|
|
started: string;
|
|
};
|
|
expect(data.pid).toBe(process.pid);
|
|
expect(typeof data.started).toBe("string");
|
|
expect(data.started).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
|
|
release();
|
|
});
|
|
|
|
it("(b) second acquire on same live lock throws", async () => {
|
|
const dir = makeTmpDir();
|
|
dirs.push(dir);
|
|
const lockPath = path.join(dir, ".disclaw.lock");
|
|
|
|
const release = await acquireLock(lockPath);
|
|
|
|
await expect(acquireLock(lockPath)).rejects.toThrow(
|
|
`DisClaw bereits aktiv (PID ${process.pid})`
|
|
);
|
|
|
|
release();
|
|
});
|
|
|
|
it("(c) stale lock (dead PID) is overwritten", async () => {
|
|
const dir = makeTmpDir();
|
|
dirs.push(dir);
|
|
const lockPath = path.join(dir, ".disclaw.lock");
|
|
|
|
// Write a stale lock with a PID that almost certainly doesn't exist
|
|
const staleData = { pid: 99999999, started: new Date().toISOString() };
|
|
fs.writeFileSync(lockPath, JSON.stringify(staleData), "utf-8");
|
|
|
|
const release = await acquireLock(lockPath);
|
|
|
|
const data = JSON.parse(fs.readFileSync(lockPath, "utf-8")) as {
|
|
pid: number;
|
|
started: string;
|
|
};
|
|
expect(data.pid).toBe(process.pid);
|
|
|
|
release();
|
|
});
|
|
|
|
it("(d) release() deletes the lock file", async () => {
|
|
const dir = makeTmpDir();
|
|
dirs.push(dir);
|
|
const lockPath = path.join(dir, ".disclaw.lock");
|
|
|
|
const release = await acquireLock(lockPath);
|
|
expect(fs.existsSync(lockPath)).toBe(true);
|
|
|
|
release();
|
|
expect(fs.existsSync(lockPath)).toBe(false);
|
|
});
|
|
});
|