1090 lines · 39.2 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * as NodeChildProcess from "node:child_process";import * as NodeFS from "node:fs";import * as NodePath from "node:path";import * as NodeURL from "node:url"; import { describe, expect, it } from "@effect/vitest";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Clock from "effect/Clock";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Scope from "effect/Scope"; import {  capturePosixOwnershipLedger,  isSafeCgroupPath,  makeLinuxCgroupController,  makePosixProcessTreeController,  observePosixOwnershipLedger,  observePosixOwnershipLedgerContinuously,  parseCgroup2Mount,  parseCgroup2Mounts,  parseUnifiedCgroupPath,  posixProcessIsZombie,  resolveLinuxCgroupTargetCommand,  signalLinuxCgroupRootTerm,  sweepStaleLinuxCgroupSiblings,  terminateLinuxCgroupLease,  terminatePosixOwnedProcessTree,  wrapCommandForLinuxCgroup,  type AcpOwnedPosixProcess,  type AcpLinuxCgroupLease,  type AcpPosixOwnershipRoot,  type AcpPosixProcessIdentity,  type AcpPosixProcessTreeController,} from "./AcpSessionRuntime.ts"; const threadSpawnHelperSource = NodeURL.fileURLToPath(  new URL("../../../scripts/acp-thread-spawn-helper.c", import.meta.url),); const identity = (  pid: number,  ppid: number,  pgid: number,  sid: number,  startTime = String(pid),  state = "S",): AcpPosixProcessIdentity => ({  executable: `/proc/${pid}/exe`,  pgid,  pid,  ppid,  sid,  startTime,  state,}); function makeController(input: {  readonly processes: ReadonlyArray<AcpPosixProcessIdentity>;  readonly onProcess?: (    processes: Map<number, AcpPosixProcessIdentity>,    pid: number,    signal: NodeJS.Signals,  ) => void;}) {  const processes = new Map(input.processes.map((process) => [process.pid, process]));  const signals: Array<string> = [];  const controller: AcpPosixProcessTreeController = {    childPidsOf: (pid) =>      [...processes.values()]        .filter((process) => process.ppid === pid)        .map((process) => process.pid),    childrenOf: (pid) => [...processes.values()].filter((process) => process.ppid === pid),    identity: (pid) => processes.get(pid),    snapshot: () => [...processes.values()],    signalProcess: (pid, signal) => {      signals.push(`process:${pid}:${signal}`);      if (input.onProcess) return input.onProcess(processes, pid, signal);      processes.delete(pid);    },  };  return { controller, processes, signals };} const server = () => identity(process.pid, 1, process.pid, process.pid, "server"); function processExists(pid: number): boolean {  try {    process.kill(pid, 0);    return true;  } catch {    return false;  }} describe("terminatePosixOwnedProcessTree", () => {  it("parses unified cgroup paths and escaped cgroup2 mountinfo", () => {    expect(parseUnifiedCgroupPath("0::/user.slice/app.scope\n")).toBe("/user.slice/app.scope");    expect(parseUnifiedCgroupPath("0::/one\n0::/two\n")).toBeUndefined();    expect(isSafeCgroupPath("/user.slice/app.scope")).toBe(true);    expect(isSafeCgroupPath("/user.slice/../escape")).toBe(false);    expect(isSafeCgroupPath("/user.slice/app.scope (deleted)")).toBe(false);    expect(      parseCgroup2Mount(        "42 31 0:37 /user.slice /sys/fs/cgroup/user\\040mount rw - cgroup2 cgroup2 rw\n",      ),    ).toEqual({ mountPoint: "/sys/fs/cgroup/user mount", root: "/user.slice" });    expect(parseCgroup2Mount("42 31 0:37 / /sys/fs/cgroup rw - tmpfs tmpfs rw\n")).toBeUndefined();    expect(      parseCgroup2Mounts(        "41 31 0:36 /other /sys/fs/cgroup/other rw - cgroup2 cgroup2 rw\n" +          "42 31 0:37 /user.slice /sys/fs/cgroup/user rw - cgroup2 cgroup2 rw\n",      ),    ).toHaveLength(2);  });   it.live("sweeps empty t3-acp sibling leases owned by dead pids only", () =>    Effect.sync(() => {      const scratchRoot = NodePath.join(process.cwd(), "tmp");      NodeFS.mkdirSync(scratchRoot, { recursive: true });      const parent = NodeFS.mkdtempSync(NodePath.join(scratchRoot, "acp-cgroup-stale-siblings-"));      // Temp dirs are not cgroupfs: rmdir fails when control files exist, so the      // test injects populated/remove while still exercising the real selection      // and path-safety logic against a real parent directory tree.      const populatedByPath = new Map<string, "0" | "1">();      const writeSibling = (name: string, populated: "0" | "1" | null) => {        const path = NodePath.join(parent, name);        NodeFS.mkdirSync(path);        if (populated !== null) populatedByPath.set(path, populated);        return path;      };      const deadPid = 2_147_483_646;      // Synthetic live foreign pid (not this process, still reported alive by the probe).      const liveForeignPid = 2_147_483_645;      const staleEmpty = writeSibling(`t3-acp-${deadPid}-aaaa`, "0");      const currentPidSibling = writeSibling(`t3-acp-${process.pid}-bbbb`, "0");      const liveForeignSibling = writeSibling(`t3-acp-${liveForeignPid}-cccc`, "0");      const stalePopulated = writeSibling(`t3-acp-${deadPid}-dddd`, "1");      const unrelated = writeSibling("other-lease", "0");      const isProcessAlive = (pid: number) => pid === process.pid || pid === liveForeignPid;      try {        sweepStaleLinuxCgroupSiblings(parent, {          isProcessAlive,          readPopulated: (path) => populatedByPath.get(path),          remove: (path) => {            populatedByPath.delete(path);            NodeFS.rmdirSync(path);          },        });        expect(NodeFS.existsSync(staleEmpty)).toBe(false);        expect(NodeFS.existsSync(currentPidSibling)).toBe(true);        expect(NodeFS.existsSync(liveForeignSibling)).toBe(true);        expect(NodeFS.existsSync(stalePopulated)).toBe(true);        expect(NodeFS.existsSync(unrelated)).toBe(true);        // Missing populated state must not remove a sibling.        writeSibling(`t3-acp-${deadPid}-eeee`, null);        sweepStaleLinuxCgroupSiblings(parent, {          isProcessAlive,          readPopulated: (path) => populatedByPath.get(path),          remove: (path) => {            populatedByPath.delete(path);            NodeFS.rmdirSync(path);          },        });        expect(NodeFS.existsSync(NodePath.join(parent, `t3-acp-${deadPid}-eeee`))).toBe(true);        expect(NodeFS.existsSync(currentPidSibling)).toBe(true);      } finally {        NodeFS.rmSync(parent, { recursive: true, force: true });      }    }),  );   it.live("preserves exact argv and strips wrapper-only environment before exec", () =>    Effect.gen(function* () {      if ((yield* HostProcessPlatform) !== "linux") return;      const lease = makeLinuxCgroupController().create();      if (lease === undefined) return;      const scratchRoot = NodePath.join(process.cwd(), "tmp");      NodeFS.mkdirSync(scratchRoot, { recursive: true });      const scratch = NodeFS.mkdtempSync(NodePath.join(scratchRoot, "acp-cgroup-wrapper-"));      const linkedNode = NodePath.join(scratch, "T3 Code AppImage 'quoted'");      const bareGrok = NodePath.join(scratch, "grok");      const relativeBin = NodePath.join(scratch, "relative-bin");      const directoryBin = NodePath.join(scratch, "directory-bin");      const validBin = NodePath.join(scratch, "valid-bin");      const outputPath = NodePath.join(scratch, "argv.json");      NodeFS.mkdirSync(relativeBin);      NodeFS.mkdirSync(NodePath.join(directoryBin, "grok"), { recursive: true });      NodeFS.mkdirSync(validBin);      NodeFS.symlinkSync(process.execPath, linkedNode);      NodeFS.symlinkSync(process.execPath, bareGrok);      NodeFS.symlinkSync(process.execPath, NodePath.join(relativeBin, "grok"));      NodeFS.symlinkSync(process.execPath, NodePath.join(validBin, "grok"));      expect(resolveLinuxCgroupTargetCommand("grok", scratch, { PATH: scratch })).toBe(bareGrok);      expect(resolveLinuxCgroupTargetCommand("grok", scratch, { PATH: "" })).toBe(bareGrok);      // Undefined PATH uses Node's default search path (/usr/bin:/bin), not cwd alone      // (bareGrok lives only in cwd / PATH=scratch).      expect(resolveLinuxCgroupTargetCommand("grok", scratch, { PATH: undefined })).toBeUndefined();      expect(resolveLinuxCgroupTargetCommand("node", scratch, { PATH: undefined })).toBeDefined();      expect(resolveLinuxCgroupTargetCommand("grok", scratch, { PATH: "relative-bin" })).toBe(        NodePath.join(relativeBin, "grok"),      );      expect(        resolveLinuxCgroupTargetCommand("grok", scratch, {          PATH: `directory-bin${NodePath.delimiter}valid-bin`,        }),      ).toBe(NodePath.join(validBin, "grok"));      const target = [        'const fs = require("node:fs");',        "fs.writeFileSync(process.argv[1], JSON.stringify({",        "  args: process.argv.slice(2),",        "  electron: process.env.ELECTRON_RUN_AS_NODE,",        "  wrapper: process.env.T3_ACP_CGROUP_WRAPPER,",        "}));",      ].join("\n");      const wrapped = wrapCommandForLinuxCgroup(lease, linkedNode, [        "-e",        target,        outputPath,        "space value",        "single'quote",        'double"quote',      ]);      try {        const result = NodeChildProcess.spawnSync(wrapped.command, wrapped.args, {          encoding: "utf8",          env: {            ...process.env,            ELECTRON_RUN_AS_NODE: "1",            T3_ACP_CGROUP_WRAPPER: "1",          },        });        expect(result.status, result.stderr).toBe(0);        expect(NodeFS.readFileSync(outputPath, "utf8")).toBe(          '{"args":["space value","single\'quote","double\\"quote"]}',        );      } finally {        yield* terminateLinuxCgroupLease(lease).pipe(Effect.ignore);        NodeFS.rmSync(scratch, { recursive: true, force: true });      }    }),  );   it.live("kills a post-TERM detached double fork without touching an unrelated sentinel", () =>    Effect.gen(function* () {      if ((yield* HostProcessPlatform) !== "linux") return;      const controller = makeLinuxCgroupController();      const lease = controller.create();      if (lease === undefined) return;      expect(parseUnifiedCgroupPath(NodeFS.readFileSync("/proc/self/cgroup", "utf8"))).not.toBe(        lease.relativePath,      );      const scratchRoot = NodePath.join(process.cwd(), "tmp");      NodeFS.mkdirSync(scratchRoot, { recursive: true });      const scratch = NodeFS.mkdtempSync(NodePath.join(scratchRoot, "acp-cgroup-kill-"));      const readyPath = NodePath.join(scratch, "ready");      const childPath = NodePath.join(scratch, "child");      const childProgram = "setInterval(() => {}, 1000);";      const launcherProgram = [        'const fs = require("node:fs");',        'const cp = require("node:child_process");',        "const child = cp.spawn(process.execPath, ['-e', process.argv[2]], { detached: true, stdio: 'ignore' });",        "fs.writeFileSync(process.argv[1], String(child.pid));",        "child.unref();",      ].join("\n");      const providerProgram = [        'const fs = require("node:fs");',        'const cp = require("node:child_process");',        "fs.writeFileSync(process.argv[1], String(process.pid));",        "process.on('SIGTERM', () => {",        "  const launcher = cp.spawn(process.execPath, ['-e', process.argv[3], process.argv[2], process.argv[4]], { detached: true, stdio: 'ignore' });",        "  launcher.unref();",        "  process.exit(0);",        "});",        "setInterval(() => {}, 1000);",      ].join("\n");      const wrapped = wrapCommandForLinuxCgroup(lease, process.execPath, [        "-e",        providerProgram,        readyPath,        childPath,        launcherProgram,        childProgram,      ]);      const provider = NodeChildProcess.spawn(wrapped.command, wrapped.args, {        detached: true,        env: { ...process.env, ELECTRON_RUN_AS_NODE: "1", T3_ACP_CGROUP_WRAPPER: "1" },        stdio: "ignore",      });      provider.unref();      const sentinel = NodeChildProcess.spawn(process.execPath, ["-e", childProgram], {        detached: true,        stdio: "ignore",      });      sentinel.unref();      let detachedChildPid: number | undefined;      try {        yield* Effect.gen(function* () {          while (!NodeFS.existsSync(readyPath)) yield* Effect.sleep("10 millis");        }).pipe(Effect.timeout("2 seconds"));        process.kill(provider.pid!, "SIGTERM");        detachedChildPid = yield* Effect.gen(function* () {          while (true) {            try {              const pid = Number(NodeFS.readFileSync(childPath, "utf8"));              if (Number.isSafeInteger(pid) && pid > 1) return pid;            } catch {              // The TERM-time launcher has not published its child yet.            }            yield* Effect.sleep("10 millis");          }        }).pipe(Effect.timeout("2 seconds"));        expect(processExists(detachedChildPid)).toBe(true);        yield* terminateLinuxCgroupLease(lease);        expect(processExists(sentinel.pid!)).toBe(true);        yield* Effect.gen(function* () {          while (processExists(detachedChildPid!)) yield* Effect.sleep("10 millis");        }).pipe(Effect.timeout("2 seconds"));        expect(NodeFS.existsSync(lease.path)).toBe(false);         const replacement = controller.create();        expect(replacement?.path).not.toBe(lease.path);        if (replacement !== undefined) {          NodeFS.mkdirSync(NodePath.join(replacement.path, "nested"));          yield* terminateLinuxCgroupLease(replacement);          expect(NodeFS.existsSync(replacement.path)).toBe(false);        }      } finally {        yield* terminateLinuxCgroupLease(lease).pipe(Effect.ignore);        try {          process.kill(-provider.pid!, "SIGKILL");        } catch {          // The contained provider already exited.        }        try {          process.kill(-sentinel.pid!, "SIGKILL");        } catch {          // The unrelated sentinel already exited.        }        if (detachedChildPid !== undefined && processExists(detachedChildPid)) {          try {            process.kill(detachedChildPid, "SIGKILL");          } catch {            // The detached child already exited.          }        }        NodeFS.rmSync(scratch, { recursive: true, force: true });      }    }),  );   it.live("re-kills repopulated cgroups and verifies root removal after retryable errors", () =>    Effect.gen(function* () {      let exists = true;      let killCalls = 0;      let populated = false;      let removeCalls = 0;      const lease: AcpLinuxCgroupLease = {        contains: () => false,        exists: () => exists,        path: "/test/t3-acp-repopulation",        relativePath: "/test/t3-acp-repopulation",        kill: () => {          killCalls += 1;          populated = false;        },        populated: () => populated,        remove: () => {          removeCalls += 1;          if (removeCalls === 1) {            populated = true;            const error = new Error("repopulated") as NodeJS.ErrnoException;            error.code = "EBUSY";            throw error;          }          if (removeCalls === 2) {            const error = new Error("nested path vanished") as NodeJS.ErrnoException;            error.code = "ENOENT";            throw error;          }          exists = false;        },      };       yield* terminateLinuxCgroupLease(lease);      expect(killCalls).toBeGreaterThanOrEqual(3);      expect(removeCalls).toBe(3);      expect(exists).toBe(false);      yield* terminateLinuxCgroupLease(lease);      expect(removeCalls).toBe(3);    }),  );   it.live("reports persistent retryable removal failures as removal failures", () =>    Effect.gen(function* () {      const removeError = Object.assign(new Error("cgroup remains busy"), {        code: "EBUSY",      });      const lease: AcpLinuxCgroupLease = {        contains: () => false,        exists: () => true,        path: "/test/t3-acp-busy",        relativePath: "/test/t3-acp-busy",        kill: () => undefined,        populated: () => false,        remove: () => {          throw removeError;        },      };       const error = yield* Effect.flip(terminateLinuxCgroupLease(lease));      expect(error.detail).toBe("Failed to remove ACP cgroup /test/t3-acp-busy");      expect(error.cause).toBe(removeError);    }),  );   it("sends TERM only to the exact captured root while it remains in the child cgroup", () => {    const fixture = makeController({      processes: [server(), identity(100, process.pid, 100, 100, "owned")],    });    const ownedRoot: AcpOwnedPosixProcess = {      ...identity(100, process.pid, 100, 100, "owned"),      parentExecutable: server().executable,      parentStartTime: server().startTime,    };    const root: AcpPosixOwnershipRoot = { captureAttempted: true, value: ownedRoot };    const lease: AcpLinuxCgroupLease = {      contains: () => true,      exists: () => true,      path: "/test/t3-acp-root",      relativePath: "/test/t3-acp-root",      kill: () => undefined,      populated: () => true,      remove: () => undefined,    };    signalLinuxCgroupRootTerm({ controller: fixture.controller, lease, root });    expect(fixture.signals).toEqual(["process:100:SIGTERM"]);     const reused = makeController({      processes: [server(), identity(100, process.pid, 100, 100, "reused")],    });    signalLinuxCgroupRootTerm({ controller: reused.controller, lease, root });    expect(reused.signals).toEqual([]);     const migrated = makeController({      processes: [server(), identity(100, process.pid, 100, 100, "owned")],    });    signalLinuxCgroupRootTerm({      controller: migrated.controller,      lease: { ...lease, contains: () => false },      root,    });    expect(migrated.signals).toEqual([]);  });   it.live("finds a child forked by a non-leader pthread", () =>    Effect.gen(function* () {      if ((yield* HostProcessPlatform) !== "linux") return;      const scratchRoot = NodePath.join(process.cwd(), "tmp");      NodeFS.mkdirSync(scratchRoot, { recursive: true });      const scratch = NodeFS.mkdtempSync(NodePath.join(scratchRoot, "acp-pthread-spawn-"));      const binary = NodePath.join(scratch, "helper");      const pidPath = NodePath.join(scratch, "pids");      expect(NodeFS.statSync(threadSpawnHelperSource).isFile()).toBe(true);      const compile = NodeChildProcess.spawnSync(        "cc",        ["-pthread", threadSpawnHelperSource, "-o", binary],        {          encoding: "utf8",        },      );      if ((compile.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {        NodeFS.rmSync(scratch, { recursive: true, force: true });        return;      }      expect(compile.status, compile.stderr).toBe(0);      const helper = NodeChildProcess.spawn(binary, [pidPath], { detached: true, stdio: "ignore" });      helper.unref();      try {        const published = yield* Effect.gen(function* () {          while (true) {            try {              const ids = NodeFS.readFileSync(pidPath, "utf8").trim().split(/\s+/).map(Number);              if (ids.length === 2 && ids.every(Number.isSafeInteger)) return ids;            } catch {              // The worker has not published its TID and child yet.            }            yield* Effect.sleep("10 millis");          }        }).pipe(Effect.timeout("2 seconds"));        const [workerTid, childPid] = published;        expect(workerTid).not.toBe(helper.pid);        expect(makePosixProcessTreeController("linux").childrenOf(helper.pid!)).toContainEqual(          expect.objectContaining({ pid: childPid, ppid: helper.pid }),        );      } finally {        try {          process.kill(-helper.pid!, "SIGKILL");        } catch {

Lines 1–500 of 1090.