mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
* Keep reporting a Linux daemon as running after its VM resumes from a host sleep A VM paused while its host sleeps keeps its uptime, but its wall clock catches up on resume. The boot instant derived from the two then lands after the running supervisor's lock, so daemon status reported it as stopped and daemon stop released its lock without stopping it. The lock now records Linux's boot id, and a lock that carries one is judged by it. Locks without one keep the wall-clock check. Closes #6019 * Give the OMP idle-deadline test a deadline a slow runner cannot pass before the first wait With a 1 ms deadline, the gate's first check, made synchronously when the turn ends, could already be past it on a loaded Windows runner. The turn then failed before the test's first scheduler wait, and the test timed out. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
136 lines
5.1 KiB
TypeScript
136 lines
5.1 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, vi } 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/);
|
|
});
|
|
});
|
|
|
|
// Linux keeps a boot's id while a VM is paused for a host sleep, though the VM's wall clock
|
|
// runs ahead of its uptime once it resumes. A sleep longer than the time between boot and
|
|
// the supervisor's start puts the wall-clock boot instant after the lock's startedAt.
|
|
describe.runIf(process.platform === "linux")("daemon instance identity on Linux", () => {
|
|
let paseoHome: string;
|
|
|
|
beforeEach(async () => {
|
|
paseoHome = await mkdtemp(join(tmpdir(), "paseo-daemon-instance-linux-"));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.useRealTimers();
|
|
await rm(paseoHome, { recursive: true, force: true });
|
|
});
|
|
|
|
test("a supervisor still holds the lock after its paused VM resumes", async () => {
|
|
await acquirePidLock(paseoHome, null, { ownerPid: process.pid });
|
|
|
|
vi.useFakeTimers({ toFake: ["Date"] });
|
|
vi.setSystemTime(Date.now() + uptime() * 1000 + 12 * 60 * 60_000);
|
|
|
|
expect(await readDaemonInstance(paseoHome)).toMatchObject({ pid: process.pid });
|
|
expect(await isLocked(paseoHome)).toMatchObject({ locked: true });
|
|
await expect(
|
|
acquirePidLock(paseoHome, null, { ownerPid: process.pid + 10_000 }),
|
|
).rejects.toThrow("Another Paseo daemon is already running");
|
|
});
|
|
|
|
test("a lock written during another boot has no running owner", async () => {
|
|
await writeLock(paseoHome, {
|
|
...lockFor(process.pid, new Date()),
|
|
bootId: "00000000-0000-0000-0000-000000000000",
|
|
});
|
|
|
|
expect(await readDaemonInstance(paseoHome)).toBeNull();
|
|
expect(await isLocked(paseoHome)).toMatchObject({ locked: false });
|
|
});
|
|
});
|