fix(process): normalize macOS launcher identity

This commit is contained in:
morluto 2026-07-24 03:38:25 +00:00
parent 13f3372a8b
commit 9c3aec8131
3 changed files with 53 additions and 9 deletions

View File

@ -0,0 +1,24 @@
import { basename } from "node:path";
/** Match process-table command text while handling macOS executable-name normalization. */
export const matchesOwnedProcessCommand = (
actual: string,
expected: string,
platform: NodeJS.Platform = process.platform,
): boolean => {
const actualCommand = actual.trim();
const expectedCommand = expected.trim();
if (
actualCommand === expectedCommand ||
actualCommand.startsWith(`${expectedCommand} `)
)
return true;
if (platform !== "darwin") return false;
const actualExecutable = actualCommand.split(/\s+/u, 1)[0];
const expectedExecutable = expectedCommand.split(/\s+/u, 1)[0];
return (
actualExecutable !== undefined &&
expectedExecutable !== undefined &&
basename(actualExecutable) === basename(expectedExecutable)
);
};

View File

@ -2,6 +2,8 @@ import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { promisify } from "node:util";
import { matchesOwnedProcessCommand } from "./ProcessCommandIdentity.js";
const execFileAsync = promisify(execFile);
/** Identity proof required before REA may signal an owned process group. */
@ -318,7 +320,7 @@ const launcherIdentityFailure = (
return "owned launcher parent identity did not match";
if (
ownership.expectedCommand !== undefined &&
!commandMatches(launcher.command, ownership.expectedCommand)
!matchesOwnedProcessCommand(launcher.command, ownership.expectedCommand)
)
return "owned launcher command identity did not match";
return null;
@ -471,7 +473,7 @@ export const observeOwnedProcessLineage = async (
);
if (
ownership.expectedCommand !== undefined &&
!commandMatches(launcher.command, ownership.expectedCommand)
!matchesOwnedProcessCommand(launcher.command, ownership.expectedCommand)
)
return unavailableLineage(
ownership,
@ -553,10 +555,3 @@ const liveProcesses = (
members: readonly ProcessTableEntry[],
): readonly ProcessTableEntry[] =>
members.filter(({ state }) => !state.startsWith("Z"));
const commandMatches = (actual: string, expected: string): boolean => {
return (
actual.trim() === expected.trim() ||
actual.trim().startsWith(`${expected.trim()} `)
);
};

View File

@ -9,6 +9,7 @@ import {
type ProcessOwnershipHost,
type WindowsProcessTreeHost,
} from "../src/process/ProcessOwnership.js";
import { matchesOwnedProcessCommand } from "../src/process/ProcessCommandIdentity.js";
const ownership = {
runId: "run-token",
@ -61,6 +62,30 @@ describe("owned process-group cleanup", () => {
).toEqual({ REA_PROCESS_RUN_ID: "owned", EMPTY: "" });
});
it("matches macOS executable names after process-table normalization", () => {
expect(
matchesOwnedProcessCommand(
"node /tmp/fake-launcher.mjs",
"/opt/node/bin/node",
"darwin",
),
).toBe(true);
expect(
matchesOwnedProcessCommand(
"node /tmp/fake-launcher.mjs",
"/opt/python/bin/python3",
"darwin",
),
).toBe(false);
expect(
matchesOwnedProcessCommand(
"node /tmp/fake-launcher.mjs",
"/opt/node/bin/node",
"linux",
),
).toBe(false);
});
it("signals only a group whose every member carries the run token", async () => {
const { adapter, signalGroup } = host({
100: { REA_PROCESS_RUN_ID: "run-token" },