mirror of
https://github.com/morluto/rea.git
synced 2026-11-02 21:06:04 +00:00
fix(process): preserve partial evidence and verify detached cleanup (#1011)
* test(ci): allow transport delivery in source identity regression * fix(process): preserve partial evidence and verify detached cleanup * fix(process): scope Linux token sweeps by stable process identity * fix(process): retain environment permission failure codes * fix(process): wait for signaled processes before verifying cleanup * fix(process): verify sampled detached groups during cleanup * fix(electron): preserve prelaunch identities through cleanup * fix(process): retry growing Darwin process table snapshots * fix(process): retain compatibility with saved v3 capture evidence * fix(process): migrate saved v3 captures during reconstruction verification * fix(process): preserve cancellation during Darwin helper compilation * fix(process): propagate cancellation through ownership baselines * perf(javascript): cache canonical export comparison sort keys * fix(process): cancel provider ownership preparation before spawn * fix(process): isolate native compiler waiter cancellation
This commit is contained in:
1 parent
63ba7424c0
commit
63cf28fd8f
51 files changed
+5506
-316
No files matched your search
+369
-35
@@ -2,8 +2,8 @@ import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import {
|
||||
createSystemProcessOwnershipHost,
|
||||
errorMessage,
|
||||
systemProcessOwnershipHost as systemHost,
|
||||
} from "./ProcessOwnershipObservation.js";
|
||||
import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js";
|
||||
import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js";
|
||||
@@ -21,8 +21,21 @@ export interface OwnedProcessGroup {
|
||||
readonly expectedParentPid?: number;
|
||||
/** Scan all live processes for this run token during cleanup. */
|
||||
readonly sweepTokenOwnedProcesses?: boolean;
|
||||
/** Prelaunch process identities for this capture; absent on generic callers. */
|
||||
readonly captureBaseline?: ProcessOwnershipBaseline;
|
||||
/**
|
||||
* Groups observed in capture samples. Observation adds cleanup candidates but
|
||||
* does not authorize signaling; every live member still needs token proof.
|
||||
*/
|
||||
readonly sampledProcessGroupIds?: readonly number[];
|
||||
}
|
||||
|
||||
/** Capture-local baseline; null means the PID identity could not be established. */
|
||||
export type ProcessOwnershipBaseline = readonly {
|
||||
readonly pid: number;
|
||||
readonly identity: string | null;
|
||||
}[];
|
||||
|
||||
/** One entry from an operating-system process-table snapshot. */
|
||||
export interface ProcessTableEntry {
|
||||
readonly pid: number;
|
||||
@@ -32,6 +45,11 @@ export interface ProcessTableEntry {
|
||||
readonly command: string;
|
||||
}
|
||||
|
||||
/** Native process start identity; unavailable identities must never be skipped. */
|
||||
export type ProcessIdentityObservation =
|
||||
| { readonly state: "readable"; readonly identity: string }
|
||||
| { readonly state: "unavailable"; readonly reason: string };
|
||||
|
||||
/** Token-verified process lineage retained for one owned provider run. */
|
||||
export interface OwnedProcessLineage {
|
||||
readonly runId: string;
|
||||
@@ -64,11 +82,32 @@ export type ProcessLineageObservation =
|
||||
/** Narrow operating-system seam used to inspect processes and signal groups. */
|
||||
export interface ProcessOwnershipHost {
|
||||
readonly platform?: NodeJS.Platform;
|
||||
listProcesses(): Promise<readonly ProcessTableEntry[]>;
|
||||
/** Prepare required native identity inspection before launching a child. */
|
||||
prepare?(signal?: AbortSignal): Promise<void>;
|
||||
/** Release owned native inspection helpers and their temporary files. */
|
||||
close?(): Promise<void>;
|
||||
/** List current processes; startup inspection may be cancelled by the caller. */
|
||||
listProcesses(signal?: AbortSignal): Promise<readonly ProcessTableEntry[]>;
|
||||
environment(pid: number): Promise<Readonly<Record<string, string>>>;
|
||||
/** Read only run-token values in one host operation when the OS supports it. */
|
||||
runTokens?(
|
||||
processes: readonly ProcessTableEntry[],
|
||||
): Promise<ReadonlyMap<number, ProcessRunTokenObservation>>;
|
||||
/** Read stable identities; omit the signal during cleanup so it can finish. */
|
||||
processIdentities?(
|
||||
processes: readonly ProcessTableEntry[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<ReadonlyMap<number, ProcessIdentityObservation>>;
|
||||
/** Snapshot identities before launch, honoring startup cancellation. */
|
||||
captureBaseline?(signal?: AbortSignal): Promise<ProcessOwnershipBaseline>;
|
||||
signalGroup(processGroupId: number, signal: NodeJS.Signals): void;
|
||||
}
|
||||
|
||||
/** Whether one live process exposed its ownership token to the host. */
|
||||
export type ProcessRunTokenObservation =
|
||||
| { readonly state: "readable"; readonly runId: string | undefined }
|
||||
| { readonly state: "unavailable"; readonly reason: string };
|
||||
|
||||
/** Narrow Windows P0 seam for bounded process-tree termination. */
|
||||
export interface WindowsProcessTreeHost {
|
||||
terminateTree(rootPid: number): Promise<"terminated" | "missing">;
|
||||
@@ -77,7 +116,10 @@ export interface WindowsProcessTreeHost {
|
||||
/** Per-member reason that token-verified cleanup failed closed. */
|
||||
interface ProcessOwnershipValidationFailure {
|
||||
readonly pid: number;
|
||||
readonly reason: "environment-unreadable" | "run-token-mismatch";
|
||||
readonly reason:
|
||||
| "environment-unreadable"
|
||||
| "run-token-mismatch"
|
||||
| "process-identity-unavailable";
|
||||
readonly diagnostic?: string;
|
||||
}
|
||||
|
||||
@@ -89,7 +131,10 @@ export type ProcessCleanupResult =
|
||||
readonly reason: string;
|
||||
readonly failures?: readonly {
|
||||
readonly pid: number;
|
||||
readonly reason: "environment-unreadable" | "run-token-mismatch";
|
||||
readonly reason:
|
||||
| "environment-unreadable"
|
||||
| "run-token-mismatch"
|
||||
| "process-identity-unavailable";
|
||||
readonly diagnostic?: string;
|
||||
}[];
|
||||
};
|
||||
@@ -128,8 +173,6 @@ export const cleanupWindowsProcessTree = async (
|
||||
}
|
||||
};
|
||||
|
||||
const systemHost = createSystemProcessOwnershipHost();
|
||||
|
||||
const systemWindowsProcessTreeHost: WindowsProcessTreeHost = {
|
||||
async terminateTree(rootPid) {
|
||||
try {
|
||||
@@ -181,6 +224,7 @@ export const cleanupOwnedProcessGroup = async (
|
||||
processGroupId,
|
||||
ownership,
|
||||
host,
|
||||
plan.tokenOwnedIdentities,
|
||||
);
|
||||
if ("cleaned" in revalidation) return revalidation;
|
||||
if (revalidation.empty) continue;
|
||||
@@ -197,6 +241,8 @@ export const cleanupOwnedProcessGroup = async (
|
||||
};
|
||||
}
|
||||
}
|
||||
if (plan.unresolved.length > 0)
|
||||
return cleanupValidationFailure(plan.unresolved);
|
||||
return { cleaned: true, signaled };
|
||||
};
|
||||
|
||||
@@ -204,6 +250,7 @@ export const cleanupOwnedProcessGroup = async (
|
||||
export const verifyNoTokenOwnedProcesses = async (
|
||||
runId: string,
|
||||
host: ProcessOwnershipHost = systemHost,
|
||||
captureBaseline?: ProcessOwnershipBaseline,
|
||||
): Promise<ProcessCleanupResult> => {
|
||||
const processTable = await readLiveProcessTable(host);
|
||||
if (!processTable.available)
|
||||
@@ -215,6 +262,7 @@ export const verifyNoTokenOwnedProcesses = async (
|
||||
processTable.processes,
|
||||
runId,
|
||||
host,
|
||||
captureBaseline,
|
||||
);
|
||||
if (scan.failures.length > 0) return cleanupValidationFailure(scan.failures);
|
||||
return scan.owned.length === 0
|
||||
@@ -243,6 +291,8 @@ const readLiveProcessTable = async (
|
||||
|
||||
interface OwnedCleanupPlan {
|
||||
readonly signalOrder: readonly number[];
|
||||
readonly tokenOwnedIdentities: ReadonlyMap<number, string | null>;
|
||||
readonly unresolved: readonly ProcessOwnershipValidationFailure[];
|
||||
}
|
||||
|
||||
const createOwnedCleanupPlan = async (
|
||||
@@ -250,12 +300,21 @@ const createOwnedCleanupPlan = async (
|
||||
processes: readonly ProcessTableEntry[],
|
||||
host: ProcessOwnershipHost,
|
||||
): Promise<OwnedCleanupPlan | ProcessCleanupResult> => {
|
||||
const sampledProcessGroupIds = new Set(
|
||||
(ownership.sampledProcessGroupIds ?? []).filter(
|
||||
(processGroupId) =>
|
||||
Number.isSafeInteger(processGroupId) && processGroupId > 0,
|
||||
),
|
||||
);
|
||||
const tokenOwned =
|
||||
ownership.sweepTokenOwnedProcesses === true
|
||||
? await scanTokenOwnedProcesses(processes, ownership.runId, host)
|
||||
: { owned: [], failures: [] };
|
||||
if (tokenOwned.failures.length > 0)
|
||||
return cleanupValidationFailure(tokenOwned.failures);
|
||||
? await scanTokenOwnedProcesses(
|
||||
processes,
|
||||
ownership.runId,
|
||||
host,
|
||||
ownership.captureBaseline,
|
||||
)
|
||||
: { owned: [], failures: [], identities: new Map() };
|
||||
const tokenOwnedGroupIds = new Set(
|
||||
tokenOwned.owned.map(({ processGroupId }) => processGroupId),
|
||||
);
|
||||
@@ -266,9 +325,18 @@ const createOwnedCleanupPlan = async (
|
||||
if (
|
||||
launcher === undefined &&
|
||||
rootMembers.length === 0 &&
|
||||
tokenOwned.owned.length === 0
|
||||
tokenOwned.owned.length === 0 &&
|
||||
!processes.some(({ processGroupId }) =>
|
||||
sampledProcessGroupIds.has(processGroupId),
|
||||
)
|
||||
)
|
||||
return { cleaned: true, signaled: false };
|
||||
return tokenOwned.failures.length > 0
|
||||
? {
|
||||
signalOrder: [],
|
||||
tokenOwnedIdentities: tokenOwned.identities,
|
||||
unresolved: tokenOwned.failures,
|
||||
}
|
||||
: { cleaned: true, signaled: false };
|
||||
let descendants: readonly ProcessTableEntry[] = [];
|
||||
if (launcher !== undefined) {
|
||||
const identityFailure = launcherIdentityFailure(launcher, ownership);
|
||||
@@ -282,6 +350,11 @@ const createOwnedCleanupPlan = async (
|
||||
processGroupIds.add(descendant.processGroupId);
|
||||
for (const process of tokenOwned.owned)
|
||||
processGroupIds.add(process.processGroupId);
|
||||
for (const processGroupId of sampledProcessGroupIds)
|
||||
processGroupIds.add(processGroupId);
|
||||
const signalableGroups = new Set<number>();
|
||||
const unresolved = [...tokenOwned.failures];
|
||||
const unverifiableSampledGroups = new Set<number>();
|
||||
for (const processGroupId of processGroupIds) {
|
||||
if (processGroupId === ownership.processGroupId) continue;
|
||||
const groupLeader = processes.find(
|
||||
@@ -290,29 +363,69 @@ const createOwnedCleanupPlan = async (
|
||||
);
|
||||
if (
|
||||
(groupLeader === undefined || !descendantPids.has(groupLeader.pid)) &&
|
||||
!tokenOwnedGroupIds.has(processGroupId)
|
||||
!tokenOwnedGroupIds.has(processGroupId) &&
|
||||
!sampledProcessGroupIds.has(processGroupId)
|
||||
)
|
||||
return {
|
||||
cleaned: false,
|
||||
reason:
|
||||
"descendant process-group leader identity could not be verified",
|
||||
};
|
||||
if (
|
||||
(groupLeader === undefined || !descendantPids.has(groupLeader.pid)) &&
|
||||
!tokenOwnedGroupIds.has(processGroupId)
|
||||
)
|
||||
unverifiableSampledGroups.add(processGroupId);
|
||||
}
|
||||
const liveMembers = processes.filter(({ processGroupId }) =>
|
||||
processGroupIds.has(processGroupId),
|
||||
);
|
||||
const failures = await processOwnershipFailures(
|
||||
liveMembers,
|
||||
ownership.runId,
|
||||
host,
|
||||
);
|
||||
if (failures.length > 0) return cleanupValidationFailure(failures);
|
||||
for (const processGroupId of processGroupIds) {
|
||||
const members = liveMembers.filter(
|
||||
({ processGroupId: observedGroupId }) =>
|
||||
observedGroupId === processGroupId,
|
||||
);
|
||||
if (members.length === 0) continue;
|
||||
const failures = await processOwnershipFailures(
|
||||
members,
|
||||
ownership.runId,
|
||||
host,
|
||||
tokenOwned.identities,
|
||||
);
|
||||
if (failures.length === 0 && unverifiableSampledGroups.has(processGroupId))
|
||||
unresolved.push(
|
||||
...members.map(({ pid }) => ({
|
||||
pid,
|
||||
reason: "process-identity-unavailable" as const,
|
||||
diagnostic:
|
||||
"sampled process-group leader identity could not be verified",
|
||||
})),
|
||||
);
|
||||
const scanFailures = tokenOwned.failures.filter(({ pid }) =>
|
||||
members.some((member) => member.pid === pid),
|
||||
);
|
||||
if (failures.length > 0) unresolved.push(...failures);
|
||||
if (
|
||||
failures.length === 0 &&
|
||||
scanFailures.length === 0 &&
|
||||
!unverifiableSampledGroups.has(processGroupId)
|
||||
)
|
||||
signalableGroups.add(processGroupId);
|
||||
}
|
||||
if (ownership.sweepTokenOwnedProcesses !== true && unresolved.length > 0)
|
||||
return cleanupValidationFailure(unresolved);
|
||||
return {
|
||||
signalOrder: [ownership.processGroupId].concat(
|
||||
[...processGroupIds]
|
||||
.filter((processGroupId) => processGroupId !== ownership.processGroupId)
|
||||
.sort((left, right) => left - right),
|
||||
),
|
||||
signalOrder: [...signalableGroups]
|
||||
.filter((groupId) => processGroupIds.has(groupId))
|
||||
.sort((left, right) =>
|
||||
left === ownership.processGroupId
|
||||
? -1
|
||||
: right === ownership.processGroupId
|
||||
? 1
|
||||
: left - right,
|
||||
),
|
||||
tokenOwnedIdentities: tokenOwned.identities,
|
||||
unresolved,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -320,6 +433,7 @@ const revalidateOwnedProcessGroup = async (
|
||||
processGroupId: number,
|
||||
ownership: OwnedProcessGroup,
|
||||
host: ProcessOwnershipHost,
|
||||
expectedIdentities: ReadonlyMap<number, string | null>,
|
||||
): Promise<{ readonly empty: boolean } | ProcessCleanupResult> => {
|
||||
let members: readonly ProcessTableEntry[];
|
||||
try {
|
||||
@@ -337,6 +451,7 @@ const revalidateOwnedProcessGroup = async (
|
||||
members,
|
||||
ownership.runId,
|
||||
host,
|
||||
expectedIdentities,
|
||||
);
|
||||
if (failures.length > 0) return cleanupValidationFailure(failures);
|
||||
if (processGroupId === ownership.processGroupId) {
|
||||
@@ -363,20 +478,102 @@ const revalidateOwnedProcessGroup = async (
|
||||
|
||||
const cleanupValidationFailure = (
|
||||
failures: readonly ProcessOwnershipValidationFailure[],
|
||||
): ProcessCleanupResult => ({
|
||||
cleaned: false,
|
||||
reason: failures.some(({ reason }) => reason === "run-token-mismatch")
|
||||
? "process tree contains an unowned or PID-reused process"
|
||||
: "process ownership could not be revalidated",
|
||||
failures,
|
||||
});
|
||||
): ProcessCleanupResult => {
|
||||
const unreadable = failures.filter(
|
||||
({ reason }) => reason === "environment-unreadable",
|
||||
);
|
||||
if (failures.some(({ reason }) => reason === "run-token-mismatch"))
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: "process tree contains an unowned or PID-reused process",
|
||||
failures,
|
||||
};
|
||||
if (failures.some(({ reason }) => reason === "process-identity-unavailable"))
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: "process identity could not be revalidated",
|
||||
failures,
|
||||
};
|
||||
if (unreadable.length > 0) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const { diagnostic } of unreadable) {
|
||||
const category = sanitizedTokenReadFailure(diagnostic);
|
||||
counts.set(category, (counts.get(category) ?? 0) + 1);
|
||||
}
|
||||
const breakdown = [...counts]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([category, count]) => `${category}=${String(count)}`)
|
||||
.join(", ");
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: `process ownership token could not be read for ${String(unreadable.length)} live process(es): ${breakdown}`,
|
||||
failures,
|
||||
};
|
||||
}
|
||||
return {
|
||||
cleaned: false,
|
||||
reason: "process ownership could not be revalidated",
|
||||
failures,
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizedTokenReadFailure = (diagnostic: string | undefined): string => {
|
||||
if (diagnostic === "environment_unavailable") return diagnostic;
|
||||
const environmentErrno = /^(EACCES|EPERM|ENOENT|ESRCH):/u.exec(
|
||||
diagnostic ?? "",
|
||||
);
|
||||
if (environmentErrno?.[1] !== undefined)
|
||||
return `environment_errno_${environmentErrno[1]}`;
|
||||
if (diagnostic === "apple_vector_unavailable") return diagnostic;
|
||||
if (diagnostic === "ambiguous_environment_boundary") return diagnostic;
|
||||
if (diagnostic === "malformed_procargs") return diagnostic;
|
||||
if (diagnostic === "process_identity_changed_during_token_read")
|
||||
return diagnostic;
|
||||
if (diagnostic === "process_not_in_snapshot") return diagnostic;
|
||||
const processTableFailure = /^process_table_failed_(\d+)$/u.exec(
|
||||
diagnostic ?? "",
|
||||
);
|
||||
if (processTableFailure?.[1] !== undefined)
|
||||
return `process_table_errno_${processTableFailure[1]}`;
|
||||
const sysctlFailure = /^sysctl_failed_(\d+)$/u.exec(diagnostic ?? "");
|
||||
if (sysctlFailure?.[1] !== undefined)
|
||||
return `sysctl_errno_${sysctlFailure[1]}`;
|
||||
if (diagnostic === "reader_failure") return diagnostic;
|
||||
if (diagnostic === "duplicate_run_token") return diagnostic;
|
||||
if (diagnostic === "invalid_run_token_encoding") return diagnostic;
|
||||
return "other_unavailable";
|
||||
};
|
||||
|
||||
const processOwnershipFailures = async (
|
||||
members: readonly ProcessTableEntry[],
|
||||
runId: string,
|
||||
host: ProcessOwnershipHost,
|
||||
expectedIdentities: ReadonlyMap<number, string | null> = new Map(),
|
||||
): Promise<readonly ProcessOwnershipValidationFailure[]> => {
|
||||
const failures: ProcessOwnershipValidationFailure[] = [];
|
||||
if (expectedIdentities.size > 0 && host.processIdentities !== undefined) {
|
||||
const expectedMembers = members.filter(({ pid }) =>
|
||||
expectedIdentities.has(pid),
|
||||
);
|
||||
const observed = await host.processIdentities(expectedMembers);
|
||||
for (const member of expectedMembers) {
|
||||
const expected = expectedIdentities.get(member.pid);
|
||||
const current = observed.get(member.pid);
|
||||
if (
|
||||
expected === null ||
|
||||
current?.state !== "readable" ||
|
||||
current.identity !== expected
|
||||
) {
|
||||
if (await processIsGone(host, member.pid)) continue;
|
||||
failures.push({
|
||||
pid: member.pid,
|
||||
reason: "process-identity-unavailable",
|
||||
diagnostic:
|
||||
"process identity changed or became unavailable before signal",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const member of members) {
|
||||
try {
|
||||
if ((await host.environment(member.pid)).REA_PROCESS_RUN_ID !== runId)
|
||||
@@ -400,25 +597,123 @@ const processOwnershipFailures = async (
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
failures.length === 0 &&
|
||||
expectedIdentities.size > 0 &&
|
||||
host.processIdentities !== undefined
|
||||
) {
|
||||
const expectedMembers = members.filter(({ pid }) =>
|
||||
expectedIdentities.has(pid),
|
||||
);
|
||||
const observed = await host.processIdentities(expectedMembers);
|
||||
for (const member of expectedMembers) {
|
||||
const expected = expectedIdentities.get(member.pid);
|
||||
const current = observed.get(member.pid);
|
||||
if (
|
||||
expected !== null &&
|
||||
current?.state === "readable" &&
|
||||
current.identity === expected
|
||||
)
|
||||
continue;
|
||||
if (await processIsGone(host, member.pid)) continue;
|
||||
failures.push({
|
||||
pid: member.pid,
|
||||
reason: "process-identity-unavailable",
|
||||
diagnostic:
|
||||
"process identity changed or became unavailable during token validation",
|
||||
});
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
};
|
||||
|
||||
interface TokenOwnedProcessScan {
|
||||
readonly owned: readonly ProcessTableEntry[];
|
||||
readonly failures: readonly ProcessOwnershipValidationFailure[];
|
||||
readonly identities: ReadonlyMap<number, string | null>;
|
||||
}
|
||||
|
||||
const scanTokenOwnedProcesses = async (
|
||||
processes: readonly ProcessTableEntry[],
|
||||
runId: string,
|
||||
host: ProcessOwnershipHost,
|
||||
captureBaseline?: ProcessOwnershipBaseline,
|
||||
): Promise<TokenOwnedProcessScan> => {
|
||||
const owned: ProcessTableEntry[] = [];
|
||||
let candidates = processes;
|
||||
let candidateIdentities: ReadonlyMap<number, ProcessIdentityObservation> =
|
||||
new Map();
|
||||
if (host.processIdentities !== undefined) {
|
||||
const current = await host.processIdentities(processes);
|
||||
candidateIdentities = current;
|
||||
if (captureBaseline !== undefined) {
|
||||
const baseline = new Map(
|
||||
captureBaseline.map(({ pid, identity }) => [pid, identity]),
|
||||
);
|
||||
candidates = processes.filter((process) => {
|
||||
const before = baseline.get(process.pid);
|
||||
const now = current.get(process.pid);
|
||||
return !(
|
||||
before !== undefined &&
|
||||
before !== null &&
|
||||
now?.state === "readable" &&
|
||||
now.identity === before
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
const failures: ProcessOwnershipValidationFailure[] = [];
|
||||
for (const process of processes) {
|
||||
if (captureBaseline !== undefined && host.processIdentities !== undefined) {
|
||||
const stableCandidates: ProcessTableEntry[] = [];
|
||||
for (const process of candidates) {
|
||||
if (candidateIdentities.get(process.pid)?.state === "readable") {
|
||||
stableCandidates.push(process);
|
||||
continue;
|
||||
}
|
||||
if (!(await processIsGone(host, process.pid)))
|
||||
failures.push({
|
||||
pid: process.pid,
|
||||
reason: "process-identity-unavailable",
|
||||
diagnostic: "process identity was unavailable during token scan",
|
||||
});
|
||||
}
|
||||
candidates = stableCandidates;
|
||||
}
|
||||
let bulkTokens: ReadonlyMap<number, ProcessRunTokenObservation> | undefined;
|
||||
if (host.runTokens !== undefined)
|
||||
bulkTokens = await host.runTokens(candidates);
|
||||
const owned: ProcessTableEntry[] = [];
|
||||
const ownedIdentities = new Map<number, string | null>();
|
||||
for (const process of candidates) {
|
||||
const bulkToken = bulkTokens?.get(process.pid);
|
||||
if (bulkToken !== undefined) {
|
||||
if (bulkToken.state === "readable") {
|
||||
if (bulkToken.runId === runId) {
|
||||
owned.push(process);
|
||||
const identity = candidateIdentities.get(process.pid);
|
||||
ownedIdentities.set(
|
||||
process.pid,
|
||||
identity?.state === "readable" ? identity.identity : null,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!(await processIsGone(host, process.pid)))
|
||||
failures.push({
|
||||
pid: process.pid,
|
||||
reason: "environment-unreadable",
|
||||
diagnostic: bulkToken.reason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if ((await host.environment(process.pid)).REA_PROCESS_RUN_ID === runId)
|
||||
if ((await host.environment(process.pid)).REA_PROCESS_RUN_ID === runId) {
|
||||
owned.push(process);
|
||||
const identity = candidateIdentities.get(process.pid);
|
||||
ownedIdentities.set(
|
||||
process.pid,
|
||||
identity?.state === "readable" ? identity.identity : null,
|
||||
);
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
try {
|
||||
const live = liveProcesses(await host.listProcesses());
|
||||
@@ -438,5 +733,44 @@ const scanTokenOwnedProcesses = async (
|
||||
});
|
||||
}
|
||||
}
|
||||
return { owned, failures };
|
||||
if (candidates.length > 0 && host.processIdentities !== undefined) {
|
||||
const afterRead = await host.processIdentities(candidates);
|
||||
const stillOwned: ProcessTableEntry[] = [];
|
||||
const ownedPids = new Set(owned.map(({ pid }) => pid));
|
||||
for (const process of candidates) {
|
||||
const before = candidateIdentities.get(process.pid);
|
||||
const current = afterRead.get(process.pid);
|
||||
if (
|
||||
before?.state === "readable" &&
|
||||
current?.state === "readable" &&
|
||||
current.identity === before.identity
|
||||
) {
|
||||
if (ownedPids.has(process.pid)) stillOwned.push(process);
|
||||
continue;
|
||||
}
|
||||
if (await processIsGone(host, process.pid)) continue;
|
||||
failures.push({
|
||||
pid: process.pid,
|
||||
reason: "process-identity-unavailable",
|
||||
diagnostic:
|
||||
"process identity changed or became unavailable during token validation",
|
||||
});
|
||||
}
|
||||
return { owned: stillOwned, failures, identities: ownedIdentities };
|
||||
}
|
||||
return { owned, failures, identities: ownedIdentities };
|
||||
};
|
||||
|
||||
const processIsGone = async (
|
||||
host: ProcessOwnershipHost,
|
||||
pid: number,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
return !liveProcesses(await host.listProcesses()).some(
|
||||
(process) => process.pid === pid,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user