mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { spawn, type ChildProcess } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir, uptime } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
|
|
import { readDaemonInstance, stopDaemonInstance } from "./daemon-instance.js";
|
|
import { acquirePidLock, getPidLockInfo, isLocked, type PidLockInfo } from "./pid-lock.js";
|
|
|
|
// A process cannot have started before the machine booted, so a lock stamped
|
|
// before this boot names a PID that some unrelated process now holds.
|
|
function bootedAt(): number {
|
|
return Date.now() - uptime() * 1000;
|
|
}
|
|
|
|
async function writeLock(paseoHome: string, lock: PidLockInfo): Promise<void> {
|
|
await writeFile(join(paseoHome, "paseo.pid"), JSON.stringify(lock));
|
|
}
|
|
|
|
function lockFor(pid: number, startedAt: Date): PidLockInfo {
|
|
return {
|
|
pid,
|
|
startedAt: startedAt.toISOString(),
|
|
hostname: "old-host",
|
|
uid: process.getuid?.() ?? 0,
|
|
listen: "127.0.0.1:6767",
|
|
desktopManaged: true,
|
|
heartbeat: true,
|
|
};
|
|
}
|
|
|
|
describe("daemon instance identity across a reboot", () => {
|
|
let paseoHome: string;
|
|
let bystander: ChildProcess | undefined;
|
|
|
|
beforeEach(async () => {
|
|
paseoHome = await mkdtemp(join(tmpdir(), "paseo-daemon-instance-"));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
bystander?.kill("SIGKILL");
|
|
bystander = undefined;
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
});
|
|
|
|
test("a lock stamped before this boot has no running owner", async () => {
|
|
await writeLock(paseoHome, lockFor(process.pid, new Date(bootedAt() - 60 * 60_000)));
|
|
|
|
expect(await readDaemonInstance(paseoHome)).toBeNull();
|
|
expect(await isLocked(paseoHome)).toMatchObject({ locked: false });
|
|
});
|
|
|
|
test("a supervisor started during this boot still holds the lock", async () => {
|
|
await writeLock(paseoHome, lockFor(process.pid, new Date()));
|
|
|
|
expect(await readDaemonInstance(paseoHome)).toMatchObject({ pid: process.pid });
|
|
expect(await isLocked(paseoHome)).toMatchObject({ locked: true });
|
|
});
|
|
|
|
test("a new supervisor takes over a lock stamped before this boot", async () => {
|
|
await writeLock(paseoHome, lockFor(process.pid, new Date(bootedAt() - 60 * 60_000)));
|
|
|
|
await acquirePidLock(paseoHome, null, { ownerPid: process.pid + 10_000 });
|
|
|
|
expect(await getPidLockInfo(paseoHome)).toMatchObject({ pid: process.pid + 10_000 });
|
|
});
|
|
|
|
test("stopping a lock stamped before this boot leaves the process holding that pid alone", async () => {
|
|
// Records delivery rather than dying of it, so a signal cannot be missed by arriving late.
|
|
const signalMarker = join(paseoHome, "bystander-signalled");
|
|
bystander = spawn(
|
|
process.execPath,
|
|
[
|
|
"-e",
|
|
`process.on("SIGTERM", () => require("node:fs").writeFileSync(${JSON.stringify(signalMarker)}, "SIGTERM"));` +
|
|
`setTimeout(() => {}, 120_000);`,
|
|
],
|
|
{ stdio: "ignore" },
|
|
);
|
|
const bystanderPid = bystander.pid;
|
|
if (bystanderPid === undefined) throw new Error("bystander process did not start");
|
|
let exited = false;
|
|
bystander.once("exit", () => {
|
|
exited = true;
|
|
});
|
|
|
|
await writeLock(paseoHome, lockFor(bystanderPid, new Date(bootedAt() - 60 * 60_000)));
|
|
|
|
expect(await stopDaemonInstance(paseoHome)).toMatchObject({ action: "not_running" });
|
|
|
|
expect(existsSync(signalMarker)).toBe(false);
|
|
expect(exited).toBe(false);
|
|
await expect(readFile(join(paseoHome, "paseo.pid"), "utf-8")).rejects.toThrow(/ENOENT/);
|
|
});
|
|
});
|