mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 06:02:56 +00:00
260 lines
8.6 KiB
TypeScript
260 lines
8.6 KiB
TypeScript
import { mkdtemp, open, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import {
|
|
acquirePidLock,
|
|
getPidLockInfo,
|
|
isLocked,
|
|
PidLockError,
|
|
refreshPidLock,
|
|
releasePidLock,
|
|
updatePidLock,
|
|
} from "./pid-lock.js";
|
|
|
|
describe("pid-lock ownership", () => {
|
|
test("writes and releases lock for explicit owner pid", async () => {
|
|
const parent = await mkdtemp(join(tmpdir(), "paseo-pid-lock-owner-"));
|
|
const paseoHome = join(parent, "home");
|
|
const ownerPid = process.pid + 10_000;
|
|
|
|
try {
|
|
await (
|
|
acquirePidLock as unknown as (
|
|
home: string,
|
|
sockPath: string | null,
|
|
options: { ownerPid: number },
|
|
) => Promise<void>
|
|
)(paseoHome, null, { ownerPid });
|
|
|
|
if (process.platform !== "win32") {
|
|
expect((await stat(paseoHome)).mode & 0o777).toBe(0o700);
|
|
}
|
|
const lock = await getPidLockInfo(paseoHome);
|
|
expect(lock?.pid).toBe(ownerPid);
|
|
expect(lock?.listen).toBeNull();
|
|
expect(lock?.heartbeat).toBe(true);
|
|
|
|
await (
|
|
updatePidLock as unknown as (
|
|
home: string,
|
|
patch: { listen: string },
|
|
options: { ownerPid: number },
|
|
) => Promise<void>
|
|
)(paseoHome, { listen: "127.0.0.1:6767" }, { ownerPid });
|
|
|
|
const updatedLock = await getPidLockInfo(paseoHome);
|
|
expect(updatedLock?.listen).toBe("127.0.0.1:6767");
|
|
|
|
await (
|
|
releasePidLock as unknown as (home: string, options: { ownerPid: number }) => Promise<void>
|
|
)(paseoHome, { ownerPid: ownerPid + 1 });
|
|
const lockAfterWrongOwnerRelease = await getPidLockInfo(paseoHome);
|
|
expect(lockAfterWrongOwnerRelease?.pid).toBe(ownerPid);
|
|
|
|
await (
|
|
releasePidLock as unknown as (home: string, options: { ownerPid: number }) => Promise<void>
|
|
)(paseoHome, { ownerPid });
|
|
const lockAfterOwnerRelease = await getPidLockInfo(paseoHome);
|
|
expect(lockAfterOwnerRelease).toBeNull();
|
|
} finally {
|
|
await rm(parent, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("keeps a stale heartbeat lock when the recorded pid is alive without a reachability check", async () => {
|
|
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-stale-heartbeat-"));
|
|
const replacementOwnerPid = process.pid + 10_000;
|
|
|
|
try {
|
|
const pidPath = join(paseoHome, "paseo.pid");
|
|
await writeFile(
|
|
pidPath,
|
|
JSON.stringify({
|
|
pid: process.pid,
|
|
startedAt: new Date().toISOString(),
|
|
hostname: "old-host",
|
|
uid: process.getuid?.() ?? 0,
|
|
listen: "127.0.0.1:6767",
|
|
desktopManaged: true,
|
|
heartbeat: true,
|
|
}),
|
|
);
|
|
const staleTime = new Date(Date.now() - 10 * 60_000);
|
|
await utimes(pidPath, staleTime, staleTime);
|
|
|
|
await expect(isLocked(paseoHome)).resolves.toMatchObject({ locked: true });
|
|
await expect(
|
|
acquirePidLock(paseoHome, null, { ownerPid: replacementOwnerPid }),
|
|
).rejects.toThrow("Another Paseo daemon is already running");
|
|
|
|
const lock = await getPidLockInfo(paseoHome);
|
|
expect(lock?.pid).toBe(process.pid);
|
|
} finally {
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("preserves a stale live desktop heartbeat lock", async () => {
|
|
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-stale-desktop-heartbeat-"));
|
|
const replacementOwnerPid = process.pid + 10_000;
|
|
|
|
try {
|
|
const pidPath = join(paseoHome, "paseo.pid");
|
|
await writeFile(
|
|
pidPath,
|
|
JSON.stringify({
|
|
pid: process.pid,
|
|
startedAt: new Date().toISOString(),
|
|
hostname: "old-host",
|
|
uid: process.getuid?.() ?? 0,
|
|
listen: "127.0.0.1:6767",
|
|
desktopManaged: true,
|
|
heartbeat: true,
|
|
}),
|
|
);
|
|
const staleTime = new Date(Date.now() - 10 * 60_000);
|
|
await utimes(pidPath, staleTime, staleTime);
|
|
|
|
await expect(
|
|
acquirePidLock(paseoHome, null, { ownerPid: replacementOwnerPid }),
|
|
).rejects.toThrow("Another Paseo daemon is already running");
|
|
|
|
const lock = await getPidLockInfo(paseoHome);
|
|
expect(lock?.pid).toBe(process.pid);
|
|
expect(lock?.listen).toBe("127.0.0.1:6767");
|
|
} finally {
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("keeps a stale live lock written by a pre-heartbeat daemon", async () => {
|
|
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-legacy-live-"));
|
|
const pidPath = join(paseoHome, "paseo.pid");
|
|
|
|
try {
|
|
await writeFile(
|
|
pidPath,
|
|
JSON.stringify({
|
|
pid: process.pid,
|
|
startedAt: new Date().toISOString(),
|
|
hostname: "old-host",
|
|
uid: process.getuid?.() ?? 0,
|
|
listen: "127.0.0.1:6767",
|
|
desktopManaged: true,
|
|
}),
|
|
);
|
|
const staleTime = new Date(Date.now() - 10 * 60_000);
|
|
await utimes(pidPath, staleTime, staleTime);
|
|
|
|
await expect(
|
|
acquirePidLock(paseoHome, null, { ownerPid: process.pid + 10_000 }),
|
|
).rejects.toThrow("Another Paseo daemon is already running");
|
|
|
|
const lock = await getPidLockInfo(paseoHome);
|
|
expect(lock?.pid).toBe(process.pid);
|
|
} finally {
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("preserves a stale live legacy desktop lock", async () => {
|
|
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-legacy-desktop-"));
|
|
const replacementOwnerPid = process.pid + 10_000;
|
|
const pidPath = join(paseoHome, "paseo.pid");
|
|
|
|
try {
|
|
await writeFile(
|
|
pidPath,
|
|
JSON.stringify({
|
|
pid: process.pid,
|
|
startedAt: new Date().toISOString(),
|
|
hostname: "old-host",
|
|
uid: process.getuid?.() ?? 0,
|
|
listen: "127.0.0.1:6767",
|
|
desktopManaged: true,
|
|
}),
|
|
);
|
|
const staleTime = new Date(Date.now() - 10 * 60_000);
|
|
await utimes(pidPath, staleTime, staleTime);
|
|
|
|
await expect(
|
|
acquirePidLock(paseoHome, null, { ownerPid: replacementOwnerPid }),
|
|
).rejects.toThrow("Another Paseo daemon is already running");
|
|
|
|
const lock = await getPidLockInfo(paseoHome);
|
|
expect(lock?.pid).toBe(process.pid);
|
|
expect(lock?.heartbeat).toBeUndefined();
|
|
} finally {
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("rejects a heartbeat refresh after another supervisor takes ownership", async () => {
|
|
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-refresh-owner-"));
|
|
|
|
try {
|
|
await acquirePidLock(paseoHome, null, { ownerPid: process.pid + 10_000 });
|
|
|
|
await expect(refreshPidLock(paseoHome, { ownerPid: process.pid })).rejects.toBeInstanceOf(
|
|
PidLockError,
|
|
);
|
|
} finally {
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("retries a heartbeat refresh while its owner is rewriting the lock", async () => {
|
|
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-refresh-rewrite-"));
|
|
const pidPath = join(paseoHome, "paseo.pid");
|
|
|
|
try {
|
|
await acquirePidLock(paseoHome, null, { ownerPid: process.pid });
|
|
const lock = await getPidLockInfo(paseoHome);
|
|
expect(lock).not.toBeNull();
|
|
|
|
const rewriteHandle = await open(pidPath, "r+");
|
|
await rewriteHandle.truncate(0);
|
|
|
|
const refresh = refreshPidLock(paseoHome, { ownerPid: process.pid });
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
await rewriteHandle.writeFile(JSON.stringify(lock));
|
|
await rewriteHandle.close();
|
|
|
|
await expect(refresh).resolves.toBeUndefined();
|
|
} finally {
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("keeps a fresh lock when the recorded pid is alive", async () => {
|
|
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-pid-lock-fresh-heartbeat-"));
|
|
|
|
try {
|
|
await writeFile(
|
|
join(paseoHome, "paseo.pid"),
|
|
JSON.stringify({
|
|
pid: process.pid,
|
|
startedAt: new Date().toISOString(),
|
|
hostname: "current-host",
|
|
uid: process.getuid?.() ?? 0,
|
|
listen: "127.0.0.1:6767",
|
|
desktopManaged: true,
|
|
heartbeat: true,
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
acquirePidLock(paseoHome, null, { ownerPid: process.pid + 10_000 }),
|
|
).rejects.toThrow("Another Paseo daemon is already running");
|
|
|
|
const lock = await getPidLockInfo(paseoHome);
|
|
expect(lock?.pid).toBe(process.pid);
|
|
expect(lock?.listen).toBe("127.0.0.1:6767");
|
|
} finally {
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|