mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
* fix(android): retain snapshot descriptors when close fails Preserve the stable-file diagnosis or completed copy facts when an APK or engine snapshot close rejects, and retry that exact owner before workspace retirement. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(kuna): retain span descriptors when close fails Preserve completed span text or the short-read diagnostic when close rejects, and keep session close incomplete until that handle is released. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(android): attribute snapshot cleanup to the descriptor that failed A finished copy can be nested under another descriptor's rejected close. Walk that chain, keep the snapshot hash on the partial observation, and name every descriptor that is still open. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(kuna): gate span admission until a retained close releases Overlapping requests can pass the entry cleanup check before any lease exists. Hold each span open through its close so the next open retries that descriptor instead of admitting another one. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(android): preserve nested snapshot facts when both closes fail Walk nested close failures so a finished copy and a changed-APK diagnosis survive when the source descriptor also rejects close. Attribute cleanup to each descriptor that failed, and classify a nested close cancellation. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(kuna): release recovered span leases before the next open Serialize span admission and close without holding the queue across the byte read. Recheck the caller signal after that wait, and drop a recovered lease from the resource scope as well as the span map. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(android): retain a source descriptor when admission close fails openRegularFile can reject after the descriptor opens. If that close also fails, keep the owner on the snapshot cleanup map instead of dropping it when adoption never runs. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(kuna): keep cancellation when a completed span close fails Pass the caller signal into span finalization. If the read already finished and close then rejects, report cancellation as the primary error and keep the span text with the retained descriptor. Co-authored-by: morluto <morluto@users.noreply.github.com> * style: format Android and Kuna close-ownership changes CI format:check rejected the four files touched by the close-ownership fixes. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(android): keep the snapshot hash inside a nested cancellation A destination close that succeeds after abort throws RegularFileCancelled with the finished copy. When the source close then rejects, recover that copy so cancellation keeps the digest and file-snapshot-hash observation. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(android): keep the snapshot hash on a direct close cancellation A finished copy whose descriptor closes successfully after abort is a top-level RegularFileCancelled. Attach its digest and file-snapshot-hash observation to that cancellation. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(kuna): preserve the caller abort reason after a successful span close A completed read whose close then succeeds throws a cancellation wrapper. Return AnalysisCancelledError with the caller's abort reason instead of that wrapper. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
646 lines
20 KiB
TypeScript
646 lines
20 KiB
TypeScript
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
|
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.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";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js";
|
|
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
|
|
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisInputError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
|
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
|
|
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import {
|
|
createJadxWorkspaceRoot,
|
|
type JadxWorkspaceRoot,
|
|
} from "./JadxWorkspaceRoot.js";
|
|
import {
|
|
snapshotAndroidEngine,
|
|
snapshotAndroidTarget,
|
|
} from "./AndroidTargetSnapshot.js";
|
|
import {
|
|
hashStableFile,
|
|
stableFileHashCause,
|
|
} from "../filesystem/StableFileHash.js";
|
|
import type { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
|
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
|
|
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
|
|
import {
|
|
inspectJadxAvailability,
|
|
resolveJadxConfigurationInputs,
|
|
resolveJadxOperationTimeout,
|
|
validateJadxConfiguration,
|
|
} from "./JadxConfiguration.js";
|
|
import type { JadxLauncher } from "./JadxMcpTransport.js";
|
|
|
|
import { JadxSession } from "./JadxSession.js";
|
|
import { captureJadxFailure } from "./JadxFailure.js";
|
|
|
|
const BRIDGE_SOURCE = fileURLToPath(
|
|
new URL("../../bridge/android/ReaJadxBridge.java", import.meta.url),
|
|
);
|
|
type Outcome = Awaited<ReturnType<AndroidAnalysisPort["execute"]>>;
|
|
|
|
const waitForTurn = (
|
|
predecessor: Promise<void>,
|
|
request: AndroidRequest,
|
|
signal?: AbortSignal,
|
|
): Promise<void> =>
|
|
new Promise((resolve, reject) => {
|
|
const abort = () =>
|
|
reject(
|
|
new AnalysisCancelledError(request.operation, {
|
|
cause: signal?.reason,
|
|
}),
|
|
);
|
|
signal?.addEventListener("abort", abort, { once: true });
|
|
if (signal?.aborted === true) abort();
|
|
void predecessor.then(() => {
|
|
signal?.removeEventListener("abort", abort);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
const androidTargetChanged = (
|
|
operation: string,
|
|
path: string,
|
|
cause?: unknown,
|
|
): AnalysisInputError =>
|
|
new AnalysisInputError(
|
|
operation,
|
|
cause === undefined ? undefined : { cause },
|
|
[
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_value",
|
|
message: `APK bytes changed after admission at ${path}; retry against a stable file.`,
|
|
},
|
|
],
|
|
);
|
|
|
|
const androidSourceChanged = (
|
|
operation: string,
|
|
source: string,
|
|
cause?: unknown,
|
|
): AnalysisCapabilityUnavailableError =>
|
|
new AnalysisCapabilityUnavailableError(
|
|
"jadx",
|
|
operation,
|
|
`Selected JADX input bytes changed during admission at ${source}; retry with a stable installation.`,
|
|
cause === undefined ? undefined : { cause },
|
|
);
|
|
|
|
const hashAndroidSource = async (
|
|
path: string,
|
|
operation: string,
|
|
signal: AbortSignal,
|
|
retainFailedClose: (owner: OwnedFileHandle) => void,
|
|
): Promise<string> => {
|
|
try {
|
|
return (await hashStableFile(path, { signal, retainFailedClose })).sha256;
|
|
} catch (cause: unknown) {
|
|
const physical = stableFileHashCause(cause);
|
|
if (physical instanceof RegularFileChangedError)
|
|
throw androidSourceChanged(operation, path, physical);
|
|
throw cause;
|
|
}
|
|
};
|
|
|
|
interface PendingCleanup {
|
|
readonly operation: string;
|
|
session: JadxSession | undefined;
|
|
root: JadxWorkspaceRoot | undefined;
|
|
readonly fileHandles: Map<OwnedFileHandle, string>;
|
|
readonly previousError: AnalysisError | undefined;
|
|
readonly operationCause: unknown;
|
|
reportedErrors: WeakRef<AnalysisError>[];
|
|
}
|
|
|
|
/** Every descriptor owner nested in one rejected close, without treating a later owner as the first. */
|
|
const retainedCleanupOwners = (
|
|
cause: unknown,
|
|
): ReadonlySet<OwnedFileHandle> => {
|
|
const owners = new Set<OwnedFileHandle>();
|
|
const visit = (value: unknown, seen: Set<unknown>): void => {
|
|
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
seen.add(value);
|
|
if (!(value instanceof RegularFileCleanupFailure)) return;
|
|
owners.add(value.owner);
|
|
if (value.outcome.kind === "failed") visit(value.outcome.cause, seen);
|
|
};
|
|
visit(cause, new Set());
|
|
return owners;
|
|
};
|
|
|
|
const cleanupResources = (resources: PendingCleanup): readonly string[] => [
|
|
...(resources.session === undefined
|
|
? []
|
|
: [resources.session.transport.runId]),
|
|
...(resources.root === undefined ? [] : [resources.root.path]),
|
|
...resources.fileHandles.values(),
|
|
];
|
|
|
|
/** The retained acquisition record owns both retry and current resource attribution. */
|
|
const cleanupFailure = (
|
|
resources: PendingCleanup,
|
|
cleanup: ProviderCleanupError,
|
|
): AnalysisError =>
|
|
(resources.previousError === undefined
|
|
? cleanup
|
|
: analysisErrorWithCleanupFailure(
|
|
resources.previousError,
|
|
cleanup,
|
|
resources.operation,
|
|
)
|
|
).bindCleanupResourceOwner({
|
|
get cleanupResources() {
|
|
return cleanupResources(resources);
|
|
},
|
|
});
|
|
|
|
const cleanup = async (
|
|
resources: PendingCleanup,
|
|
): Promise<Result<void, AnalysisError>> => {
|
|
const { session, root } = resources;
|
|
let sessionCloseFailed = false;
|
|
let sessionCloseCause: unknown;
|
|
try {
|
|
await session?.close();
|
|
resources.session = undefined;
|
|
} catch (cause) {
|
|
// Still drain independent hash descriptors, but keep the workspace until
|
|
// this session's process ownership is proven stopped.
|
|
sessionCloseFailed = true;
|
|
sessionCloseCause = cause;
|
|
}
|
|
const tracked = retainedCleanupOwners(resources.operationCause);
|
|
const trackedInMap = [...tracked].filter((owner) =>
|
|
resources.fileHandles.has(owner),
|
|
);
|
|
const fileCloseCauses: unknown[] = [];
|
|
for (const owner of resources.fileHandles.keys()) {
|
|
try {
|
|
await owner.close();
|
|
resources.fileHandles.delete(owner);
|
|
// One descriptor's release clears only that owner. The admission error
|
|
// stays incomplete until every owner from the same close failure is gone.
|
|
if (
|
|
trackedInMap.length > 0 &&
|
|
trackedInMap.every((candidate) => !resources.fileHandles.has(candidate))
|
|
)
|
|
resources.previousError?.resolveCleanup();
|
|
} catch (cause: unknown) {
|
|
fileCloseCauses.push(cause);
|
|
}
|
|
}
|
|
if (sessionCloseFailed || fileCloseCauses.length > 0) {
|
|
const cleanupCauses = [
|
|
...(sessionCloseFailed ? [sessionCloseCause] : []),
|
|
...fileCloseCauses,
|
|
];
|
|
const cleanupError = new ProviderCleanupError(
|
|
"jadx",
|
|
cleanupResources(resources),
|
|
{
|
|
reason: cleanupCauses
|
|
.map((failure) =>
|
|
failure instanceof Error ? failure.message : String(failure),
|
|
)
|
|
.join("; "),
|
|
cleanup_causes: cleanupCauses.map((failure) =>
|
|
failure instanceof Error ? failure.message : String(failure),
|
|
),
|
|
provider_cleanup: cleanupCauses.map((failure) =>
|
|
failure instanceof ProviderCleanupError
|
|
? (failure.diagnostics ?? null)
|
|
: null,
|
|
),
|
|
},
|
|
{
|
|
operation: resources.operation,
|
|
cause: new AggregateError(
|
|
cleanupCauses,
|
|
"JADX resource cleanup failed",
|
|
),
|
|
},
|
|
);
|
|
return err(cleanupFailure(resources, cleanupError));
|
|
}
|
|
try {
|
|
// Child descriptors are released above. The workspace root follows only
|
|
// after those owners confirm close.
|
|
await root?.close();
|
|
resources.root = undefined;
|
|
} catch (cause) {
|
|
const error = new ProviderCleanupError(
|
|
"jadx",
|
|
cleanupResources(resources),
|
|
{
|
|
reason: cause instanceof Error ? cause.message : String(cause),
|
|
},
|
|
{ operation: resources.operation, cause },
|
|
);
|
|
return err(cleanupFailure(resources, error));
|
|
}
|
|
return ok(undefined);
|
|
};
|
|
|
|
interface RetainedSession {
|
|
readonly key: string;
|
|
readonly root: JadxWorkspaceRoot;
|
|
readonly session: JadxSession;
|
|
readonly snapshot: string;
|
|
readonly jarHash: string;
|
|
readonly bridgeHash: string;
|
|
}
|
|
|
|
/** One serialized, immutable APK session per REA instance, retired on idle or shutdown. */
|
|
export class JadxProvider implements AndroidAnalysisPort {
|
|
#tail: Promise<void> = Promise.resolve();
|
|
#pendingCleanup: PendingCleanup | undefined;
|
|
#retained: RetainedSession | undefined;
|
|
#idle: NodeJS.Timeout | undefined;
|
|
readonly #shutdown = new AbortController();
|
|
readonly #prerequisites = new ArtifactResourceScope();
|
|
readonly #inspections = new Set<
|
|
Promise<Awaited<ReturnType<typeof inspectJadxAvailability>>>
|
|
>();
|
|
readonly #prerequisiteOwner: RetainedResourceScope = {
|
|
retain: (owner, resource) =>
|
|
this.#prerequisites.retain({
|
|
kind: "lease",
|
|
resource,
|
|
lease: AnalysisCleanupLease.forCloseOwner(owner, resource),
|
|
}),
|
|
run: (operation) => this.#prerequisites.run(operation),
|
|
};
|
|
#closePromise: Promise<void> | undefined;
|
|
constructor(
|
|
readonly environment: Readonly<Record<string, string | undefined>>,
|
|
readonly launcher?: JadxLauncher,
|
|
) {}
|
|
|
|
/** Report actual local prerequisites without starting JADX or opening a target. */
|
|
inspectAvailability(signal?: AbortSignal) {
|
|
const pending = inspectJadxAvailability(this.environment, {
|
|
resourceOwner: this.#prerequisiteOwner,
|
|
signal:
|
|
signal === undefined
|
|
? this.#shutdown.signal
|
|
: AbortSignal.any([signal, this.#shutdown.signal]),
|
|
});
|
|
this.#inspections.add(pending);
|
|
void pending.then(
|
|
() => this.#inspections.delete(pending),
|
|
() => this.#inspections.delete(pending),
|
|
);
|
|
return pending;
|
|
}
|
|
|
|
/** Serialize expensive work and bind both raw and normalized results to the APK. */
|
|
async execute(
|
|
target: BinaryTarget,
|
|
request: AndroidRequest,
|
|
options?: ExecutionOptions,
|
|
): Promise<Outcome> {
|
|
if (this.#shutdown.signal.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(request.operation, {
|
|
cause: this.#shutdown.signal.reason,
|
|
}),
|
|
);
|
|
const signal =
|
|
options?.signal === undefined
|
|
? this.#shutdown.signal
|
|
: AbortSignal.any([options.signal, this.#shutdown.signal]);
|
|
const predecessor = this.#tail;
|
|
let release: () => void = () => {};
|
|
this.#tail = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
try {
|
|
await waitForTurn(predecessor, request, signal);
|
|
} catch (cause) {
|
|
void predecessor.then(release);
|
|
return err(
|
|
cause instanceof AnalysisError
|
|
? cause
|
|
: new AnalysisCancelledError(request.operation, { cause }),
|
|
);
|
|
}
|
|
try {
|
|
if (signal.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(request.operation, {
|
|
cause: signal.reason,
|
|
}),
|
|
);
|
|
clearTimeout(this.#idle);
|
|
if (this.#pendingCleanup !== undefined) {
|
|
const retired = await this.#retire();
|
|
if (!retired.ok) return retired;
|
|
}
|
|
const outcome = await this.#execute(target, request, { signal });
|
|
if (outcome.ok) this.#scheduleIdle();
|
|
return outcome;
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
|
|
/** Cancel queued/active operations and join the owned engine and workspace cleanup. */
|
|
close(): Promise<void> {
|
|
this.#shutdown.abort();
|
|
clearTimeout(this.#idle);
|
|
this.#closePromise ??= this.#tail
|
|
.then(async () => {
|
|
await Promise.allSettled(this.#inspections);
|
|
const retired = await this.#retire();
|
|
try {
|
|
await this.#prerequisites.close();
|
|
} catch (cause: unknown) {
|
|
const cleanup = analysisClientFailureForOperation(
|
|
cause,
|
|
"jadx",
|
|
"close_binary",
|
|
);
|
|
if (!retired.ok)
|
|
throw analysisErrorWithCleanupFailure(
|
|
retired.error,
|
|
cleanup,
|
|
"close_binary",
|
|
).bindCleanupDependencies([retired.error, cleanup]);
|
|
throw cleanup;
|
|
}
|
|
if (!retired.ok) throw retired.error;
|
|
})
|
|
.catch((cause: unknown) => {
|
|
this.#closePromise = undefined;
|
|
throw cause;
|
|
});
|
|
return this.#closePromise;
|
|
}
|
|
|
|
async #retire(): Promise<Result<void, AnalysisError>> {
|
|
if (this.#pendingCleanup === undefined && this.#retained !== undefined) {
|
|
this.#pendingCleanup = {
|
|
operation: "close_binary",
|
|
session: this.#retained.session,
|
|
root: this.#retained.root,
|
|
fileHandles: new Map(),
|
|
previousError: undefined,
|
|
operationCause: undefined,
|
|
reportedErrors: [],
|
|
};
|
|
this.#retained = undefined;
|
|
}
|
|
if (this.#pendingCleanup === undefined) return ok(undefined);
|
|
const pending = this.#pendingCleanup;
|
|
const result = await cleanup(pending);
|
|
if (result.ok) {
|
|
for (const reportedError of pending.reportedErrors)
|
|
reportedError.deref()?.resolveCleanup();
|
|
pending.reportedErrors = [];
|
|
this.#pendingCleanup = undefined;
|
|
} else {
|
|
pending.reportedErrors = [
|
|
...pending.reportedErrors.filter(
|
|
(reportedError) => reportedError.deref() !== undefined,
|
|
),
|
|
new WeakRef(result.error),
|
|
];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#scheduleIdle(): void {
|
|
this.#idle = setTimeout(() => {
|
|
this.#tail = this.#tail.then(async () => {
|
|
await this.#retire();
|
|
});
|
|
}, 60_000);
|
|
this.#idle.unref();
|
|
}
|
|
|
|
async #execute(
|
|
target: BinaryTarget,
|
|
request: AndroidRequest,
|
|
options?: ExecutionOptions,
|
|
): Promise<Outcome> {
|
|
// The deadline is fixed per provider environment; an unusable value is
|
|
// refused before any file handle, workspace, or engine is acquired.
|
|
const timeoutMs = resolveJadxOperationTimeout(
|
|
this.environment,
|
|
request.operation,
|
|
);
|
|
if (!timeoutMs.ok) return timeoutMs;
|
|
const timeout = AbortSignal.timeout(timeoutMs.value);
|
|
const signal =
|
|
options?.signal === undefined
|
|
? timeout
|
|
: AbortSignal.any([options.signal, timeout]);
|
|
let root: JadxWorkspaceRoot | undefined;
|
|
let session: JadxSession | undefined;
|
|
const fileHandles: PendingCleanup["fileHandles"] = new Map();
|
|
const retainFailedClose =
|
|
(path: string) =>
|
|
(owner: OwnedFileHandle): void => {
|
|
fileHandles.set(owner, path);
|
|
};
|
|
try {
|
|
if (target.format !== "apk")
|
|
throw new AnalysisCapabilityUnavailableError(
|
|
"jadx",
|
|
request.operation,
|
|
`Target ${target.path} is ${target.format}; provide one standalone APK.`,
|
|
);
|
|
const configuration = await resolveJadxConfigurationInputs(
|
|
this.environment,
|
|
request.operation,
|
|
signal,
|
|
);
|
|
throwIfJadxInterrupted(signal);
|
|
const jarHash = await hashAndroidSource(
|
|
configuration.jar,
|
|
request.operation,
|
|
signal,
|
|
retainFailedClose(configuration.jar),
|
|
);
|
|
const bridgeHash = await hashAndroidSource(
|
|
BRIDGE_SOURCE,
|
|
request.operation,
|
|
signal,
|
|
retainFailedClose(BRIDGE_SOURCE),
|
|
);
|
|
let targetHash: string;
|
|
try {
|
|
targetHash = (
|
|
await hashStableFile(target.path, {
|
|
signal,
|
|
retainFailedClose: retainFailedClose(target.path),
|
|
})
|
|
).sha256;
|
|
} catch (cause: unknown) {
|
|
if (signal.aborted) throw cause;
|
|
if (cause instanceof RegularFileChangedError)
|
|
throw androidTargetChanged(request.operation, target.path, cause);
|
|
throw cause;
|
|
}
|
|
if (targetHash !== target.sha256)
|
|
throw androidTargetChanged(request.operation, target.path);
|
|
const key = JSON.stringify({
|
|
path: target.path,
|
|
sha256: target.sha256,
|
|
jar: configuration.jar,
|
|
jarHash,
|
|
bridgeHash,
|
|
java: configuration.java,
|
|
arguments: configuration.jvmArguments,
|
|
options: [
|
|
this.environment.JAVA_TOOL_OPTIONS,
|
|
this.environment._JAVA_OPTIONS,
|
|
this.environment.JDK_JAVA_OPTIONS,
|
|
this.environment.PATH,
|
|
this.environment.JAVA_HOME,
|
|
],
|
|
});
|
|
if (this.#retained !== undefined && this.#retained.key !== key) {
|
|
const retired = await this.#retire();
|
|
if (!retired.ok) return retired;
|
|
}
|
|
let retained = this.#retained;
|
|
if (retained === undefined) {
|
|
await validateJadxConfiguration(
|
|
configuration,
|
|
this.environment,
|
|
request.operation,
|
|
{
|
|
resourceOwner: this.#prerequisiteOwner,
|
|
signal,
|
|
operationDeadline: { signal: timeout, timeoutMs: timeoutMs.value },
|
|
},
|
|
);
|
|
root = await createJadxWorkspaceRoot();
|
|
const workspaceSignal =
|
|
root.failureSignal === undefined
|
|
? signal
|
|
: AbortSignal.any([signal, root.failureSignal]);
|
|
const retainSnapshot = (path: string, owner: OwnedFileHandle): void => {
|
|
fileHandles.set(owner, path);
|
|
};
|
|
const engine = await snapshotAndroidEngine(
|
|
configuration.jar,
|
|
jarHash,
|
|
root.path,
|
|
request.operation,
|
|
workspaceSignal,
|
|
retainSnapshot,
|
|
);
|
|
const snapshot = await snapshotAndroidTarget(
|
|
target.path,
|
|
target.sha256,
|
|
root.path,
|
|
request.operation,
|
|
workspaceSignal,
|
|
retainSnapshot,
|
|
);
|
|
const bridge = join(root.path, "ReaJadxBridge.java");
|
|
await copyFile(BRIDGE_SOURCE, bridge);
|
|
await chmod(bridge, 0o400);
|
|
const copiedBridgeHash = await hashAndroidSource(
|
|
bridge,
|
|
request.operation,
|
|
workspaceSignal,
|
|
retainFailedClose(bridge),
|
|
);
|
|
if (copiedBridgeHash !== bridgeHash)
|
|
throw androidSourceChanged(
|
|
request.operation,
|
|
BRIDGE_SOURCE,
|
|
undefined,
|
|
);
|
|
throwIfJadxInterrupted(workspaceSignal);
|
|
session = new JadxSession(
|
|
{
|
|
command: configuration.java,
|
|
arguments: [
|
|
...configuration.jvmArguments,
|
|
"--class-path",
|
|
engine.path,
|
|
bridge,
|
|
],
|
|
cwd: root.path,
|
|
hostEnvironment: this.environment,
|
|
signal: workspaceSignal,
|
|
},
|
|
this.launcher,
|
|
);
|
|
retained = {
|
|
key,
|
|
root,
|
|
session,
|
|
snapshot,
|
|
jarHash: engine.sha256,
|
|
bridgeHash,
|
|
};
|
|
}
|
|
root = retained.root;
|
|
session = retained.session;
|
|
const activeSignal =
|
|
root.failureSignal === undefined
|
|
? signal
|
|
: AbortSignal.any([signal, root.failureSignal]);
|
|
const { snapshot } = retained;
|
|
const outcome = ok(
|
|
await session.execute({
|
|
request,
|
|
target,
|
|
snapshot,
|
|
jarHash: retained.jarHash,
|
|
bridgeHash: retained.bridgeHash,
|
|
signal: activeSignal,
|
|
timeoutMs: timeoutMs.value,
|
|
}),
|
|
);
|
|
throwIfJadxInterrupted(activeSignal);
|
|
this.#retained = retained;
|
|
return outcome;
|
|
} catch (cause) {
|
|
const failure = captureJadxFailure({
|
|
cause,
|
|
request,
|
|
timeout,
|
|
timeoutMs: timeoutMs.value,
|
|
signal: options?.signal,
|
|
session,
|
|
});
|
|
// Admission can fail before selecting an existing session for execution.
|
|
root ??= this.#retained?.root;
|
|
session ??= this.#retained?.session;
|
|
this.#retained = undefined;
|
|
this.#pendingCleanup = {
|
|
operation: request.operation,
|
|
session,
|
|
root,
|
|
fileHandles,
|
|
get previousError() {
|
|
return failure();
|
|
},
|
|
operationCause: cause,
|
|
reportedErrors: [],
|
|
};
|
|
const retired = await this.#retire();
|
|
return retired.ok ? err(failure()) : retired;
|
|
}
|
|
}
|
|
}
|