mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
b2048d46d1
* fix(process): retain unresolved descendants during lineage observation * fix(process): preserve lineage across ancestor generation changes * fix(process): avoid retaining replacements of exited candidates * test(cli): avoid unused cache writes in module trace acceptance * fix(process): retire replaced lineage candidates * fix(process): preserve uncertain owners and validate launcher scope
161 lines
6.0 KiB
TypeScript
161 lines
6.0 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
import { randomUUID } from "node:crypto";
|
|
import { Writable } from "node:stream";
|
|
import { expect, it } from "vitest";
|
|
|
|
import {
|
|
cleanupOwnedProcessGroup,
|
|
type PersistentOwnedProcessGroup,
|
|
type ProcessOwnershipHost,
|
|
} from "../../../src/process/ProcessOwnership.js";
|
|
import { ProcessOwnershipEvidenceOwner } from "../../../src/process/ProcessOwnershipEvidence.js";
|
|
import {
|
|
createSystemProcessOwnershipHost,
|
|
observeOwnedProcessLineage,
|
|
} from "../../../src/process/ProcessOwnershipObservation.js";
|
|
import { waitForDetachedProviderChild } from "../../fixtures/providerProcess.js";
|
|
|
|
const descendantSource = `
|
|
process.stdin.resume();
|
|
process.stdin.once('end', () => process.exit(0));
|
|
process.stdout.write('ready\\n');
|
|
`;
|
|
|
|
const launcherSource = `
|
|
const { spawn } = require('node:child_process');
|
|
const child = spawn(process.execPath, ['-e', ${JSON.stringify(descendantSource)}], {
|
|
detached: true,
|
|
stdio: [3, 'pipe', process.stderr],
|
|
});
|
|
child.once('error', () => process.exit(1));
|
|
child.stdout.once('data', () => {
|
|
process.stdout.write('ready:' + child.pid + '\\n');
|
|
});
|
|
child.unref();
|
|
process.stdin.once('data', () => process.exit(0));
|
|
`;
|
|
|
|
it.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
|
"retains a native descendant after identity denial, launcher exit and a later table omission",
|
|
async () => {
|
|
const native = createSystemProcessOwnershipHost(
|
|
process.platform,
|
|
process.env,
|
|
);
|
|
try {
|
|
await native.prepare?.();
|
|
const captureBaseline = await native.captureBaseline?.();
|
|
const runId = randomUUID();
|
|
const launcher = spawn(process.execPath, ["-e", launcherSource], {
|
|
detached: true,
|
|
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
|
|
// FD3 is the descendant's lifetime barrier; launcher stdin only exits
|
|
// the launcher. Its exit must not close the descendant's inherited pipe.
|
|
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
});
|
|
const launcherExited = new Promise<void>((resolve, reject) => {
|
|
launcher.once("exit", () => resolve());
|
|
launcher.once("error", reject);
|
|
});
|
|
launcher.stderr?.resume();
|
|
const barrier = launcher.stdio[3];
|
|
let ownership: PersistentOwnedProcessGroup | undefined;
|
|
try {
|
|
if (launcher.pid === undefined || !(barrier instanceof Writable))
|
|
throw new Error("Expected an owned launcher and descendant barrier");
|
|
ownership = {
|
|
runId,
|
|
leaderPid: launcher.pid,
|
|
processGroupId: launcher.pid,
|
|
expectedParentPid: process.pid,
|
|
sweepTokenOwnedProcesses: true,
|
|
evidenceOwner: new ProcessOwnershipEvidenceOwner(),
|
|
...(captureBaseline === undefined ? {} : { captureBaseline }),
|
|
};
|
|
const descendantPid = await waitForDetachedProviderChild(launcher);
|
|
const descendant = (await native.listProcesses()).find(
|
|
({ pid }) => pid === descendantPid,
|
|
);
|
|
expect(descendant).toMatchObject({
|
|
parentPid: launcher.pid,
|
|
processGroupId: descendantPid,
|
|
});
|
|
if (descendant === undefined || native.processIdentities === undefined)
|
|
throw new Error("Expected the real native descendant identity owner");
|
|
|
|
const deniedIdentityHost: ProcessOwnershipHost = {
|
|
...native,
|
|
processIdentities: async (rows, signal, effectiveUidPids) => {
|
|
const identities = new Map(
|
|
await native.processIdentities?.(rows, signal, effectiveUidPids),
|
|
);
|
|
if (rows.some(({ pid }) => pid === descendantPid))
|
|
identities.set(descendantPid, {
|
|
state: "unavailable",
|
|
reason: "controlled descendant identity denial",
|
|
});
|
|
return identities;
|
|
},
|
|
};
|
|
const lineage = await observeOwnedProcessLineage(
|
|
ownership,
|
|
deniedIdentityHost,
|
|
);
|
|
const retainedPids = ownership.evidenceOwner.candidates.map(
|
|
({ pid }) => pid,
|
|
);
|
|
|
|
launcher.stdin?.end("exit\n");
|
|
await launcherExited;
|
|
expect(launcher.exitCode).toBe(0);
|
|
expect(
|
|
(await native.processIdentities([descendant])).get(descendantPid),
|
|
).toMatchObject({
|
|
state: "readable",
|
|
lifecycle: { state: "observed", lifecycle: "non-zombie" },
|
|
});
|
|
|
|
const omittedHost: ProcessOwnershipHost = {
|
|
...native,
|
|
listProcesses: async (signal) =>
|
|
(await native.listProcesses(signal)).filter(
|
|
({ pid }) => pid !== descendantPid,
|
|
),
|
|
};
|
|
const omitted = await cleanupOwnedProcessGroup(ownership, omittedHost);
|
|
// The incomplete table cannot authorize signaling. It also cannot erase
|
|
// a retained native candidate and certify cleanup while it is alive.
|
|
expect(omitted).toMatchObject({ cleaned: false });
|
|
expect(lineage.status).toBe("unavailable");
|
|
expect(retainedPids).toContain(descendantPid);
|
|
expect(
|
|
(await native.processIdentities([descendant])).get(descendantPid),
|
|
).toMatchObject({
|
|
state: "readable",
|
|
lifecycle: { state: "observed", lifecycle: "non-zombie" },
|
|
});
|
|
|
|
// Restore the complete native table and retry the same retained owner.
|
|
// The run token, native generation and current group authorize cleanup.
|
|
await expect(
|
|
cleanupOwnedProcessGroup(ownership, native),
|
|
).resolves.toMatchObject({ cleaned: true, signaled: true });
|
|
expect(ownership.evidenceOwner.candidates).toEqual([]);
|
|
} finally {
|
|
if (barrier instanceof Writable) barrier.end();
|
|
if (launcher.exitCode === null && launcher.signalCode === null)
|
|
launcher.stdin?.end("exit\n");
|
|
await launcherExited;
|
|
if (ownership !== undefined)
|
|
expect(
|
|
await cleanupOwnedProcessGroup(ownership, native),
|
|
).toMatchObject({
|
|
cleaned: true,
|
|
});
|
|
}
|
|
} finally {
|
|
await native.close?.();
|
|
}
|
|
},
|
|
);
|