Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cli/gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export function processStart(pid: number): string | undefined {
try {
return `${readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim()} ${startTime(pid)}`;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
if (["ENOENT", "ESRCH"].includes((error as NodeJS.ErrnoException).code ?? "")) return undefined;
throw error;
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/cli/supervise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export interface Owner {
start?: string;
}

/** Field 22 of /proc/<pid>/stat; the command name in parentheses may hold spaces. */
/**
* Field 22 of /proc/<pid>/stat; the command name in parentheses may hold spaces. Throws once the
* process is gone: ENOENT, or ESRCH when it exits mid-read.
*/
export function startTime(pid: number): string | undefined {
if (process.platform !== "linux") return undefined;
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
Expand All @@ -48,7 +51,7 @@ export function alive(owner: Owner): boolean {
try {
return startTime(owner.pid) === owner.start;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
if (["ENOENT", "ESRCH"].includes((error as NodeJS.ErrnoException).code ?? "")) return false;
throw error;
}
}
Expand Down
40 changes: 38 additions & 2 deletions test/gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
utimesSync,
writeFileSync,
} from "node:fs";
import { constants } from "node:os";
import { constants, hostname } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, test, vi } from "vitest";
Expand Down Expand Up @@ -42,9 +42,27 @@ import {
} from "../src/cli/gate";
import { forwards, foregroundStat, statProcess } from "../src/cli/interrupt";
import { gateBoundary, main } from "../src/cli/main";
import { questScope } from "../src/cli/supervise";
import { alive as ownerAlive, questScope } from "../src/cli/supervise";
import { created, onTerminal, run, signalled, tempDir } from "./fixture";

// Linux reports a /proc read that races a process's exit as ESRCH rather than ENOENT, so fail
// one process's stat read with either code, without timing.
const racing = vi.hoisted(() => ({ read: undefined as { path: string; code: string } | undefined }));
vi.mock("node:fs", async (importOriginal) => {
const fs = await importOriginal<typeof import("node:fs")>();
return {
...fs,
readFileSync: (...args: Parameters<typeof fs.readFileSync>) => {
if (racing.read && args[0] === racing.read.path) {
throw Object.assign(new Error(`${racing.read.code}: no such process, read`), {
code: racing.read.code,
});
}
return fs.readFileSync(...args);
},
};
});

const GiB = 2 ** 30;

/** How this process's gate records itself in an admission. */
Expand Down Expand Up @@ -1146,6 +1164,24 @@ function detached(dir: string, rest: string) {
};
}

describe.runIf(process.platform === "linux")("a process that exits while its stat is read", () => {
const pid = 4242;
const identity = { host: hostname(), pid, start: "1" };
afterEach(() => (racing.read = undefined));

test.each(["ENOENT", "ESRCH"])("reads as gone when the stat read fails with %s", (code) => {
racing.read = { path: `/proc/${pid}/stat`, code };
expect(processStart(pid)).toBeUndefined();
expect(ownerAlive(identity)).toBe(false);
});

test("surfaces any other read error", () => {
racing.read = { path: `/proc/${pid}/stat`, code: "EACCES" };
expect(() => processStart(pid)).toThrow("EACCES");
expect(() => ownerAlive(identity)).toThrow("EACCES");
});
});

describe("the Windows job stub", () => {
test("passes the exact exit status and restores the caller's environment", async () => {
const script =
Expand Down
Loading