fix(kuna): own version-probe processes through output and cleanup

Use the shared command supervisor for installation probes and retain exact
process cleanup owners through doctor, profile resolution, and session health.
Preserve exit status, bounded captured output, cancellation reasons, and live
cleanup facts. Block subsequent inspection until retained cleanup is retried.

Generalize the existing inspection resource scope and migrate its consumers,
including verifier scripts; remove the verifier's duplicate raw version probe.

Validation: 116 distinct focused tests passed, including built CLI doctor with
inherited output, cancellation/finalization, cleanup retry, and fake-provider
verifier workflows. Build, check:fast, and test discovery passed. No real Kuna
distribution or analysis target was executed.

Synthetic Darwin inspection timing: first-call median 707ms versus 5.79ms;
same-process repeat 37.9ms versus 3.08ms. First use includes ownership-helper
preparation. Family CPU and actual peak family memory remain unmeasured.

Fixes #5557
This commit is contained in:
morluto
2026-10-21 00:09:34 +08:00
parent 90e433df06
commit ff78165978
41 changed files with 1319 additions and 263 deletions
@@ -14,14 +14,14 @@ export const nativeKunaIncomingSites = async ({
binary,
address,
retain,
fileCloseResources,
resourceScope,
}) => {
const installed = await inspectKunaInstallation(
{
REA_KUNA_COMMAND: executable,
REA_KUNA_CACHE_DIR: cacheRoot,
},
{ fileCloseResources },
{ resourceScope },
);
assert.ok(installed.ok, "Native reference oracle could not inspect Kuna");
const input = await realpath(binary);
@@ -37,7 +37,7 @@ export const nativeKunaIncomingSites = async ({
outputBytes: KUNA_XREF_OUTPUT_BYTES,
signal: new AbortController().signal,
retainFailedFileClose: (owner, resource) =>
fileCloseResources.retain(owner, resource),
resourceScope.retain(owner, resource),
});
if (outcome.kind === "retained") retain(outcome);
if (outcome.kind !== "ok") throw outcome.error;
+3 -4
View File
@@ -19,14 +19,14 @@ export const nativeKunaStrings = async ({
workspace,
binary,
retain,
fileCloseResources,
resourceScope,
}) => {
const installed = await inspectKunaInstallation(
{
REA_KUNA_COMMAND: executable,
REA_KUNA_CACHE_DIR: cacheRoot,
},
{ fileCloseResources },
{ resourceScope },
);
assert.ok(installed.ok, "Native string oracle could not inspect Kuna");
const input = await realpath(binary);
@@ -51,7 +51,7 @@ export const nativeKunaStrings = async ({
outputBytes: 16 * 1024 * 1024,
signal: new AbortController().signal,
retainFailedFileClose: (owner, resource) =>
fileCloseResources.retain(owner, resource),
resourceScope.retain(owner, resource),
});
if (outcome.kind === "retained") retain(outcome);
if (outcome.kind !== "ok") throw outcome.error;
@@ -87,7 +87,6 @@ export const verifyKunaStrings = async ({
cacheRoot,
nativeStrings,
expectedStrings = [],
fileCloseResources,
}) => {
const expected = await nativeStrings();
if (expectedStrings.length > 0)
+2 -2
View File
@@ -1,9 +1,9 @@
import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import type { FileCloseResourceOwner } from "../../src/application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../../src/application/AnalysisProvider.js";
/** Complete a prerequisite operation, including retained cleanup on failure. */
export function withVerifierResourceOwner<T>(
operation: (owner: FileCloseResourceOwner) => Promise<T>,
operation: (owner: RetainedResourceScope) => Promise<T>,
): Promise<T>;
/** Hash one verifier input and transfer failed-close ownership to its scope. */
+27 -19
View File
@@ -25,8 +25,9 @@ import { fileURLToPath } from "node:url";
import { parseArgs, promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { execFileOutput } from "../dist/process/ExecFileOutput.js";
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { verifyKunaReferences } from "./lib/kuna-reference-verifier.mjs";
import {
@@ -71,11 +72,15 @@ if (values["compare-ghidra"]) {
);
}
const run = createVerifierRun();
const fileCloseScope = new ArtifactResourceScope();
const fileCloseResources = {
const inspectionResources = new ArtifactResourceScope();
const resourceScope = {
retain: (owner, resource) =>
fileCloseScope.retain({ kind: "file-handle", resource, handle: owner }),
run: (operation) => fileCloseScope.run(operation),
inspectionResources.retain({
kind: "lease",
resource,
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
}),
run: (operation) => inspectionResources.run(operation),
};
const workspace = await mkdtemp(join(tmpdir(), "rea-kuna-lane-"));
const cacheRoot = join(workspace, "cache");
@@ -292,7 +297,6 @@ const verifyKuna = async (
call: cold.call,
cacheRoot,
expectedStrings,
fileCloseResources,
nativeStrings: () =>
nativeKunaStrings({
executable: kuna,
@@ -300,7 +304,7 @@ const verifyKuna = async (
workspace,
binary: path,
retain: (owner) => retainedNative.add(owner),
fileCloseResources,
resourceScope,
}),
}),
});
@@ -324,7 +328,7 @@ const verifyKuna = async (
binary: path,
address: targetAddress,
retain: (outcome) => retainedNative.add(outcome),
fileCloseResources,
resourceScope,
}),
});
const pseudocode = await cold.call("procedure_pseudo_code", {
@@ -492,15 +496,17 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
const results = [];
const failures = [];
let fileResourcesClean = true;
let inspectionResourcesClean = true;
try {
kunaVersion = (
await execFileOutput(kuna, ["--version"], {
timeout: 10_000,
maxBuffer: 64 * 1024,
stopSignal: "SIGTERM",
})
).stdout.trim();
const installation = await inspectKunaInstallation(
{ REA_KUNA_COMMAND: kuna, REA_KUNA_CACHE_DIR: cacheRoot },
{ resourceScope },
);
assert.ok(
installation.ok,
installation.ok ? undefined : installation.error.reason,
);
kunaVersion = `kuna ${installation.value.version}`;
let callerPairs = [];
if (values.targets !== undefined) {
const names = (values.functions ?? "").split(",").filter(Boolean);
@@ -566,14 +572,16 @@ for (const retained of [...retainedNative]) {
}
}
try {
await fileCloseScope.close();
await inspectionResources.close();
} catch (cause) {
fileResourcesClean = false;
inspectionResourcesClean = false;
failures.push(cause);
}
const lifecycle = await completeVerifierRun(run);
let cleanupComplete =
pendingServers.size === 0 && retainedNative.size === 0 && fileResourcesClean;
pendingServers.size === 0 &&
retainedNative.size === 0 &&
inspectionResourcesClean;
try {
assertVerifierCleanupComplete(lifecycle.process_lineage);
} catch (cause) {
+2 -2
View File
@@ -1,4 +1,4 @@
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import type {
DoctorProviderCheck,
DoctorProviderInspection,
@@ -15,7 +15,7 @@ import {
/** Project the validated JADX configuration into the doctor contract. */
export const inspectJadxProvider = async (
environment: Readonly<Record<string, string | undefined>>,
resourceOwner: FileCloseResourceOwner,
resourceOwner: RetainedResourceScope,
signal?: AbortSignal,
): Promise<DoctorProviderInspection> => {
const configured = environment.REA_JADX_MCP_JAR !== undefined;
+2 -2
View File
@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import {
AnalysisError,
type AnalysisCapturedOutput,
@@ -27,7 +27,7 @@ const JAVA_MODULE_LISTING_TIMEOUT_MS = 10_000;
const JAVA_MODULE_LISTING_MAX_OUTPUT_BYTES = 1024 * 1024;
export interface JadxJavaProbeOptions {
readonly resourceOwner: FileCloseResourceOwner;
readonly resourceOwner: RetainedResourceScope;
readonly signal?: AbortSignal | undefined;
readonly operation?: string;
/** The Android operation owns this deadline; the probe also has its own bound. */
+2 -2
View File
@@ -1,6 +1,6 @@
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
import { throwIfJadxInterrupted } from "./JadxInterruption.js";
import { chmod, copyFile } from "node:fs/promises";
@@ -250,7 +250,7 @@ export class JadxProvider implements AndroidAnalysisPort {
readonly #inspections = new Set<
Promise<Awaited<ReturnType<typeof inspectJadxAvailability>>>
>();
readonly #prerequisiteOwner: FileCloseResourceOwner = {
readonly #prerequisiteOwner: RetainedResourceScope = {
retain: (owner, resource) =>
this.#prerequisites.retain({
kind: "lease",
+3 -3
View File
@@ -215,8 +215,8 @@ export interface AnalysisClientContext {
readonly runId: string;
}
/** One lifecycle owner for descriptor cleanup and abortable inspection work. */
export interface FileCloseResourceOwner {
/** One lifecycle owner for retained resources and abortable inspection work. */
export interface RetainedResourceScope {
retain(
owner: {
close(): Promise<void>;
@@ -246,7 +246,7 @@ export interface AnalysisProviderInspectionOptions {
}
export interface AnalysisProfileResolutionOptions extends AnalysisProviderInspectionOptions {
readonly fileCloseResources?: FileCloseResourceOwner;
readonly resourceScope?: RetainedResourceScope;
}
interface CapabilityEffects {
+4 -7
View File
@@ -10,7 +10,7 @@ import { STARTUP_OWNER_CLOSED } from "../process/SharedStartupAttempt.js";
import { constants } from "node:fs";
import { access, readdir, stat } from "node:fs/promises";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import type { FileCloseResourceOwner } from "./AnalysisProvider.js";
import type { RetainedResourceScope } from "./AnalysisProvider.js";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import { join } from "node:path";
@@ -480,7 +480,7 @@ export interface SystemDoctorHostOptions {
readonly environment?: NodeJS.ProcessEnv;
readonly execFileOutput?: typeof execFileOutput;
readonly providerInspections?: (
fileCloseResources: FileCloseResourceOwner,
resourceScope: RetainedResourceScope,
signal: AbortSignal,
) => Promise<readonly DoctorProviderInspection[]>;
readonly providerRegistrationEnvironment?: () => Readonly<
@@ -506,7 +506,7 @@ export const systemDoctorHost = (
const hostExecFileOutput = options.execFileOutput ?? execFileOutput;
const homeDirectory = homeDirectoryFromEnvironment(environment, platform);
const commandEnvironment = { env: environment };
const fileCloseResources: FileCloseResourceOwner = {
const resourceScope: RetainedResourceScope = {
retain: (owner, resource) =>
resources.retain({
kind: "lease",
@@ -620,10 +620,7 @@ export const systemDoctorHost = (
: {
providerInspections: () =>
resources.run(() =>
options.providerInspections!(
fileCloseResources,
inspectionSignal,
),
options.providerInspections!(resourceScope, inspectionSignal),
),
}),
...(options.providerRegistrationEnvironment === undefined
@@ -39,7 +39,7 @@ import {
type AnalysisProviderCandidateEvaluation,
} from "./AnalysisProviderEvaluation.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import type { FileCloseResourceOwner } from "../AnalysisProvider.js";
import type { RetainedResourceScope } from "../AnalysisProvider.js";
/** Deterministic discovery truth for one configured deep provider. */
export interface AnalysisProviderCandidateStatus {
@@ -108,15 +108,15 @@ export class AnalysisProviderRegistry {
>
>();
readonly #selectionCleanup = new Set<AnalysisError>();
readonly #profileFiles = new ArtifactResourceScope();
readonly #profileFileOwner: FileCloseResourceOwner = {
readonly #profileResources = new ArtifactResourceScope();
readonly #profileOwner: RetainedResourceScope = {
retain: (owner, resource) =>
this.#profileFiles.retain({
this.#profileResources.retain({
kind: "lease",
resource,
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
}),
run: (operation) => this.#profileFiles.run(operation),
run: (operation) => this.#profileResources.run(operation),
};
#closing: Promise<Result<null, AnalysisError>> | undefined;
@@ -186,7 +186,7 @@ export class AnalysisProviderRegistry {
return (
this.#selections.size > 0 ||
this.#discoveries.size > 0 ||
this.#profileFiles.hasPending ||
this.#profileResources.hasPending ||
[...this.#selectionCleanup].some((error) => error.cleanupIncomplete) ||
this.#providers.some(
(provider) => provider.hasPendingOwnedResources?.() === true,
@@ -215,7 +215,7 @@ export class AnalysisProviderRegistry {
]);
const failures: AnalysisError[] = [];
try {
await this.#profileFiles.retryCleanup();
await this.#profileResources.retryCleanup();
} catch (cause: unknown) {
const cleanup = ArtifactReaderFailure.cleanupObservation(
cause,
@@ -564,7 +564,7 @@ export class AnalysisProviderRegistry {
provider,
initial,
target,
{ signal, fileCloseResources: this.#profileFileOwner, scope },
{ signal, resourceScope: this.#profileOwner, scope },
);
if (!evaluation.ok) return evaluation;
const evaluated = evaluation.value;
@@ -608,7 +608,7 @@ export class AnalysisProviderRegistry {
status.targetSupport.status === "supported"
? evaluateAnalysisProviderCandidate(provider, status, target, {
signal,
fileCloseResources: this.#profileFileOwner,
resourceScope: this.#profileOwner,
scope,
})
: ok({
+4 -8
View File
@@ -49,20 +49,16 @@ export const createSystemDoctorHost = (
// parser-owned error when IDA itself was explicitly selected.
return analysisProvider === "ida" ? registration : ok(undefined);
},
providerInspections: async (fileCloseResources, inspectionSignal) => [
providerInspections: async (resourceScope, inspectionSignal) => [
await inspectSystemGhidraProvider(
environment,
fileCloseResources,
resourceScope,
inspectionSignal,
),
inspectIdaRegistration(environment.REA_IDA_MCP_CONFIG),
await inspectJadxProvider(
environment,
fileCloseResources,
inspectionSignal,
),
await inspectJadxProvider(environment, resourceScope, inspectionSignal),
await inspectKunaProvider(environment, process.platform, {
fileCloseResources,
resourceScope,
signal: inspectionSignal,
}),
],
+2 -2
View File
@@ -1,6 +1,6 @@
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import type {
DoctorProviderCheck,
DoctorProviderInspection,
@@ -69,7 +69,7 @@ export const projectGhidraDoctorInspection = (
/** Inspect the process-configured BYO Ghidra installation for `rea doctor`. */
export const inspectSystemGhidraProvider = async (
environment: Readonly<NodeJS.ProcessEnv>,
resourceOwner: FileCloseResourceOwner,
resourceOwner: RetainedResourceScope,
signal?: AbortSignal,
): Promise<DoctorProviderInspection> => {
try {
+2 -2
View File
@@ -1,6 +1,6 @@
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import {
probeGhidraJava,
type GhidraJavaObservation,
@@ -160,7 +160,7 @@ const assessNativeDecompiler = (
/** Caller-owned paths and host coordinates used for one installation probe. */
export interface GhidraInstallationOptions {
readonly environment: Readonly<NodeJS.ProcessEnv>;
readonly resourceOwner: FileCloseResourceOwner;
readonly resourceOwner: RetainedResourceScope;
readonly signal?: AbortSignal;
readonly installDir?: string;
readonly javaHome?: string;
+2 -2
View File
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import {
ArtifactFileCancelled,
ArtifactFileCleanupFailure,
@@ -15,7 +15,7 @@ import { ghidraFileFailure } from "./GhidraFileArtifacts.js";
/** Metadata admission budget, not a Java Properties format limit. */
export const MAX_GHIDRA_INSTALLATION_PROPERTIES_BYTES = 1024 * 1024;
export interface GhidraInstallationFileOptions {
readonly resourceOwner: FileCloseResourceOwner;
readonly resourceOwner: RetainedResourceScope;
readonly signal?: AbortSignal;
}
+2 -2
View File
@@ -6,7 +6,7 @@ import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
import {
OwnedCommandFailure,
@@ -110,7 +110,7 @@ const executablePath = (path: string): boolean => {
}
};
export interface GhidraJavaProbeOptions {
readonly resourceOwner: FileCloseResourceOwner;
readonly resourceOwner: RetainedResourceScope;
readonly signal?: AbortSignal;
readonly launcher?: NonNullable<
Parameters<typeof runOwnedCommand>[2]
+3 -3
View File
@@ -14,7 +14,7 @@ import type {
AnalysisClient,
AnalysisClientContext,
AnalysisProviderInspectionOptions,
FileCloseResourceOwner,
RetainedResourceScope,
AnalysisProfileResolutionOptions,
AnalysisProfileResolution,
AnalysisProviderCandidate,
@@ -108,7 +108,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
#inspectionAttempt:
| SharedStartupAttempt<Result<GhidraInstallationInspection, AnalysisError>>
| undefined;
readonly #inspectionOwner: FileCloseResourceOwner = {
readonly #inspectionOwner: RetainedResourceScope = {
retain: (owner, resource) =>
this.#inspectionResources.retain({
kind: "lease",
@@ -601,7 +601,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
#observeInstallation(
signal: AbortSignal,
resourceOwner: FileCloseResourceOwner = this.#inspectionOwner,
resourceOwner: RetainedResourceScope = this.#inspectionOwner,
): Promise<GhidraInstallationInspection> {
const options = {
resourceOwner,
+23
View File
@@ -13,6 +13,8 @@ import {
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
OwnedCommandFailure,
runOwnedCommand,
@@ -130,6 +132,27 @@ export const kunaFileCleanupError = (
return analysisErrorWithCleanupFailure(primary, cleanup, operation);
};
/** Preserve the exact retryable resources when an inspection admission retries them. */
export const kunaRetainedResourceCleanupError = (
cause: unknown,
operation: string,
): AnalysisError | undefined => {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanupLeases === undefined ||
cause.cleanupLeases.length === 0
)
return undefined;
const cleanup = new ProviderCleanupError(
KUNA_PROVIDER_ID,
cause.cleanup?.resources ?? [],
{ reason: message(cause) },
{ cause, operation },
);
bindArtifactFileCleanup(cause, cleanup);
return cleanup;
};
/** File metadata that, unchanged, lets a verified digest stand without re-hashing. */
const verifiedExecutables = new Map<
string,
+68 -12
View File
@@ -7,10 +7,12 @@ import type {
DoctorProviderInspection,
} from "../application/Doctor.js";
import { readKunaConfiguration } from "./KunaConfiguration.js";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { kunaRetainedResourceCleanupError } from "./KunaCommand.js";
import {
createKunaResourceScope,
inspectKunaInstallation,
kunaInstallationCancellation,
kunaInstallationFailureDiagnostics,
@@ -39,25 +41,60 @@ export const inspectKunaProvider = async (
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform = process.platform,
options: {
readonly fileCloseResources: FileCloseResourceOwner;
readonly resourceScope: RetainedResourceScope;
readonly signal?: AbortSignal;
},
): Promise<DoctorProviderInspection> => {
const kunaResources = createKunaResourceScope(options.resourceScope);
let installation: Awaited<ReturnType<typeof inspectKunaInstallation>>;
try {
installation = await inspectKunaInstallation(environment, {
platform,
fileCloseResources: options.fileCloseResources,
resourceScope: kunaResources.owner,
...(options.signal === undefined ? {} : { signal: options.signal }),
});
} catch (cause: unknown) {
const retainedCleanup = kunaRetainedResourceCleanupError(cause, "doctor");
if (retainedCleanup !== undefined)
return {
id: KUNA_PROVIDER_ID,
configured: environment.REA_KUNA_COMMAND !== undefined,
available: false,
providerVersion: null,
registrationEnvironment: {},
checks: [
{
name: "installation",
ok: false,
code: "cleanup_incomplete",
detail: retainedCleanup.message,
remediation: null,
classification: "config_drift",
details: {
cleanup_incomplete: true,
cleanup_resources: [...retainedCleanup.cleanupResources],
},
},
],
};
if (!(cause instanceof RegularFileCleanupFailure)) {
const cancellation = kunaInstallationCancellation(cause);
if (cancellation !== undefined)
throw new AnalysisCancelledError("doctor", {
if (cancellation !== undefined) {
const error = new AnalysisCancelledError("doctor", {
cause: cancellation.reason,
diagnostics: cancellation.diagnostics,
...(!kunaResources.hasPending
? {}
: {
cleanup: {
reason: "Kuna version-probe process cleanup is unconfirmed.",
resources: kunaResources.cleanupResources ?? [],
},
}),
});
kunaResources.bindRetainedFailure(error);
throw error;
}
throw cause;
}
return {
@@ -85,15 +122,34 @@ export const inspectKunaProvider = async (
const configured =
environment.REA_KUNA_COMMAND !== undefined &&
environment.REA_KUNA_COMMAND.length > 0;
if (!installation.ok)
const base = {
id: KUNA_PROVIDER_ID,
configured,
available: false,
providerVersion: null,
registrationEnvironment: {},
} as const;
if (!installation.ok) {
const check = installationFailure(installation.error, configured);
const cleanupResources = kunaResources.cleanupResources;
if (!kunaResources.hasPending) return { ...base, checks: [check] };
return {
id: KUNA_PROVIDER_ID,
configured,
available: false,
providerVersion: null,
registrationEnvironment: {},
checks: [installationFailure(installation.error, configured)],
...base,
checks: [
{
...check,
code: "cleanup_incomplete",
details: {
...installation.error.diagnostics,
cleanup_incomplete: true,
...(cleanupResources === undefined
? {}
: { cleanup_resources: [...cleanupResources] }),
},
},
],
};
}
const { value } = installation;
return {
id: KUNA_PROVIDER_ID,
+201 -61
View File
@@ -3,18 +3,23 @@ import {
RegularFileOperationCancelled,
} from "../filesystem/RegularFile.js";
import { constants } from "node:fs";
import { randomUUID } from "node:crypto";
import { access, realpath, stat } from "node:fs/promises";
import { join } from "node:path";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../domain/result.js";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
AnalysisCleanupLease,
AnalysisCleanupLeaseGroup,
} from "../artifacts/AnalysisCleanupLease.js";
import {
hashStableFile,
stableFileHashCause,
} from "../filesystem/StableFileHash.js";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import {
RegularFileCleanupFailure,
RegularFileCancelled,
@@ -22,10 +27,15 @@ import {
withOwnedRegularFile,
} from "../filesystem/OwnedRegularFile.js";
import {
execFileOutput,
execFileOutputFailure,
ExecFileOutputCancelledError,
} from "../process/ExecFileOutput.js";
OwnedCommandFailure,
runOwnedCommand,
} from "../process/OwnedCommand.js";
import { retainOwnedCommandCleanup } from "../providers/OwnedCommandCleanup.js";
import type {
OwnedProviderProcessSpawnOptions,
ProviderProcessSnapshot,
SpawnedOwnedProviderProcess,
} from "../process/ProviderProcess.js";
import {
readKunaConfiguration,
type KunaConfiguration,
@@ -44,6 +54,45 @@ const VERSION_PROBE_MAX_OUTPUT_BYTES = 64 * 1024;
/** Release CI prints exactly `kuna <MAJOR.MINOR>`; source builds print the Cargo version. */
const VERSION_LINE = /^kuna (\d+\.\d+(?:\.\d+)?)\r?\n?$/u;
const versionProbeOutput = (snapshot: ProviderProcessSnapshot | null) => ({
stdout: snapshot?.stdout.text ?? "",
stderr: snapshot?.stderr.text ?? "",
exitCode: snapshot?.exitCode ?? null,
signal: snapshot?.signal ?? null,
stdoutBytes: snapshot?.stdout.bytes ?? 0,
stdoutObservedBytes: snapshot?.stdout.observedBytes ?? 0,
stderrBytes: snapshot?.stderr.bytes ?? 0,
stderrObservedBytes: snapshot?.stderr.observedBytes ?? 0,
stdoutInvalidUtf8: snapshot?.stdout.invalidUtf8 === true,
stderrInvalidUtf8: snapshot?.stderr.invalidUtf8 === true,
diagnosticTruncated: snapshot?.diagnosticTruncated === true,
outputIncomplete: snapshot?.outputIncomplete === true,
truncated:
snapshot?.diagnosticTruncated === true ||
snapshot?.outputIncomplete === true ||
(snapshot !== null &&
snapshot !== undefined &&
snapshot.stdout.bytes < snapshot.stdout.observedBytes),
});
const versionProbeOutputDiagnostics = (
output: ReturnType<typeof versionProbeOutput>,
) => ({
stdout: output.stdout,
stderr: output.stderr,
exit_code: output.exitCode,
signal: output.signal,
stdout_bytes: output.stdoutBytes,
stdout_observed_bytes: output.stdoutObservedBytes,
stderr_bytes: output.stderrBytes,
stderr_observed_bytes: output.stderrObservedBytes,
...(output.stdoutInvalidUtf8 ? { stdout_invalid_utf8: true } : {}),
...(output.stderrInvalidUtf8 ? { stderr_invalid_utf8: true } : {}),
diagnostic_truncated: output.diagnosticTruncated,
output_incomplete: output.outputIncomplete,
output_truncated: output.truncated,
});
/** One observed Kuna executable, identified by its bytes as well as its version. */
export interface KunaInstallation {
readonly configuration: KunaConfiguration;
@@ -77,7 +126,11 @@ export interface KunaInstallationOptions {
readonly signal?: AbortSignal;
/** Probe deadline; tests shorten it. */
readonly probeTimeoutMs?: number;
readonly fileCloseResources: FileCloseResourceOwner;
readonly resourceScope: RetainedResourceScope;
/** Test seam for the owned command launcher; production uses the verified default. */
readonly processLauncher?: (
options: OwnedProviderProcessSpawnOptions,
) => Promise<SpawnedOwnedProviderProcess>;
}
/** Cancellation facts normalized by the installation inspection that observed them. */
@@ -113,30 +166,52 @@ export const kunaInstallationCancellation = (
};
};
/** One installation inspection's file owners and their originating errors. */
export interface KunaFileCloseScope {
readonly owner: FileCloseResourceOwner;
/** One installation inspection's retained owners and originating errors. */
export interface KunaResourceScope {
readonly owner: RetainedResourceScope;
readonly cleanupResources: readonly string[] | undefined;
readonly hasPending: boolean;
bindFailure(failure: RegularFileCleanupFailure, error: AnalysisError): void;
bindRetainedFailure(error: AnalysisError): void;
}
/** Adapt retained descriptor owners into leases and bind errors to exact owners. */
export const createKunaFileCloseScope = (
parent: FileCloseResourceOwner,
): KunaFileCloseScope => {
/** Adapt retained resource owners into leases and bind errors to exact owners. */
export const createKunaResourceScope = (
parent: RetainedResourceScope,
): KunaResourceScope => {
const leases = new Map<
Parameters<FileCloseResourceOwner["retain"]>[0],
AnalysisCleanupLease
Parameters<RetainedResourceScope["retain"]>[0],
{ readonly lease: AnalysisCleanupLease }
>();
const bindPendingFailure = (error: AnalysisError) => {
const retained = [...leases.values()]
.map(({ lease }) => lease)
.filter((lease) => !lease.released);
if (retained.length > 0) new AnalysisCleanupLeaseGroup(error, retained);
};
return {
get hasPending() {
return [...leases.values()].some(({ lease }) => !lease.released);
},
get cleanupResources() {
const pending = [...leases.values()]
.map(({ lease }) => lease)
.filter((lease) => !lease.released);
if (pending.some((lease) => lease.cleanupResources === undefined))
return undefined;
return [
...new Set(pending.flatMap((lease) => lease.cleanupResources ?? [])),
];
},
owner: {
retain: (owner, resource) => {
const lease = AnalysisCleanupLease.forCloseOwner(owner, resource);
leases.set(owner, lease);
leases.set(owner, { lease });
parent.retain(
{
close: () => lease.release(),
get cleanupResources() {
return lease.cleanupResources ?? [resource];
return lease.cleanupResources;
},
},
resource,
@@ -144,8 +219,10 @@ export const createKunaFileCloseScope = (
},
run: (operation) => parent.run(operation),
},
bindFailure: (failure, error) =>
leases.get(failure.owner)?.bindCleanupError(error),
bindFailure: (failure, error) => {
if (leases.has(failure.owner)) bindPendingFailure(error);
},
bindRetainedFailure: bindPendingFailure,
};
};
@@ -211,40 +288,105 @@ export const inspectKunaInstallation = async (
assertRegularFileActive(options.signal);
const deadline = options.probeTimeoutMs ?? VERSION_PROBE_TIMEOUT_MS;
let stdout: string;
let snapshot: ProviderProcessSnapshot;
try {
({ stdout } = await execFileOutput(executable, ["--version"], {
env: kunaChildEnvironment(configuration.value),
timeout: deadline,
maxBuffer: VERSION_PROBE_MAX_OUTPUT_BYTES,
// Escalate to SIGKILL so an executable that ignores SIGTERM cannot hang doctor.
stopSignal: "SIGTERM",
...(options.signal === undefined ? {} : { signal: options.signal }),
}));
snapshot = await options.resourceScope.run(() =>
runOwnedCommand(
{
command: executable,
arguments: ["--version"],
runId: `rea-kuna-version-${randomUUID()}`,
// The configured command may be an interpreter-driven shim.
expectedCommand: null,
hostEnvironment: kunaChildEnvironment(configuration.value),
},
{
timeoutMs: deadline,
diagnosticBytes: VERSION_PROBE_MAX_OUTPUT_BYTES,
},
{
...(options.signal === undefined ? {} : { signal: options.signal }),
...(options.processLauncher === undefined
? {}
: { launcher: options.processLauncher }),
},
),
);
} catch (cause: unknown) {
if (cause instanceof ExecFileOutputCancelledError)
throw new RegularFileOperationCancelled(cause.cause);
const output = execFileOutputFailure(cause);
if (cause instanceof ArtifactReaderFailure) throw cause;
const retainedCleanup = retainOwnedCommandCleanup(
cause,
options.resourceScope,
"kuna",
"kuna:installation",
);
const cleanupIncomplete = retainedCleanup.length > 0;
if (cause instanceof OwnedCommandFailure) {
const output = versionProbeOutput(cause.snapshot);
if (cause.reason === "cancelled") {
const cancellation = new RegularFileOperationCancelled(
cause.cancellation?.reason,
);
installationFailureDiagnostics.set(cancellation, {
kuna_version_probe: {
status: "partial",
command,
executable,
deadline_ms: deadline,
...versionProbeOutputDiagnostics(output),
probe_failure_reason: cause.reason,
stop_requested: cause.snapshot !== null,
cleanup_incomplete: cleanupIncomplete,
},
});
throw cancellation;
}
return failure(
"version_unresolved",
`${executable} --version failed: ${output.stderr.trim() || cause.message}. ${KUNA_VERSION_PROBE_REMEDIATION}`,
{
command,
executable,
probe_failure_reason: cause.reason,
stop_requested: cause.snapshot !== null,
cleanup_incomplete: cleanupIncomplete,
deadline_ms: deadline,
...versionProbeOutputDiagnostics(output),
},
);
}
return failure(
"version_unresolved",
`${executable} --version failed: ${output?.stderr.trim() || message(cause)}. ${KUNA_VERSION_PROBE_REMEDIATION}`,
`${executable} --version failed: ${message(cause)}. ${KUNA_VERSION_PROBE_REMEDIATION}`,
{
command,
executable,
exit_code: typeof output?.code === "number" ? output.code : null,
signal: output?.signal ?? null,
killed: output?.killed ?? false,
output_truncated: output?.outputTruncated ?? false,
exit_code: null,
signal: null,
cleanup_incomplete: cleanupIncomplete,
deadline_ms: deadline,
},
);
}
const version = VERSION_LINE.exec(stdout)?.[1];
const probeOutput = versionProbeOutput(snapshot);
if (probeOutput.truncated)
return failure(
"version_unresolved",
`${executable} --version output was incomplete. ${KUNA_VERSION_PROBE_REMEDIATION}`,
{
command,
executable,
deadline_ms: deadline,
output_truncated: true,
stdout: probeOutput.stdout.slice(0, 256),
},
);
const version = VERSION_LINE.exec(probeOutput.stdout)?.[1];
if (version === undefined)
return failure(
"version_unresolved",
`${executable} --version printed no recognizable Kuna version. ${KUNA_VERSION_PROBE_REMEDIATION}`,
{ command, executable, stdout: stdout.slice(0, 256) },
{ command, executable, stdout: probeOutput.stdout.slice(0, 256) },
);
let fingerprint: { readonly sha256: string; readonly bytes: number };
@@ -255,26 +397,24 @@ export const inspectKunaInstallation = async (
script?: boolean;
} = { phase: "fingerprint" };
try {
({ fingerprint, script } = await options.fileCloseResources.run(
async () => {
const fingerprint = await hashStableFile(executable, {
...(options.signal === undefined ? {} : { signal: options.signal }),
retainFailedClose: (owner) =>
options.fileCloseResources.retain(owner, executable),
});
observed.completed_hash = fingerprint;
assertRegularFileActive(options.signal);
observed.phase = "shebang";
const script = await startsWithShebang(
executable,
options.fileCloseResources,
options.signal,
);
observed.script = script;
assertRegularFileActive(options.signal);
return { fingerprint, script };
},
));
({ fingerprint, script } = await options.resourceScope.run(async () => {
const fingerprint = await hashStableFile(executable, {
...(options.signal === undefined ? {} : { signal: options.signal }),
retainFailedClose: (owner) =>
options.resourceScope.retain(owner, executable),
});
observed.completed_hash = fingerprint;
assertRegularFileActive(options.signal);
observed.phase = "shebang";
const script = await startsWithShebang(
executable,
options.resourceScope,
options.signal,
);
observed.script = script;
assertRegularFileActive(options.signal);
return { fingerprint, script };
}));
} catch (cause: unknown) {
const cancellation = regularFileCancellation(cause);
if (
@@ -332,7 +472,7 @@ export const inspectKunaInstallation = async (
const startsWithShebang = (
path: string,
fileCloseResources: FileCloseResourceOwner,
resourceScope: RetainedResourceScope,
signal?: AbortSignal,
): Promise<boolean> =>
withOwnedRegularFile(
@@ -340,7 +480,7 @@ const startsWithShebang = (
{
symlinks: "follow",
...(signal === undefined ? {} : { signal }),
retainFailedClose: (owner) => fileCloseResources.retain(owner, path),
retainFailedClose: (owner) => resourceScope.retain(owner, path),
},
async (handle) => {
const prefix = Buffer.alloc(2);
+54 -16
View File
@@ -19,6 +19,7 @@ import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import {
kunaFileCleanupError,
kunaRetainedResourceCleanupError,
type KunaProcessLauncher,
} from "./KunaCommand.js";
import {
@@ -26,7 +27,7 @@ import {
type KunaConfiguration,
} from "./KunaConfiguration.js";
import {
createKunaFileCloseScope,
createKunaResourceScope,
inspectKunaInstallation,
kunaInstallationCancellation,
} from "./KunaInstallation.js";
@@ -55,6 +56,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
readonly #environment: Readonly<NodeJS.ProcessEnv>;
readonly #platform: NodeJS.Platform;
readonly #launcher: KunaProcessLauncher | undefined;
readonly #probeTimeoutMs: number | undefined;
readonly #singleFunctionRuns: number | undefined;
#cache: KunaProjectCache | undefined;
#runSlots: KunaRunSlots | undefined;
@@ -64,6 +66,8 @@ export class KunaProvider implements AnalysisProviderCandidate {
options: {
readonly platform?: NodeJS.Platform;
readonly launcher?: KunaProcessLauncher;
/** Test seam for bounded installation-probe lifecycle cases. */
readonly probeTimeoutMs?: number;
/** Test seam for foreground native runs; production derives it from the host. */
readonly singleFunctionRuns?: number;
} = {},
@@ -71,6 +75,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
this.#platform = options.platform ?? process.platform;
this.#environment = snapshotEnvironment(environment, this.#platform);
this.#launcher = options.launcher;
this.#probeTimeoutMs = options.probeTimeoutMs;
this.#singleFunctionRuns = options.singleFunctionRuns;
}
@@ -175,40 +180,73 @@ export class KunaProvider implements AnalysisProviderCandidate {
}),
);
let installation: Awaited<ReturnType<typeof inspectKunaInstallation>>;
const fileCloseResources = options?.fileCloseResources;
if (fileCloseResources === undefined)
const resourceScope = options?.resourceScope;
if (resourceScope === undefined)
return err(
kunaUnavailable(
"kuna:profile",
"Kuna profile inspection requires a durable file resource owner",
"Kuna profile inspection requires a durable resource scope",
),
);
const fileCloseScope = createKunaFileCloseScope(fileCloseResources);
const kunaResources = createKunaResourceScope(resourceScope);
try {
installation = await inspectKunaInstallation(this.#environment, {
platform: this.#platform,
...(options?.signal === undefined ? {} : { signal: options.signal }),
fileCloseResources: fileCloseScope.owner,
resourceScope: kunaResources.owner,
...(this.#launcher === undefined
? {}
: { processLauncher: this.#launcher }),
...(this.#probeTimeoutMs === undefined
? {}
: { probeTimeoutMs: this.#probeTimeoutMs }),
});
} catch (cause: unknown) {
const retainedCleanup = kunaRetainedResourceCleanupError(
cause,
"kuna:profile",
);
if (retainedCleanup !== undefined) return err(retainedCleanup);
const cancellation = kunaInstallationCancellation(cause);
if (
!(cause instanceof RegularFileCleanupFailure) &&
cancellation !== undefined
)
return err(
new AnalysisCancelledError("kuna:profile", {
cause: cancellation.reason,
diagnostics: cancellation.diagnostics,
}),
);
) {
const error = new AnalysisCancelledError("kuna:profile", {
cause: cancellation.reason,
diagnostics: cancellation.diagnostics,
...(!kunaResources.hasPending
? {}
: {
cleanup: {
reason: "Kuna version-probe process cleanup is unconfirmed.",
resources: kunaResources.cleanupResources ?? [],
},
}),
});
kunaResources.bindRetainedFailure(error);
return err(error);
}
if (!(cause instanceof RegularFileCleanupFailure)) throw cause;
const error = kunaFileCleanupError(cause, "kuna:profile");
fileCloseScope.bindFailure(cause, error);
kunaResources.bindFailure(cause, error);
return err(error);
}
if (!installation.ok) {
const error = kunaUnavailable("open_binary", installation.error.reason, {
diagnostics: installation.error.diagnostics,
...(!kunaResources.hasPending
? {}
: {
cleanup: {
reason: "Kuna version-probe process cleanup is unconfirmed.",
resources: kunaResources.cleanupResources ?? [],
},
}),
});
kunaResources.bindRetainedFailure(error);
return err(error);
}
if (!installation.ok)
return err(kunaUnavailable("open_binary", installation.error.reason));
const { value } = installation;
return ok({
profile: createAnalysisProfile(
+49 -19
View File
@@ -7,7 +7,7 @@ import {
type AnalysisExecution,
type AnalysisOperation,
type ExecutionOptions,
type FileCloseResourceOwner,
type RetainedResourceScope,
} from "../application/AnalysisProvider.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
@@ -38,11 +38,12 @@ import { SharedStartupAttempt } from "../process/SharedStartupAttempt.js";
import {
KUNA_INVENTORY_OUTPUT_BYTES,
kunaFileCleanupError,
kunaRetainedResourceCleanupError,
type KunaProcessLauncher,
} from "./KunaCommand.js";
import { kunaFunctionDeadline } from "./KunaConfiguration.js";
import {
createKunaFileCloseScope,
createKunaResourceScope,
inspectKunaInstallation,
kunaInstallationCancellation,
type KunaInstallation,
@@ -236,7 +237,8 @@ export class KunaSessionClient implements AnalysisClient {
readonly #cache: KunaProjectCache;
readonly #environment: Readonly<NodeJS.ProcessEnv>;
readonly #runSlots: KunaRunSlots;
readonly #fileCloseResources = new ArtifactResourceScope();
readonly #launcher: KunaProcessLauncher | undefined;
readonly #resourceScope = new ArtifactResourceScope();
/** Single-function runs in flight, by entry address. */
readonly #singleFunctionRuns = new Map<string, SingleFunctionRun>();
/** Completed single-function texts by address, least recently used first. */
@@ -289,6 +291,7 @@ export class KunaSessionClient implements AnalysisClient {
this.#cache = dependencies.cache;
this.#runSlots = dependencies.runSlots;
this.#environment = dependencies.environment;
this.#launcher = dependencies.launcher;
this.#commands = new KunaTargetCommands(target, {
signal: this.#session.signal,
...(dependencies.launcher === undefined
@@ -450,7 +453,7 @@ export class KunaSessionClient implements AnalysisClient {
const commands = await this.#commands.close();
if (!commands.ok) failures.push(commands.error);
try {
await this.#fileCloseResources.close();
await this.#resourceScope.close();
} catch (cause: unknown) {
const observation = ArtifactReaderFailure.cleanupObservation(
cause,
@@ -1376,24 +1379,40 @@ export class KunaSessionClient implements AnalysisClient {
async #inspectInstallation(
signal: AbortSignal,
): Promise<Result<KunaInstallation, AnalysisError>> {
const parentFileCloseResources: FileCloseResourceOwner = {
const parentResourceScope: RetainedResourceScope = {
retain: (owner, resource) =>
this.#fileCloseResources.retain({
this.#resourceScope.retain({
kind: "lease",
resource,
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
}),
run: <T>(operation: () => Promise<T>) =>
this.#fileCloseResources.run(operation),
this.#resourceScope.run(operation),
};
const fileCloseScope = createKunaFileCloseScope(parentFileCloseResources);
const kunaResources = createKunaResourceScope(parentResourceScope);
try {
const inspected = await inspectKunaInstallation(this.#environment, {
fileCloseResources: fileCloseScope.owner,
resourceScope: kunaResources.owner,
signal,
...(this.#launcher === undefined
? {}
: { processLauncher: this.#launcher }),
});
if (!inspected.ok)
return err(kunaUnavailable("health", inspected.error.reason));
if (!inspected.ok) {
const error = kunaUnavailable("health", inspected.error.reason, {
diagnostics: inspected.error.diagnostics,
...(!kunaResources.hasPending
? {}
: {
cleanup: {
reason: "Kuna version-probe process cleanup is unconfirmed.",
resources: kunaResources.cleanupResources ?? [],
},
}),
});
kunaResources.bindRetainedFailure(error);
return err(error);
}
const committed = this.#profile?.parameters["kuna_sha256"];
if (committed !== undefined && committed !== inspected.value.sha256)
return err(
@@ -1408,21 +1427,32 @@ export class KunaSessionClient implements AnalysisClient {
);
return ok(inspected.value);
} catch (cause: unknown) {
const retainedCleanup = kunaRetainedResourceCleanupError(cause, "health");
if (retainedCleanup !== undefined) return err(retainedCleanup);
const cancellation = kunaInstallationCancellation(cause);
if (
!(cause instanceof RegularFileCleanupFailure) &&
cancellation !== undefined
)
return err(
new AnalysisCancelledError("health", {
cause: cancellation.reason,
diagnostics: cancellation.diagnostics,
}),
);
) {
const error = new AnalysisCancelledError("health", {
cause: cancellation.reason,
diagnostics: cancellation.diagnostics,
...(!kunaResources.hasPending
? {}
: {
cleanup: {
reason: "Kuna version-probe process cleanup is unconfirmed.",
resources: kunaResources.cleanupResources ?? [],
},
}),
});
kunaResources.bindRetainedFailure(error);
return err(error);
}
if (!(cause instanceof RegularFileCleanupFailure))
return err(failure("health", cause));
const error = kunaFileCleanupError(cause, "health");
fileCloseScope.bindFailure(cause, error);
kunaResources.bindFailure(cause, error);
return err(error);
}
}
+3 -3
View File
@@ -8,7 +8,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type {
AnalysisClient,
ExecutionOptions,
FileCloseResourceOwner,
RetainedResourceScope,
} from "../application/AnalysisProvider.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
@@ -21,7 +21,7 @@ import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
/** Own deferred client creation while callers wait independently and close joins it. */
export class AnalysisClientCreation {
readonly #inspectionResources = new ArtifactResourceScope();
readonly #inspectionOwner: FileCloseResourceOwner = {
readonly #inspectionOwner: RetainedResourceScope = {
retain: (owner, resource) =>
this.#inspectionResources.retain({
kind: "lease",
@@ -44,7 +44,7 @@ export class AnalysisClientCreation {
private readonly providerId: string,
private readonly create: (
signal: AbortSignal,
resourceOwner: FileCloseResourceOwner,
resourceOwner: RetainedResourceScope,
) => Promise<AnalysisClient>,
) {}
+2 -2
View File
@@ -1,6 +1,6 @@
import type {
AnalysisClient,
FileCloseResourceOwner,
RetainedResourceScope,
ProviderRuntimeLineageSnapshot,
ProviderRequestActivitySnapshot,
} from "../application/AnalysisProvider.js";
@@ -29,7 +29,7 @@ export class DeferredAnalysisClient implements AnalysisClient {
constructor(
loadClient: (
signal: AbortSignal,
resourceOwner: FileCloseResourceOwner,
resourceOwner: RetainedResourceScope,
) => Promise<AnalysisClient>,
providerId: string,
) {
+2 -2
View File
@@ -1,6 +1,6 @@
import { jsonValueSchema } from "../domain/jsonValue.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { FileCloseResourceOwner } from "../application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
@@ -9,7 +9,7 @@ import { OwnedCommandFailure } from "../process/OwnedCommand.js";
/** Transfer exact helper and command cleanup capabilities to the caller's scope. */
export const retainOwnedCommandCleanup = (
cause: unknown,
resourceOwner: FileCloseResourceOwner,
resourceOwner: RetainedResourceScope,
providerId: string,
operation: string,
): readonly AnalysisError[] => {
@@ -0,0 +1,139 @@
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { expect } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import {
cleanupOwnedProcessGroup,
type OwnedProcessGroup,
} from "../../../src/process/ProcessOwnership.js";
import { observeOwnedProcessGroups } from "../../../src/process/ProcessOwnershipObservation.js";
cliTest.skipIf(!["darwin", "linux"].includes(process.platform))(
"doctor returns bounded Kuna version diagnostics when an exited probe's descendant holds its output pipe",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-kuna-doctor-held-output-");
const home = join(root, "home");
const executable = join(root, "kuna");
const marker = join(root, "probe-owner.json");
await mkdir(home);
await writeFile(
executable,
`#!${process.execPath}
const fs = require("node:fs");
const { spawn } = require("node:child_process");
if (process.argv[2] !== "--version") process.exit(7);
const child = spawn(process.execPath, ["-e", ${JSON.stringify(
`process.on("SIGTERM", () => {}); setTimeout(() => process.exit(0), 28000);`,
)}], { stdio: "inherit" });
fs.writeFileSync(${JSON.stringify(marker)}, JSON.stringify({
leaderPid: process.pid,
descendantPid: child.pid,
runId: process.env.REA_PROCESS_RUN_ID,
}));
process.stdout.write("kuna 1.833\\n");
process.stderr.write("probe stderr\\n");
process.exit(0);
`,
{ mode: 0o700 },
);
await chmod(executable, 0o700);
const pending = cli.run({
arguments: ["doctor", "--provider", "kuna", "--json"],
environment: {
HOME: home,
USERPROFILE: home,
HOPPER_LAUNCHER_PATH: join(root, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(root, "missing-ghidra"),
REA_KUNA_COMMAND: executable,
REA_KUNA_CACHE_DIR: join(root, "kuna-cache"),
REA_LOG_LEVEL: "silent",
},
// Leave room for the production probe deadline, while still bounding a
// regression that waits for the descendant's finite fallback expiry.
timeoutMs: 20_000,
});
let ownership: OwnedProcessGroup | undefined;
const startedAt = Date.now();
try {
let markerContents: string | undefined;
for (let attempt = 0; attempt < 100; attempt++) {
try {
markerContents = await readFile(marker, "utf8");
break;
} catch (cause: unknown) {
if (
!(
cause instanceof Error &&
"code" in cause &&
cause.code === "ENOENT"
)
)
throw cause;
}
await delay(50);
}
expect(markerContents, "fake Kuna probe did not start").toBeDefined();
const observed = z
.strictObject({
leaderPid: z.number().int().positive(),
descendantPid: z.number().int().positive(),
runId: z.string().min(1),
})
.parse(JSON.parse(markerContents!));
ownership = {
leaderPid: observed.leaderPid,
processGroupId: observed.leaderPid,
runId: observed.runId,
};
const result = await pending;
expect(Date.now() - startedAt).toBeLessThan(16_000);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
providerInspections: expect.arrayContaining([
expect.objectContaining({
id: "kuna",
available: false,
checks: expect.arrayContaining([
expect.objectContaining({
name: "installation",
ok: false,
code: "version_unresolved",
details: expect.objectContaining({
probe_failure_reason: "timeout",
stop_requested: true,
exit_code: 0,
signal: null,
stdout: "kuna 1.833\n",
stderr: "probe stderr\n",
stdout_bytes: 11,
stdout_observed_bytes: 11,
stderr_bytes: 13,
stderr_observed_bytes: 13,
diagnostic_truncated: false,
output_incomplete: false,
output_truncated: false,
deadline_ms: 10_000,
}),
}),
]),
}),
]),
});
const groups = await observeOwnedProcessGroups(ownership.runId, [
ownership.processGroupId,
]);
expect(groups.get(ownership.processGroupId)).toEqual({ state: "empty" });
} finally {
if (ownership !== undefined) {
expect((await cleanupOwnedProcessGroup(ownership)).cleaned).toBe(true);
}
await pending;
}
},
);
@@ -1,7 +1,7 @@
import { expect, it as test } from "vitest";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaFileCloseOwner.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaResourceScope.js";
import { KunaTargetCommands } from "../../../../src/kuna/KunaTargetCommands.js";
import { fakeKunaCalls } from "./fakeKuna.js";
import { session } from "./kunaSessionFixture.js";
@@ -7,17 +7,18 @@ import { session } from "./kunaSessionFixture.js";
const SCRIPT = String.raw`
import assert from "node:assert/strict";
import { ArtifactResourceScope } from "./dist/artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "./dist/artifacts/AnalysisCleanupLease.js";
import { inspectKunaInstallation } from "./dist/kuna/KunaInstallation.js";
import { KunaTargetCommands } from "./dist/kuna/KunaTargetCommands.js";
const fixture = JSON.parse(process.env.REA_TEST_KUNA_FIXTURE);
const fileCloseScope = new ArtifactResourceScope();
const kunaResources = new ArtifactResourceScope();
const installation = await inspectKunaInstallation(
{ REA_KUNA_COMMAND: fixture.kuna, REA_KUNA_CACHE_DIR: fixture.cacheRoot },
{
fileCloseResources: {
resourceScope: {
retain: (handle, resource) =>
fileCloseScope.retain({ kind: "file-handle", handle, resource }),
run: (operation) => fileCloseScope.run(operation),
kunaResources.retain({ kind: "lease", resource, lease: AnalysisCleanupLease.forCloseOwner(handle, resource) }),
run: (operation) => kunaResources.run(operation),
},
},
);
@@ -43,7 +44,7 @@ try {
} finally {
controller.abort();
assert.equal((await commands.close()).ok, true);
await fileCloseScope.close();
await kunaResources.close();
}
`;
@@ -3,10 +3,11 @@ import { join } from "node:path";
import { expect, it as test } from "vitest";
import { inspectKunaProviderOwned as inspectKunaProvider } from "./kunaFileCloseOwner.js";
import { inspectKunaProviderOwned as inspectKunaProvider } from "./kunaResourceScope.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { fakeKunaCalls, writeFakeKuna } from "./fakeKuna.js";
import { createSystemDoctorHost } from "../../../../src/doctorRuntime.js";
import { runDoctorOwned } from "../../../../src/application/Doctor.js";
import { AnalysisCancelledError } from "../../../../src/domain/analysisErrorCore.js";
import { STARTUP_OWNER_CLOSED } from "../../../../src/process/SharedStartupAttempt.js";
@@ -252,3 +253,34 @@ it("keeps a healthy composed Kuna inspection when the caller stays active", asyn
await host.close!();
}
});
it("projects the fake Kuna through the completed doctor workflow", async () => {
const root = await createTestTempDirectory("rea-kuna-public-doctor-");
const kuna = await writeFakeKuna(root);
const host = createSystemDoctorHost(
doctorEnvironment(root, kuna),
new AbortController().signal,
);
try {
const report = await runDoctorOwned(undefined, host, {
providers: ["kuna"],
skill: false,
clients: [],
});
expect(report.providerInspections).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "kuna",
available: true,
providerVersion: "1.833",
checks: expect.arrayContaining([
expect.objectContaining({ name: "installation", ok: true }),
]),
}),
]),
);
expect(await fakeKunaCalls(kuna)).toHaveLength(1);
} finally {
await host.close!();
}
});
@@ -1,41 +0,0 @@
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import type { FileCloseResourceOwner } from "../../../../src/application/AnalysisProvider.js";
import type { KunaInstallationOptions } from "../../../../src/kuna/KunaInstallation.js";
import { inspectKunaInstallation } from "../../../../src/kuna/KunaInstallation.js";
import { inspectKunaProvider } from "../../../../src/kuna/KunaDoctor.js";
export const withKunaFileCloseOwner = async <T>(
operation: (owner: FileCloseResourceOwner) => Promise<T>,
): Promise<T> => {
const scope = new ArtifactResourceScope();
const owner: FileCloseResourceOwner = {
retain: (handle, resource) =>
scope.retain({
kind: "lease",
resource,
lease: { release: () => handle.close() },
}),
run: (work) => scope.run(work),
};
try {
return await operation(owner);
} finally {
await scope.close();
}
};
export const inspectKunaInstallationOwned = (
environment: Readonly<NodeJS.ProcessEnv>,
options: Omit<KunaInstallationOptions, "fileCloseResources"> = {},
) =>
withKunaFileCloseOwner((fileCloseResources) =>
inspectKunaInstallation(environment, { ...options, fileCloseResources }),
);
export const inspectKunaProviderOwned = (
environment: Readonly<NodeJS.ProcessEnv>,
platform?: NodeJS.Platform,
) =>
withKunaFileCloseOwner((fileCloseResources) =>
inspectKunaProvider(environment, platform, { fileCloseResources }),
);
@@ -14,7 +14,7 @@ import { AnalysisError } from "../../../../src/domain/analysisErrorBase.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import type { FileCloseResourceOwner } from "../../../../src/application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../../../../src/application/AnalysisProvider.js";
const injected = vi.hoisted(() => ({
failNextClose: false,
@@ -125,7 +125,7 @@ import { composeBinarySession } from "../../../../src/application/binary/BinaryS
import { KunaTargetCommands } from "../../../../src/kuna/KunaTargetCommands.js";
import { KunaProjectCache } from "../../../../src/kuna/KunaProjectCache.js";
import { inspectKunaProvider } from "../../../../src/kuna/KunaDoctor.js";
import { inspectKunaInstallationOwned } from "./kunaFileCloseOwner.js";
import { inspectKunaInstallationOwned } from "./kunaResourceScope.js";
import { fakeKunaCalls } from "./fakeKuna.js";
import { run, session } from "./kunaSessionFixture.js";
@@ -575,7 +575,7 @@ it.skipIf(process.platform === "win32")(
injected.closeCalls = 0;
injected.failNextClose = true;
const resources = new ArtifactResourceScope();
const fileCloseResources: FileCloseResourceOwner = {
const resourceScope: RetainedResourceScope = {
retain: (owner, resource) =>
resources.retain({
kind: "lease",
@@ -591,7 +591,7 @@ it.skipIf(process.platform === "win32")(
REA_KUNA_CACHE_DIR: fixture.cacheRoot,
},
process.platform,
{ fileCloseResources },
{ resourceScope },
);
expect(inspection.checks[0]).toMatchObject({
ok: false,
@@ -678,7 +678,7 @@ it.skipIf(process.platform === "win32").each(
const controller = new AbortController();
const reason = { request: "cancel completed Kuna installation read" };
const resources = new ArtifactResourceScope();
const fileCloseResources: FileCloseResourceOwner = {
const resourceScope: RetainedResourceScope = {
retain: (owner, resource) =>
resources.retain({
kind: "lease",
@@ -700,7 +700,7 @@ it.skipIf(process.platform === "win32").each(
REA_KUNA_CACHE_DIR: fixture.cacheRoot,
},
process.platform,
{ fileCloseResources, signal: controller.signal },
{ resourceScope, signal: controller.signal },
).then(
(inspection) => {
expect(closeRefusal).toBe(true);
@@ -718,7 +718,7 @@ it.skipIf(process.platform === "win32").each(
const result =
consumer === "profile"
? await fixture.provider.resolveAnalysisProfile(fixture.target, {
fileCloseResources,
resourceScope,
signal: controller.signal,
})
: await client!.execute(
@@ -12,9 +12,10 @@ import { expect, it as test } from "vitest";
import { kunaInstallationCancellation } from "../../../../src/kuna/KunaInstallation.js";
import { readKunaConfiguration } from "../../../../src/kuna/KunaConfiguration.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaFileCloseOwner.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaResourceScope.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { fakeKunaCalls, writeFakeKuna } from "./fakeKuna.js";
import { makeLauncher, ownedGroupCleanup } from "./kunaVersionProbeProcess.js";
const it = test.skipIf(process.platform === "win32");
@@ -98,7 +99,7 @@ it("reports a failed version probe with its exit status", async () => {
error: {
code: "version_unresolved",
reason: expect.stringContaining("version probe failed"),
diagnostics: { exit_code: 3, killed: false },
diagnostics: { exit_code: 3, stop_requested: true },
},
});
});
@@ -210,19 +211,19 @@ it("stops a version probe that ignores SIGTERM instead of waiting on it", async
const result = await inspectKunaInstallation(
{ REA_KUNA_COMMAND: kuna },
// Long enough for Node to start the fake and install its SIGTERM handler.
{ probeTimeoutMs: 1500 },
// Allow owned-process identity preparation, then exercise the stopped child.
{ probeTimeoutMs: 4_000 },
);
expect(result).toMatchObject({
ok: false,
error: {
code: "version_unresolved",
diagnostics: { killed: true, deadline_ms: 1500 },
diagnostics: { stop_requested: true, deadline_ms: 4_000 },
},
});
// The deadline plus ExecFileOutput's bounded kill grace, never an open-ended wait.
expect(Date.now() - started).toBeLessThan(15_000);
// The owned process-group stop is bounded and must not wait on inherited output forever.
expect(Date.now() - started).toBeLessThan(10_000);
const pid = (await fakeKunaCalls(kuna))[0]?.pid ?? 0;
expect(() => process.kill(pid, 0)).toThrow();
}, 20_000);
@@ -232,6 +233,24 @@ it("keeps a version deadline ahead of cancellation during its joined stop", asyn
const command = join(root, "probe.cjs");
const stopped = join(root, "stopped");
const release = join(root, "release");
const launcher = makeLauncher(async (_child, pid) => {
process.kill(-pid, "SIGTERM");
const waitFor = async (path: string) => {
const deadline = Date.now() + 2_000;
while (Date.now() < deadline) {
try {
await readFile(path);
return;
} catch {
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
throw new Error(`Timed out waiting for ${path}`);
};
await waitFor(stopped);
await waitFor(release);
return ownedGroupCleanup(pid);
});
await (
await import("node:fs/promises")
).writeFile(
@@ -252,7 +271,11 @@ setTimeout(() => process.exit(9), 10000).unref();
REA_KUNA_COMMAND: command,
REA_KUNA_CACHE_DIR: join(root, "cache"),
},
{ signal: controller.signal, probeTimeoutMs: 1000 },
{
signal: controller.signal,
probeTimeoutMs: 1_000,
processLauncher: launcher.launcher,
},
);
const outcome = pending.then(
(value) => ({ kind: "result" as const, value }),
@@ -277,7 +300,7 @@ setTimeout(() => process.exit(9), 10000).unref();
ok: false,
error: {
code: "version_unresolved",
diagnostics: { deadline_ms: 1000, killed: true },
diagnostics: { deadline_ms: 1000, stop_requested: true },
},
},
});
@@ -61,7 +61,7 @@ vi.mock("../../../../src/filesystem/RegularFile.js", async (importOriginal) => {
});
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import type { FileCloseResourceOwner } from "../../../../src/application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../../../../src/application/AnalysisProvider.js";
import {
inspectKunaInstallation,
kunaInstallationCancellation,
@@ -91,7 +91,7 @@ it
const root = await createTestTempDirectory("rea-kuna-inspection-lifetime-");
const executable = await writeFakeKuna(root);
const scope = new ArtifactResourceScope();
const fileCloseResources: FileCloseResourceOwner = {
const resourceScope: RetainedResourceScope = {
retain: (owner, resource) =>
scope.retain({
kind: "lease",
@@ -103,7 +103,7 @@ it
const controller = new AbortController();
const inspection = inspectKunaInstallation(
{ REA_KUNA_COMMAND: executable },
{ fileCloseResources, signal: controller.signal },
{ resourceScope, signal: controller.signal },
);
const inspectionOutcome = inspection.then(
@@ -14,7 +14,7 @@ import { join } from "node:path";
import { expect, it as test, vi } from "vitest";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaFileCloseOwner.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaResourceScope.js";
import { cleanupOwnedProcessGroup } from "../../../../src/process/ProcessOwnership.js";
import { spawnOwnedProviderProcess } from "../../../../src/process/ProviderProcess.js";
import type { KunaInstallation } from "../../../../src/kuna/KunaInstallation.js";
@@ -5,7 +5,7 @@ import { join } from "node:path";
import { afterEach, expect, it as test, onTestFinished, vi } from "vitest";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaFileCloseOwner.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaResourceScope.js";
import { KunaProjectCache } from "../../../../src/kuna/KunaProjectCache.js";
import type { KunaExportStatus } from "../../../../src/kuna/KunaProjectExport.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
@@ -0,0 +1,32 @@
import { createInspectionResources } from "../../../fixtures/inspectionResources.js";
import type { RetainedResourceScope } from "../../../../src/application/AnalysisProvider.js";
import type { KunaInstallationOptions } from "../../../../src/kuna/KunaInstallation.js";
import { inspectKunaInstallation } from "../../../../src/kuna/KunaInstallation.js";
import { inspectKunaProvider } from "../../../../src/kuna/KunaDoctor.js";
export const withKunaResourceScope = async <T>(
operation: (owner: RetainedResourceScope) => Promise<T>,
): Promise<T> => {
const scope = createInspectionResources();
try {
return await operation(scope.owner);
} finally {
await scope.close();
}
};
export const inspectKunaInstallationOwned = (
environment: Readonly<NodeJS.ProcessEnv>,
options: Omit<KunaInstallationOptions, "resourceScope"> = {},
) =>
withKunaResourceScope((resourceScope) =>
inspectKunaInstallation(environment, { ...options, resourceScope }),
);
export const inspectKunaProviderOwned = (
environment: Readonly<NodeJS.ProcessEnv>,
platform?: NodeJS.Platform,
) =>
withKunaResourceScope((resourceScope) =>
inspectKunaProvider(environment, platform, { resourceScope }),
);
@@ -2,7 +2,7 @@ import { setImmediate } from "node:timers/promises";
import { expect, it as test, vi } from "vitest";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaFileCloseOwner.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaResourceScope.js";
import { KunaRunSlots } from "../../../../src/kuna/KunaRunSlots.js";
import { KunaTargetCommands } from "../../../../src/kuna/KunaTargetCommands.js";
import { spawnOwnedProviderProcess } from "../../../../src/process/ProviderProcess.js";
@@ -9,7 +9,7 @@ import { KunaProvider } from "../../../../src/kuna/KunaProvider.js";
import { cleanupOwnedProcessGroup } from "../../../../src/process/ProcessOwnership.js";
import { spawnOwnedProviderProcess } from "../../../../src/process/ProviderProcess.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { withKunaFileCloseOwner } from "./kunaFileCloseOwner.js";
import { withKunaResourceScope } from "./kunaResourceScope.js";
import {
fakeKunaCalls,
writeFakeKuna,
@@ -54,8 +54,8 @@ export const session = async (
...(singleFunctionRuns === undefined ? {} : { singleFunctionRuns }),
},
);
const profile = await withKunaFileCloseOwner((fileCloseResources) =>
provider.resolveAnalysisProfile(target, { fileCloseResources }),
const profile = await withKunaResourceScope((resourceScope) =>
provider.resolveAnalysisProfile(target, { resourceScope }),
);
if (!profile.ok || profile.value.profile === null)
throw new Error("profile did not resolve");
@@ -6,7 +6,7 @@ import {
options,
} from "../../../../src/application/Setup.fixture.js";
import { runSetup } from "../../../../src/application/Setup.js";
import { inspectKunaProviderOwned } from "./kunaFileCloseOwner.js";
import { inspectKunaProviderOwned } from "./kunaResourceScope.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { writeFakeKuna } from "./fakeKuna.js";
@@ -0,0 +1,492 @@
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { chmod, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { expect, it } from "vitest";
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import {
createKunaResourceScope,
inspectKunaInstallation,
kunaInstallationCancellation,
} from "../../../../src/kuna/KunaInstallation.js";
import { KunaProvider } from "../../../../src/kuna/KunaProvider.js";
import type { RetainedResourceScope } from "../../../../src/application/AnalysisProvider.js";
import { ProviderAdapterError } from "../../../../src/domain/providerAdapterError.js";
import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js";
import { RegularFileCleanupFailure } from "../../../../src/filesystem/OwnedRegularFile.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { makeLauncher, ownedGroupCleanup } from "./kunaVersionProbeProcess.js";
const writeProbe = async (
root: string,
modeFile: string,
childStarted: string,
parentExited: string,
) => {
const executable = join(root, "kuna-probe");
await writeFile(
executable,
`#!${process.execPath}
const fs = require("node:fs");
const { spawn } = require("node:child_process");
const mode = fs.readFileSync(${JSON.stringify(modeFile)}, "utf8");
process.stdout.write("kuna 1.833\\n");
process.stderr.write("x".repeat(1024));
if (mode === "hold" || mode === "detached") {
const child = spawn(process.execPath, ["-e", ${JSON.stringify(
`const fs=require("node:fs");fs.writeFileSync(${JSON.stringify(childStarted)},"started");process.on("SIGTERM",()=>{});setInterval(()=>{},1000);`,
)}], { stdio: mode === "hold" ? "inherit" : "ignore" });
const poll = setInterval(() => {
if (!fs.existsSync(${JSON.stringify(childStarted)})) return;
clearInterval(poll);
fs.writeFileSync(${JSON.stringify(parentExited)}, "exited");
process.exit(0);
}, 5);
}
`,
{ mode: 0o700 },
);
await chmod(executable, 0o700);
await writeFile(modeFile, "hold");
return executable;
};
const resourceScopeFor = (
scope: ArtifactResourceScope,
): RetainedResourceScope => ({
retain: (owner, resource) =>
scope.retain({
kind: "lease",
resource,
lease: { release: () => owner.close() },
}),
run: (operation) => scope.run(operation),
});
const waitForFile = async (path: string) => {
const deadline = Date.now() + 3_000;
while (Date.now() < deadline) {
try {
await import("node:fs/promises").then(({ access }) => access(path));
return;
} catch {
await delay(10);
}
}
throw new Error(`Timed out waiting for test marker ${path}`);
};
const target = (path: string) => ({
path,
sha256: createHash("sha256").update("target").digest("hex"),
kind: "executable" as const,
format: "elf" as const,
architecture: "x86_64" as const,
availableArchitectures: ["x86_64" as const],
});
it.skipIf(process.platform === "win32")(
"bounds timeout settlement when a test-owned descendant holds stdout and preserves the captured version output",
async () => {
const root = await createTestTempDirectory("rea-kuna-held-version-pipe-");
const modeFile = join(root, "mode");
const childStarted = join(root, "child-started");
const parentExited = join(root, "parent-exited");
const executable = await writeProbe(
root,
modeFile,
childStarted,
parentExited,
);
const launcher = makeLauncher();
const scope = new ArtifactResourceScope();
const startedAt = Date.now();
try {
const result = await inspectKunaInstallation(
{ REA_KUNA_COMMAND: executable },
{
resourceScope: resourceScopeFor(scope),
processLauncher: launcher.launcher,
probeTimeoutMs: 3_000,
},
);
expect(result).toMatchObject({
ok: false,
error: {
code: "version_unresolved",
diagnostics: {
exit_code: 0,
signal: null,
stop_requested: true,
stdout: "kuna 1.833\n",
stderr: "x".repeat(1024),
stdout_bytes: 11,
stdout_observed_bytes: 11,
stderr_bytes: 1024,
stderr_observed_bytes: 1024,
diagnostic_truncated: false,
output_incomplete: false,
output_truncated: false,
deadline_ms: 3_000,
},
},
});
await waitForFile(childStarted);
await waitForFile(parentExited);
expect(Date.now() - startedAt).toBeLessThan(7_000);
expect(scope.hasPending).toBe(false);
expect(launcher.groups).toHaveLength(1);
} finally {
await scope.close();
}
},
);
it.skipIf(process.platform === "win32")(
"keeps a complete successful version capture unavailable until cleanup-only ownership is confirmed",
async () => {
const root = await createTestTempDirectory(
"rea-kuna-cleanup-only-version-owner-",
);
const modeFile = join(root, "mode");
const childStarted = join(root, "child-started");
const parentExited = join(root, "parent-exited");
const executable = await writeProbe(
root,
modeFile,
childStarted,
parentExited,
);
await writeFile(modeFile, "detached");
let cleanupAttempts = 0;
let allowCleanup = false;
const launcher = makeLauncher(async (_child, pid) => {
cleanupAttempts += 1;
if (!allowCleanup)
return { cleaned: false, reason: "controlled cleanup-only refusal" };
return ownedGroupCleanup(pid);
});
const scope = new ArtifactResourceScope();
const provider = new KunaProvider(
{ REA_KUNA_COMMAND: executable, REA_KUNA_CACHE_DIR: join(root, "cache") },
{ launcher: launcher.launcher },
);
const input = target(join(root, "target"));
await writeFile(input.path, "target");
const options = { resourceScope: resourceScopeFor(scope) };
try {
const first = await provider.resolveAnalysisProfile(input, options);
await waitForFile(childStarted);
await waitForFile(parentExited);
expect(first.ok).toBe(false);
if (first.ok)
throw new Error("Unconfirmed cleanup cannot admit a profile");
expect(first.error.diagnostics).toMatchObject({
exit_code: 0,
signal: null,
probe_failure_reason: "process",
stop_requested: true,
cleanup_incomplete: true,
stdout: "kuna 1.833\n",
stderr: "x".repeat(1024),
stdout_bytes: 11,
stdout_observed_bytes: 11,
stderr_bytes: 1024,
stderr_observed_bytes: 1024,
diagnostic_truncated: false,
output_incomplete: false,
output_truncated: false,
});
expect(scope.hasPending).toBe(true);
const blocked = await provider.resolveAnalysisProfile(input, options);
expect(blocked.ok).toBe(false);
expect(launcher.launches).toBe(1);
expect(cleanupAttempts).toBe(2);
allowCleanup = true;
const retried = await provider.resolveAnalysisProfile(input, options);
expect(retried.ok).toBe(true);
expect(launcher.launches).toBe(2);
expect(scope.hasPending).toBe(false);
expect(first.error.cleanupIncomplete).toBe(false);
} finally {
allowCleanup = true;
await scope.close();
}
},
);
it.skipIf(process.platform === "win32")(
"settles caller abort after direct-child exit with the exact reason and partial streams",
async () => {
const root = await createTestTempDirectory("rea-kuna-abort-version-pipe-");
const modeFile = join(root, "mode");
const childStarted = join(root, "child-started");
const parentExited = join(root, "parent-exited");
const executable = await writeProbe(
root,
modeFile,
childStarted,
parentExited,
);
const launcher = makeLauncher();
const scope = new ArtifactResourceScope();
const controller = new AbortController();
const reason = Object.freeze({
request: "cancel while inherited stdout stays open",
});
try {
const pending = inspectKunaInstallation(
{ REA_KUNA_COMMAND: executable },
{
resourceScope: resourceScopeFor(scope),
processLauncher: launcher.launcher,
signal: controller.signal,
probeTimeoutMs: 10_000,
},
);
await waitForFile(parentExited);
controller.abort(reason);
const cause = await pending.catch((error: unknown) => error);
const cancellation = kunaInstallationCancellation(cause);
expect(cancellation?.reason).toBe(reason);
expect(cancellation?.diagnostics).toMatchObject({
kuna_version_probe: {
status: "partial",
exit_code: 0,
signal: null,
stdout: "kuna 1.833\n",
stderr: "x".repeat(1024),
stdout_bytes: 11,
stdout_observed_bytes: 11,
stderr_bytes: 1024,
stderr_observed_bytes: 1024,
diagnostic_truncated: false,
output_incomplete: false,
output_truncated: false,
},
});
expect(scope.hasPending).toBe(false);
} finally {
controller.abort(reason);
await scope.close();
}
},
);
it.skipIf(process.platform === "win32")(
"retains incomplete probe cleanup, blocks another profile, then retries only that exact process group",
async () => {
const root = await createTestTempDirectory(
"rea-kuna-retained-version-owner-",
);
const modeFile = join(root, "mode");
const childStarted = join(root, "child-started");
const parentExited = join(root, "parent-exited");
const executable = await writeProbe(
root,
modeFile,
childStarted,
parentExited,
);
let cleanupAttempts = 0;
let allowCleanup = false;
const launcher = makeLauncher(async (_child, pid) => {
cleanupAttempts += 1;
if (!allowCleanup)
return { cleaned: false, reason: "controlled stop refusal" };
return ownedGroupCleanup(pid);
});
const scope = new ArtifactResourceScope();
const unrelated = spawn(
process.execPath,
["-e", "setInterval(()=>{},1000)"],
{
detached: true,
stdio: "ignore",
},
);
await new Promise<void>((resolve, reject) => {
unrelated.once("spawn", resolve);
unrelated.once("error", reject);
});
const provider = new KunaProvider(
{ REA_KUNA_COMMAND: executable, REA_KUNA_CACHE_DIR: join(root, "cache") },
{ launcher: launcher.launcher, probeTimeoutMs: 2_500 },
);
const input = target(join(root, "target"));
await writeFile(input.path, "target");
const options = { resourceScope: resourceScopeFor(scope) };
try {
const first = await provider.resolveAnalysisProfile(input, options);
expect(first.ok).toBe(false);
if (first.ok)
throw new Error("Expected the held version probe to time out");
const primary = first.error;
expect(primary.cleanupIncomplete).toBe(true);
expect(scope.hasPending).toBe(true);
await waitForFile(parentExited);
const blocked = await provider.resolveAnalysisProfile(input, options);
expect(blocked.ok).toBe(false);
expect(launcher.launches).toBe(1);
expect(cleanupAttempts).toBe(2);
if (blocked.ok)
throw new Error("Expected retained cleanup to block a new probe");
expect(blocked.error.cleanupIncomplete).toBe(true);
allowCleanup = true;
await writeFile(modeFile, "success");
const retried = await provider.resolveAnalysisProfile(input, options);
expect(retried.ok).toBe(true);
expect(launcher.launches).toBe(2);
expect(scope.hasPending).toBe(false);
expect(primary.cleanupIncomplete).toBe(false);
expect(() => process.kill(unrelated.pid!, 0)).not.toThrow();
} finally {
allowCleanup = true;
await scope.close();
unrelated.kill("SIGKILL");
}
},
);
it("keeps a file-operation primary linked to every retained file and process owner", async () => {
const scope = new ArtifactResourceScope();
const resourceScope = resourceScopeFor(scope);
const kunaResources = createKunaResourceScope(resourceScope);
let processCanClose = false;
let processResources = ["initial Kuna process group"];
const processOwner = {
close: async () => {
processResources = ["remaining Kuna descendant"];
if (!processCanClose) throw new Error("process stop unconfirmed");
},
get cleanupResources() {
return processResources;
},
};
const fileHandle = new OwnedFileHandle({
fd: 17,
close: async () => {},
}) as unknown as OwnedFileHandle;
kunaResources.owner.retain(fileHandle, "/tmp/kuna-test-source");
kunaResources.owner.retain(processOwner, "kuna test process");
const primary = new ProviderAdapterError("kuna", "kuna:profile", {
userMessage: "The admitted Kuna version probe failed.",
cleanup: {
reason: "The source descriptor close is unconfirmed.",
resources: ["/tmp/kuna-test-source"],
},
});
const fileFailure = new RegularFileCleanupFailure(
"/tmp/kuna-test-source",
fileHandle,
{ kind: "failed", cause: primary },
new Error("descriptor close refused"),
);
kunaResources.bindFailure(fileFailure, primary);
expect(primary.cleanupIncomplete).toBe(true);
expect(primary.cleanupResources).toEqual([
"/tmp/kuna-test-source",
"initial Kuna process group",
]);
await expect(scope.retryCleanup()).rejects.toMatchObject({
failedOwners: expect.any(Array),
});
expect(primary.cleanupIncomplete).toBe(true);
expect(primary.cleanupResources).toEqual(["remaining Kuna descendant"]);
processCanClose = true;
await scope.retryCleanup();
expect(primary.cleanupIncomplete).toBe(false);
expect(primary.userMessage).toBe("The admitted Kuna version probe failed.");
await scope.close();
});
it("keeps unknown cleanup labels unknown instead of substituting the lookup key", async () => {
const scope = new ArtifactResourceScope();
const kunaResources = createKunaResourceScope(resourceScopeFor(scope));
const unknownOwner = {
close: async () => {
throw new Error("stop remains unconfirmed");
},
cleanupResources: undefined,
};
kunaResources.owner.retain(unknownOwner, "display-only lookup key");
const primary = new ProviderAdapterError("kuna", "kuna:profile", {
userMessage: "Cleanup is still pending.",
cleanup: { reason: "stop remains unconfirmed", resources: [] },
});
kunaResources.bindRetainedFailure(primary);
expect(kunaResources.hasPending).toBe(true);
expect(kunaResources.cleanupResources).toBeUndefined();
await scope.close().catch(() => undefined);
});
it.skipIf(process.platform === "win32")(
"keeps caller cancellation primary while cancellation cleanup is held and then retried",
async () => {
const root = await createTestTempDirectory(
"rea-kuna-final-cancel-cleanup-",
);
const modeFile = join(root, "mode");
const childStarted = join(root, "child-started");
const parentExited = join(root, "parent-exited");
const executable = await writeProbe(
root,
modeFile,
childStarted,
parentExited,
);
const cleanupGate = (() => {
let release!: () => void;
let entered!: () => void;
const held = new Promise<void>((resolve) => (release = resolve));
const started = new Promise<void>((resolve) => (entered = resolve));
return { held, started, release, entered };
})();
const launcher = makeLauncher(async (_child, pid) => {
cleanupGate.entered();
await cleanupGate.held;
return ownedGroupCleanup(pid);
});
const scope = new ArtifactResourceScope();
const controller = new AbortController();
const reason = Object.freeze({
request: "abort during final process cleanup",
});
try {
const pending = inspectKunaInstallation(
{ REA_KUNA_COMMAND: executable },
{
resourceScope: resourceScopeFor(scope),
processLauncher: launcher.launcher,
signal: controller.signal,
probeTimeoutMs: 10_000,
},
);
await waitForFile(parentExited);
controller.abort(reason);
await cleanupGate.started;
let settled = false;
void pending.then(
() => (settled = true),
() => (settled = true),
);
await delay(25);
expect(settled).toBe(false);
cleanupGate.release();
const cause = await pending.catch((error: unknown) => error);
expect(kunaInstallationCancellation(cause)?.reason).toBe(reason);
expect(scope.hasPending).toBe(false);
} finally {
controller.abort(reason);
cleanupGate.release();
await scope.close();
}
},
);
@@ -0,0 +1,91 @@
import { spawn, type ChildProcess } from "node:child_process";
import { setTimeout as delay } from "node:timers/promises";
import type { ProcessCleanupResult } from "../../../../src/process/ProcessOwnership.js";
import type {
OwnedProviderProcessSpawnOptions,
SpawnedOwnedProviderProcess,
} from "../../../../src/process/ProviderProcess.js";
/** Cleanup authority for the exact detached group created by this test launcher. */
export const ownedGroupCleanup = async (
pid: number,
): Promise<ProcessCleanupResult> => {
let signaled = false;
try {
process.kill(-pid, "SIGTERM");
signaled = true;
} catch {
// The test-owned process group may have exited before cleanup began.
}
await delay(50);
try {
process.kill(-pid, "SIGKILL");
signaled = true;
} catch {
// An empty process group confirms cleanup.
}
const deadline = Date.now() + 1_000;
while (Date.now() < deadline) {
try {
process.kill(-pid, 0);
} catch {
return { cleaned: true, signaled };
}
await delay(10);
}
return {
cleaned: false,
reason: `test-owned process group ${pid} remains live`,
};
};
export const makeLauncher = (
cleanup: (
child: ChildProcess,
pid: number,
) => Promise<ProcessCleanupResult> = (_child, pid) => ownedGroupCleanup(pid),
) => {
let launches = 0;
const groups: number[] = [];
const launcher = async (
options: OwnedProviderProcessSpawnOptions,
): Promise<SpawnedOwnedProviderProcess> => {
options.signal?.throwIfAborted();
const child = spawn(options.command, [...options.arguments], {
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
env: {
...options.hostEnvironment,
...options.env,
REA_PROCESS_RUN_ID: options.runId,
},
detached: true,
stdio: ["ignore", "pipe", "pipe"],
});
await new Promise<void>((resolve, reject) => {
child.once("spawn", resolve);
child.once("error", reject);
});
const pid = child.pid;
if (pid === undefined)
throw new Error("test launcher did not receive a PID");
launches += 1;
groups.push(pid);
return {
process: child,
ownership: {
runId: options.runId,
leaderPid: pid,
processGroupId: pid,
},
cleanup: () => cleanup(child, pid),
};
};
return {
launcher,
groups,
get launches() {
return launches;
},
};
};
+2 -2
View File
@@ -1,11 +1,11 @@
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../../src/artifacts/AnalysisCleanupLease.js";
import type { FileCloseResourceOwner } from "../../src/application/AnalysisProvider.js";
import type { RetainedResourceScope } from "../../src/application/AnalysisProvider.js";
/** Test-scoped real inspection leases; every retained owner is closed by the fixture. */
export const createInspectionResources = () => {
const scope = new ArtifactResourceScope();
const owner: FileCloseResourceOwner = {
const owner: RetainedResourceScope = {
retain: (resourceOwner, resource) =>
scope.retain({
kind: "lease",