Files
reverse-engineering-anything/src/ghidra/GhidraClientWire.ts
T
morluto 9d940c2f28 fix(ghidra): preserve lifecycle ownership and failure provenance
Refs #4502, #4506, #4508, #4518, #4522, #4526.

Retain snapshot, endpoint, launch rollback and compiler cleanup owners. Join request finalization and cleanup cancellation. Preserve bounded compiler output and original byte counts through capture and Ghidra failures. Normalize execFile cancellation once at its producer.

Validation includes real macOS Ghidra TCP MCP success workflows, controlled Node compiler/process fault fixtures, focused regressions and static checks. Native Swift compiler faults, Windows execution and stdio transport were not verified. Broader startup primary classification in #4517 and native adapter propagation remain unfinished.
2026-10-17 00:44:02 +08:00

109 lines
3.4 KiB
TypeScript

import type { Socket } from "node:net";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, type Result } from "../domain/result.js";
import type { Logger } from "pino";
import type { PendingOperations } from "../process/PendingOperations.js";
import type { GhidraRequestOptions } from "./GhidraClientTypes.js";
import type {
GhidraSessionError,
GhidraSessionFailureFactory,
} from "./GhidraSessionError.js";
/** Dependencies needed to write one authenticated line and await its reply. */
export interface GhidraWireOptions {
readonly getSocket: () => Socket | undefined;
readonly getToken: () => string | undefined;
readonly nextId: () => number;
readonly pending: PendingOperations<
number,
Result<JsonValue, GhidraSessionError>
>;
readonly logger: Logger;
readonly failure: GhidraSessionFailureFactory;
}
/** Owns the socket write and pending-reply lifecycle for one bridge session. */
export class GhidraWire {
readonly #options: GhidraWireOptions;
constructor(options: GhidraWireOptions) {
this.#options = options;
}
/** Write one authenticated request and return its correlated reply. */
async request(
method: string,
params: JsonValue,
options: GhidraRequestOptions,
): Promise<Result<JsonValue, GhidraSessionError>> {
const socket = this.#options.getSocket();
const token = this.#options.getToken();
if (socket === undefined || socket.destroyed || token === undefined)
return err(
this.#options.failure("process", "Ghidra bridge is unavailable"),
);
if (options.signal?.aborted === true)
return err(this.#cancelled(options.signal));
const id = this.#options.nextId();
if (options.timeoutMs !== undefined && options.timeoutMs <= 0)
return err(
this.#options.failure(
"timeout",
"Ghidra request deadline elapsed",
undefined,
{ timeoutMs: options.timeoutMs },
),
);
const timeoutMs = options.timeoutMs;
const response = this.#options.pending.wait(id, {
...(timeoutMs === undefined
? {}
: {
timeoutMs,
timeoutValue: () =>
err(
this.#options.failure(
"timeout",
"Ghidra bridge request timed out",
undefined,
{ timeoutMs },
),
),
}),
...(options.signal === undefined ? {} : { signal: options.signal }),
cancelledValue: () => err(this.#cancelled(options.signal)),
});
socket.write(
`${JSON.stringify({ id, token, method, params })}\n`,
(cause) => {
if (cause !== undefined && cause !== null)
this.#options.pending.settle(
id,
err(
this.#options.failure(
"process",
"Ghidra bridge write failed",
cause,
),
),
);
},
);
const result = await response;
this.#options.logger[result.ok ? "debug" : "warn"](
{ method, status: result.ok ? "ok" : "error" },
"Ghidra bridge request completed",
);
return result;
}
#cancelled(signal: AbortSignal | undefined): GhidraSessionError {
return this.#options.failure(
"cancelled",
"Ghidra request was cancelled",
signal?.reason,
signal === undefined ? {} : { cancellation: { reason: signal.reason } },
);
}
}