Files
reverse-engineering-anything/src/artifacts/AnalysisCleanupLease.ts
T
morluto 964decd989 fix(providers): preserve live cleanup ownership through retries
Share exact command/helper cleanup adoption across Java probes. Preserve complete live resource labels and explicit unknowns through inspection leases, and retain unresolved ownership when a cleanup capability is unavailable.

Validation: focused cleanup, provider/doctor and Kuna consumer checks; shared Ghidra lifecycle and public doctor regressions. Native Windows/Linux provider behavior remains unverified.
2026-10-18 01:59:10 +08:00

133 lines
3.9 KiB
TypeScript

import type {
AnalysisError,
AnalysisCleanupResourceOwner,
} from "../domain/analysisErrorBase.js";
/** Link one resource's confirmed release to its originating analysis error. */
export class AnalysisCleanupLease {
#released = false;
#errors: WeakRef<AnalysisError>[] = [];
readonly #groups = new Set<AnalysisCleanupLeaseGroup>();
/** Whether this exact owner has confirmed release. */
get released(): boolean {
return this.#released;
}
readonly #resourceOwner: AnalysisCleanupResourceOwner | undefined;
readonly #resources: readonly string[] | undefined;
get cleanupResources(): readonly string[] | undefined {
return this.#resourceOwner === undefined
? this.#resources
: this.#resourceOwner.cleanupResources;
}
/** Adopt exact close authority while retaining its live labels across retries. */
static forCloseOwner(
owner: {
close(): Promise<void>;
readonly cleanupResources?: readonly string[] | undefined;
},
resource: string,
): AnalysisCleanupLease {
return new AnalysisCleanupLease(
() => owner.close(),
!("cleanupResources" in owner)
? [resource]
: {
get cleanupResources() {
return owner.cleanupResources;
},
},
);
}
constructor(
private readonly cleanup: () => Promise<void>,
resources?: readonly string[] | AnalysisCleanupResourceOwner,
) {
if (resources !== undefined && "cleanupResources" in resources) {
this.#resourceOwner = resources;
this.#resources = undefined;
} else {
this.#resourceOwner = undefined;
this.#resources = resources === undefined ? undefined : [...resources];
}
}
async release(): Promise<void> {
if (this.#released) return;
await this.cleanup();
this.#released = true;
this.#resolveBoundErrors();
for (const group of this.#groups) group.ownerReleased(this);
this.#groups.clear();
}
/** Binding can follow a concurrent retry that already confirmed release. */
bindCleanupError(error: AnalysisError): void {
if (this.#released) {
error.resolveCleanup();
return;
}
this.#errors = [
...this.#errors.filter((reference) => reference.deref() !== undefined),
new WeakRef(error),
];
}
/** Bind one aggregate error to every exact lease in a failed retry set. */
bindCleanupGroup(group: AnalysisCleanupLeaseGroup): void {
if (this.#released) {
group.ownerReleased(this);
return;
}
for (const retained of this.#groups)
if (!retained.hasLiveError) this.#groups.delete(retained);
this.#groups.add(group);
}
#resolveBoundErrors(): void {
for (const reference of this.#errors) reference.deref()?.resolveCleanup();
this.#errors = [];
}
}
/** Conjunctive cleanup binding for an error covering multiple exact owners. */
export class AnalysisCleanupLeaseGroup {
readonly #error: WeakRef<AnalysisError>;
readonly #pending: Set<AnalysisCleanupLease>;
constructor(error: AnalysisError, owners: readonly AnalysisCleanupLease[]) {
this.#error = new WeakRef(error);
this.#pending = new Set(owners);
if (this.#pending.size === 0) return;
error.bindCleanupResourceOwner(this);
for (const owner of this.#pending) owner.bindCleanupGroup(this);
}
/** Unlabelled owners keep attribution unknown; never match resources by text. */
get cleanupResources(): readonly string[] | undefined {
if (
[...this.#pending].some((owner) => owner.cleanupResources === undefined)
)
return undefined;
return [
...new Set(
[...this.#pending].flatMap((owner) => owner.cleanupResources ?? []),
),
];
}
get hasLiveError(): boolean {
return this.#error.deref() !== undefined;
}
ownerReleased(owner: AnalysisCleanupLease): void {
if (!this.#pending.delete(owner) || this.#pending.size > 0) return;
this.#error.deref()?.resolveCleanup();
this.#pending.clear();
}
}