fix(ghidra): recover stale clients on same-target reopen (#2014)

* fix(ghidra): recover stale clients on same-target reopen

* test(ghidra): allow supervised shutdown in health regression

* fix(ghidra): read process exit without diagnostic snapshots

Health and startup exit checks called the supervisor snapshot, which
concatenates retained launcher output only to read the exit code.

* test(adb): align fixtures with literal output and directory commands

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
lyly
2026-10-12 14:06:52 +08:00
committed by GitHub
parent 69e097a8ca
commit 4573e8cf35
9 changed files with 498 additions and 24 deletions
+12 -9
View File
@@ -495,7 +495,7 @@ it("reads a bounded logcat dump with the caller count and buffer", async () => {
const { binary } = await writeAdbStub(
shellStub({
logcat:
"08-10 13:00:00.000 I/Tag( 123): first\\n08-10 13:00:01.000 I/Tag( 123): second\\n",
"08-10 13:00:00.000 I/Tag( 123): first\n08-10 13:00:01.000 I/Tag( 123): second\n",
}),
);
const result = await evidenceResult(providerOver(binary), {
@@ -903,7 +903,7 @@ it("resolves packages by case-insensitive substring", async () => {
const { binary } = await writeAdbStub(
shellStub({
"pm:list":
"package:/data/app/x/base.apk=com.whatsapp\\npackage:/data/app/y/base.apk=com.whatsapp.w4b\\npackage:/data/app/z/base.apk=com.example\\n",
"package:/data/app/x/base.apk=com.whatsapp\npackage:/data/app/y/base.apk=com.whatsapp.w4b\npackage:/data/app/z/base.apk=com.example\n",
}),
);
const result = await evidenceResult(providerOver(binary), {
@@ -1109,20 +1109,23 @@ exit 1`,
it("quotes caller text before adb joins the remote shell command", async () => {
const { binary, root } = await writeAdbStub(
(root) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
shift 3
# Model adb's remote command joining, then execute the resulting shell string.
printf '#!/bin/sh\nprintf "%%s" "$@" > "${join(root, "observed")}"\n' > "${join(root, "ls")}"
chmod +x "${join(root, "ls")}"
PATH="${root}:$PATH" sh -c "$*"
sh -c "$*"
`,
);
const value = `/sdcard/a 'quoted'; touch ${join(root, "injected")}`;
await providerOver(binary).execute({
const value = join(root, `device 'quoted'; touch ${join(root, "injected")}`);
await mkdir(value, { recursive: true });
const result = await evidenceResult(providerOver(binary), {
operation: "list_adb_directory",
input: { serial: "device", device_path: value },
});
expect(await readFile(join(root, "observed"), "utf8")).toContain(value);
expect(result).toMatchObject({
device_path: value,
entries: [],
coverage: "complete",
});
await expect(readFile(join(root, "injected"))).rejects.toThrow();
});
+4 -4
View File
@@ -32,13 +32,13 @@ export const registerWebModuleCommands = (
options: z.object({
importerUrl: webModuleTraceInputSchema.shape.importer_url,
importMapPath: importMapInputSchema.shape.path
.optional()
.describe(
"Absolute local import-map JSON path; requires --import-map-base-url",
)
.optional(),
),
importMapBaseUrl: importMapInputSchema.shape.base_url
.describe("HTTP(S) import-map base URL; requires --import-map-path")
.optional(),
.optional()
.describe("HTTP(S) import-map base URL; requires --import-map-path"),
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
+31 -1
View File
@@ -7,6 +7,10 @@ import type { ProgressReporter } from "../application/ProgressReporter.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import {
providerRetryAction,
type ProviderOperationHealth,
} from "../domain/providerOperationHealth.js";
import { err, ok, type Result } from "../domain/result.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
@@ -477,6 +481,32 @@ export class GhidraClient {
});
}
/** Observed failures that prevent a retained session from being reused. */
operationHealthSnapshot(): ProviderOperationHealth | undefined {
const exit = this.#process?.observedExit();
const state =
exit !== undefined
? "exited"
: this.#socket?.destroyed === true
? "unreachable"
: this.#startPromise === undefined &&
(this.#process !== undefined ||
this.#socket !== undefined ||
this.#runtimeRoot !== undefined ||
this.#socketRoot !== undefined ||
this.#snapshotOwners.size > 0)
? "unknown"
: undefined;
if (state === undefined) return undefined;
return {
state,
stage: null,
retryAction: providerRetryAction(state),
exitCode: exit?.code ?? null,
requests: [],
};
}
/** Latest bounded local diagnostics, with bearer material redacted. */
diagnostics(): Readonly<Record<string, JsonValue>> {
return structuredClone(this.#diagnostics());
@@ -633,7 +663,7 @@ export class GhidraClient {
deadline,
failure: this.#failure,
isClosed: () => this.#closing,
processExited: () => this.#process?.snapshot().exitCode !== undefined,
processExited: () => this.#process?.observedExit() !== undefined,
startupTimeoutMs: this.#options.startupTimeoutMs,
handlers: {
data: this.#onSocketData,
+5 -1
View File
@@ -108,7 +108,10 @@ export type GhidraProviderClientFactory = (
Partial<
Pick<
GhidraClient,
"runtimeLineage" | "readTargetSnapshot" | "hashTargetSnapshot"
| "operationHealthSnapshot"
| "runtimeLineage"
| "readTargetSnapshot"
| "hashTargetSnapshot"
>
>;
@@ -840,6 +843,7 @@ export const createGhidraProviderClient = (input: {
}),
);
},
operationHealthSnapshot: () => client.operationHealthSnapshot?.(),
runtimeLineageSnapshots: () => {
const observation = client.runtimeLineage?.() ?? null;
return observation === null
+21 -7
View File
@@ -78,6 +78,15 @@ export type ProviderProcessLaunch =
readonly cleanup?: () => Promise<ProcessCleanupResult>;
};
/**
* Process exit recorded by the supervisor.
* Independent of retained stream diagnostics.
*/
export interface ProviderProcessExitObservation {
readonly code: number | null;
readonly signal: NodeJS.Signals | null;
}
/** Detached diagnostics captured for one supervised provider process. */
export interface ProviderProcessSnapshot {
/** True only when a configured diagnostic budget excluded output bytes. */
@@ -233,10 +242,7 @@ export class ProviderProcessSupervisor {
readonly #options: ProviderProcessSupervisorOptions;
readonly #stdout = new ProcessOutputCapture();
readonly #stderr = new ProcessOutputCapture();
readonly #exit = deferred<{
readonly code: number | null;
readonly signal: NodeJS.Signals | null;
}>();
readonly #exit = deferred<ProviderProcessExitObservation>();
readonly #outputClose = deferred<void>();
#outputClosed = false;
#stdoutObservedBytes = 0;
@@ -271,9 +277,7 @@ export class ProviderProcessSupervisor {
readonly #onError = (cause: Error): void => {
this.#options.onDiagnostic?.({ type: "error", message: cause.message });
};
#exitObservation:
| { readonly code: number | null; readonly signal: NodeJS.Signals | null }
| undefined;
#exitObservation: ProviderProcessExitObservation | undefined;
#stopPromise: Promise<ProviderProcessStopResult> | undefined;
#disposed = false;
@@ -292,6 +296,16 @@ export class ProviderProcessSupervisor {
this.#recordExit(launch.process.exitCode, launch.process.signalCode);
}
/**
* Recorded process exit without concatenating retained stream diagnostics.
* Undefined until exit or close is observed.
*/
observedExit(): ProviderProcessExitObservation | undefined {
const exit = this.#exitObservation;
if (exit === undefined) return undefined;
return { code: exit.code, signal: exit.signal };
}
/** Latest retained output and exit observation, with explicit configured truncation. */
snapshot(): ProviderProcessSnapshot {
return {
+52 -1
View File
@@ -1,6 +1,6 @@
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import { expect, it } from "vitest";
import { expect, it, vi } from "vitest";
import { ProviderProcessSupervisor } from "./ProviderProcess.js";
it("exit does not establish complete output when a producer closes its streams later", async () => {
@@ -34,6 +34,57 @@ it("exit does not establish complete output when a producer closes its streams l
}
});
it("reads an observed exit without materializing stream diagnostics", () => {
const producer = Object.assign(new EventEmitter(), {
stdout: new PassThrough(),
stderr: new PassThrough(),
exitCode: null,
signalCode: null,
kill: () => false,
});
const supervisor = new ProviderProcessSupervisor({
process: producer,
ownsProcessLifetime: false,
});
try {
producer.stdout.write("retained-diagnostic");
const snapshot = vi.spyOn(supervisor, "snapshot");
expect(supervisor.observedExit()).toBeUndefined();
producer.emit("exit", null, "SIGKILL");
expect(supervisor.observedExit()).toEqual({
code: null,
signal: "SIGKILL",
});
expect(snapshot).not.toHaveBeenCalled();
expect(supervisor.snapshot()).toMatchObject({
stdout: { text: "retained-diagnostic" },
exitCode: null,
signal: "SIGKILL",
});
} finally {
supervisor.dispose();
}
});
it("observes an exit that completed before supervision", () => {
const producer = Object.assign(new EventEmitter(), {
stdout: new PassThrough(),
stderr: new PassThrough(),
exitCode: 2,
signalCode: null,
kill: () => false,
});
const supervisor = new ProviderProcessSupervisor({
process: producer,
ownsProcessLifetime: false,
});
try {
expect(supervisor.observedExit()).toEqual({ code: 2, signal: null });
} finally {
supervisor.dispose();
}
});
it("reports a reached process memory limit only when the handle observed one", () => {
let reached = false;
const producer = Object.assign(new EventEmitter(), {
+346
View File
@@ -0,0 +1,346 @@
import { spawn, type ChildProcess } from "node:child_process";
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
import { LazyAnalysisProvider } from "../../../src/application/binary/LazyAnalysisProvider.js";
import { parseConfig } from "../../../src/config/parseConfig.js";
import { elf } from "../../../src/domain/binaryTarget.fixture.js";
import { ok } from "../../../src/domain/result.js";
import { GhidraClient } from "../../../src/ghidra/GhidraClient.js";
import type { GhidraInstallationHost } from "../../../src/ghidra/GhidraInstallation.js";
import type {
GhidraLaunchSession,
GhidraLauncher,
} from "../../../src/ghidra/GhidraLauncher.js";
import { GhidraProvider } from "../../../src/ghidra/GhidraProvider.js";
import { silentLogger } from "../../../src/logger.js";
import { createServer } from "../../../src/server/createServer.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { waitForExit } from "../../support/process/processFixture.js";
const fixturePath = fileURLToPath(
new URL("../../fixtures/fakeGhidra.mjs", import.meta.url),
);
type FixtureLaunch = {
child: ChildProcess;
runtimeRoot: string;
endpointPath: string;
};
class FixtureLauncher implements GhidraLauncher {
readonly launches: FixtureLaunch[] = [];
readonly requests: string[] = [];
cleanupAllowed = true;
cleanupAttempts = 0;
async launch(session: GhidraLaunchSession) {
const projectRoot = join(session.runtimeRoot, "project");
await mkdir(projectRoot, { recursive: true });
const child = spawn(process.execPath, [fixturePath], {
stdio: ["ignore", "pipe", "pipe", "ipc"],
env: {
...process.env,
REA_GHIDRA_FIXTURE_CONFIG: JSON.stringify({
endpointPath: session.endpointPath,
token: session.token,
runId: session.runId,
providerVersion: session.providerVersion,
profileDigest: session.profileDigest,
targetSha256: session.targetSha256,
transport: session.transport,
}),
},
});
this.launches.push({
child,
runtimeRoot: session.runtimeRoot,
endpointPath: session.endpointPath,
});
child.on("message", (value: unknown) => {
if (
typeof value === "object" &&
value !== null &&
"method" in value &&
typeof value.method === "string"
)
this.requests.push(value.method);
});
return ok({
process: child,
ownsProcessLifetime: true as const,
projectRoot,
ghidraLogPath: join(session.runtimeRoot, "ghidra.log"),
scriptLogPath: join(session.runtimeRoot, "script.log"),
cleanup: async () => {
this.cleanupAttempts += 1;
if (!this.cleanupAllowed)
return {
cleaned: false as const,
reason: "fixture cleanup is not yet confirmed",
};
const signaled = child.exitCode === null && child.signalCode === null;
if (signaled) child.kill("SIGTERM");
return (await waitForExit(child, 2_000))
? { cleaned: true as const, signaled }
: { cleaned: false as const, reason: "fixture child did not exit" };
},
});
}
}
const installationHost: GhidraInstallationHost = {
platform: "linux",
architecture: "x64",
readText: () => "application.version=12.1.4\n",
executable: () => true,
probeJava: () => ({
version: "21.0.11",
major: 21,
home: "/fixture/jdk-21",
bits: 64,
runtime: "jdk",
}),
};
// Only installation discovery and the Java bridge are simulated. Requests use
// the production client, real child/socket and public MCP session routing.
const connectGhidra = async () => {
const directory = await createTestTempDirectory("rea-ghidra-reopen-");
const target = join(directory, "target.elf");
const source = elf(2, 1, 62);
await writeFile(target, source);
const config = parseConfig({
GHIDRA_INSTALL_DIR: "/fixture/ghidra_12.1.4_PUBLIC",
});
if (!config.ok) throw config.error;
const launcher = new FixtureLauncher();
const clients: GhidraClient[] = [];
const provider = new GhidraProvider(
config.value,
silentLogger,
{},
installationHost,
(options) => {
const client = new GhidraClient({
...options,
launcher,
platform: process.platform,
startupTimeoutMs: 5_000,
});
clients.push(client);
return client;
},
);
const lazy = new LazyAnalysisProvider({
identity: provider.identity(),
capabilities: provider.capabilities(),
load: () => Promise.resolve(provider),
});
const session = composeBinarySession(
new AnalysisProviderRegistry([
{
identity: () => provider.identity(),
capabilities: () => provider.capabilities(),
inspectAvailability: () => provider.inspectAvailability(),
inspectTargetSupport: (value) => provider.inspectTargetSupport(value),
resolveAnalysisProfile: (value, options) =>
provider.resolveAnalysisProfile(value, options),
createClient: (value, profile, context) =>
lazy.createClient(value, profile, context),
},
]),
);
const server = createServer(
{ kind: "session", session },
{ logger: silentLogger },
);
const mcp = new Client({ name: "ghidra-reopen", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const close = async () => {
launcher.cleanupAllowed = true;
await Promise.allSettled([mcp.close(), server.close()]);
const closed = await session.close();
try {
for (const client of clients)
expect((await client.close()).ok).toBe(true);
expect(closed.ok).toBe(true);
} finally {
for (const { child } of launcher.launches) {
if (child.exitCode === null && child.signalCode === null)
child.kill("SIGTERM");
expect(await waitForExit(child, 2_000)).toBe(true);
}
}
for (const launched of launcher.launches) await expectReleased(launched);
expect(await readFile(target)).toEqual(source);
};
try {
await server.connect(serverTransport);
await mcp.connect(clientTransport);
} catch (cause) {
await close();
throw cause;
}
return {
clients,
launcher,
close,
open: () =>
mcp.callTool({ name: "open_binary", arguments: { path: target } }),
query: (addressSuffix: number) =>
mcp.callTool({
name: "list_strings",
arguments: { address: `0x200${addressSuffix}` },
}),
};
};
const firstLaunch = (launcher: FixtureLauncher): FixtureLaunch => {
const first = launcher.launches[0];
if (first === undefined) throw new Error("Expected a running Ghidra fixture");
return first;
};
const expectReleased = async ({
child,
runtimeRoot,
endpointPath,
}: FixtureLaunch) => {
expect(await waitForExit(child, 2_000)).toBe(true);
await expect(access(runtimeRoot)).rejects.toMatchObject({ code: "ENOENT" });
await expect(access(endpointPath)).rejects.toMatchObject({ code: "ENOENT" });
};
const disconnect = async (
child: ChildProcess,
client: GhidraClient | undefined,
) => {
if (client === undefined)
throw new Error("Expected a connected Ghidra client");
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
child.off("message", received);
reject(new Error("Fixture did not acknowledge socket disconnection"));
}, 2_000);
const received = (message: unknown) => {
if (
typeof message !== "object" ||
message === null ||
!("event" in message) ||
message.event !== "disconnected"
)
return;
clearTimeout(timer);
child.off("message", received);
resolve();
};
child.on("message", received);
child.send("disconnect");
});
await expect
.poll(async () => (await client.ping({ timeoutMs: 200 })).ok)
.toBe(false);
expect(child.exitCode).toBeNull();
expect(child.signalCode).toBeNull();
};
describe.skipIf(process.platform === "win32")(
"Ghidra MCP session recovery over Unix sockets",
() => {
it("keeps open lazy, reuses a live child, and replaces an exited child on reopen", async () => {
const harness = await connectGhidra();
try {
expect((await harness.open()).isError).not.toBe(true);
expect((await harness.open()).isError).not.toBe(true);
expect(harness.clients).toHaveLength(0);
expect(harness.launcher.launches).toHaveLength(0);
expect((await harness.query(1)).isError).not.toBe(true);
const old = firstLaunch(harness.launcher);
expect((await harness.open()).isError).not.toBe(true);
expect((await harness.query(2)).isError).not.toBe(true);
expect(harness.launcher.launches).toHaveLength(1);
await expect
.poll(
() =>
harness.launcher.requests.filter(
(method) => method === "list_strings",
).length,
)
.toBe(2);
expect(old.child.kill("SIGTERM")).toBe(true);
expect(await waitForExit(old.child, 2_000)).toBe(true);
expect(old.child.signalCode).toBe("SIGTERM");
expect((await harness.open()).isError).not.toBe(true);
await expectReleased(old);
expect(harness.clients).toHaveLength(1);
expect(harness.launcher.launches).toHaveLength(1);
expect((await harness.query(3)).isError).not.toBe(true);
expect(harness.clients).toHaveLength(2);
expect(harness.launcher.launches).toHaveLength(2);
} finally {
await harness.close();
}
});
it("retires a disconnected live child before lazily starting a replacement", async () => {
const harness = await connectGhidra();
try {
expect((await harness.open()).isError).not.toBe(true);
expect((await harness.query(1)).isError).not.toBe(true);
const old = firstLaunch(harness.launcher);
await disconnect(old.child, harness.clients[0]);
expect((await harness.open()).isError).not.toBe(true);
await expectReleased(old);
expect(harness.clients).toHaveLength(1);
expect(harness.launcher.launches).toHaveLength(1);
expect((await harness.query(2)).isError).not.toBe(true);
expect(harness.clients).toHaveLength(2);
expect(harness.launcher.launches).toHaveLength(2);
} finally {
await harness.close();
}
});
it("retains failed cleanup and refuses replacement until cleanup can be retried", async () => {
const harness = await connectGhidra();
try {
expect((await harness.open()).isError).not.toBe(true);
expect((await harness.query(1)).isError).not.toBe(true);
const old = firstLaunch(harness.launcher);
await disconnect(old.child, harness.clients[0]);
harness.launcher.cleanupAllowed = false;
for (let attempt = 1; attempt <= 2; attempt += 1) {
expect(parseMcpToolError(await harness.open())).toMatchObject({
error: { code: "cleanup_incomplete" },
});
expect(harness.launcher.cleanupAttempts).toBe(attempt);
expect(harness.clients).toHaveLength(1);
expect(harness.launcher.launches).toHaveLength(1);
expect(old.child.exitCode).toBeNull();
expect(old.child.signalCode).toBeNull();
await access(old.runtimeRoot);
}
harness.launcher.cleanupAllowed = true;
expect((await harness.open()).isError).not.toBe(true);
await expectReleased(old);
expect(harness.launcher.cleanupAttempts).toBe(3);
expect(harness.clients).toHaveLength(1);
expect(harness.launcher.launches).toHaveLength(1);
expect((await harness.query(2)).isError).not.toBe(true);
expect(harness.clients).toHaveLength(2);
expect(harness.launcher.launches).toHaveLength(2);
} finally {
await harness.close();
}
});
},
);
@@ -232,6 +232,7 @@ describe("GhidraClient captured source snapshot", () => {
if (!started.ok) throw started.error;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockRejectedValueOnce(new Error("snapshot descriptor close failed"))
.mockRejectedValueOnce(new Error("snapshot descriptor close failed"));
try {
await expect(client.hashTargetSnapshot()).resolves.toMatchObject({
@@ -240,11 +241,19 @@ describe("GhidraClient captured source snapshot", () => {
});
const owner = close.mock.contexts[0];
expect(owner).toBeInstanceOf(OwnedFileHandle);
await expect(client.close()).resolves.toMatchObject({
ok: false,
error: { cleanupIncomplete: true },
});
expect(client.operationHealthSnapshot()).toMatchObject({
state: "unknown",
});
const closed = await client.close();
if (!closed.ok) throw closed.error;
expect(client.operationHealthSnapshot()).toBeUndefined();
expect(
close.mock.contexts.filter((context) => context === owner),
).toHaveLength(2);
).toHaveLength(3);
} finally {
close.mockRestore();
}
@@ -591,11 +600,15 @@ describe("GhidraClient runtime removal failures", () => {
},
});
await expect(access(runtimeRoot)).resolves.toBeUndefined();
expect(client.operationHealthSnapshot()).toMatchObject({
state: "unknown",
});
} finally {
await chmod(runtimeRoot, 0o700);
}
await expect(client.close()).resolves.toEqual({ ok: true, value: null });
expect(client.operationHealthSnapshot()).toBeUndefined();
await expect(access(runtimeRoot)).rejects.toMatchObject({
code: "ENOENT",
});
@@ -626,16 +639,21 @@ describe("GhidraClient cleanup and diagnostics", () => {
const client = clientFor(launcher, {
runId: "11111111-1111-4111-8111-111111111111",
});
expect(client.operationHealthSnapshot()).toBeUndefined();
await expect(client.start()).resolves.toMatchObject({ ok: true });
expect(client.operationHealthSnapshot()).toBeUndefined();
const child = await launcher.started;
await client.close();
expect(client.operationHealthSnapshot()).toBeUndefined();
expect(client.diagnostics()).toMatchObject({
runtime_root: launcher.runtimeRoots[0],
transport: HOST_TRANSPORT,
endpoint_path: launcher.endpointPaths[0],
project_root: join(launcher.runtimeRoots[0] ?? "", "project"),
process_id: expect.any(Number),
exit_code: child.exitCode,
run_id: "11111111-1111-4111-8111-111111111111",
});
await expect(access(launcher.runtimeRoots[0] ?? "")).rejects.toMatchObject({
+8
View File
@@ -31,7 +31,10 @@ if (mode === "exit") process.exit(73);
if (mode === "silent") setInterval(() => undefined, 1_000);
else {
await rm(endpointPath, { force: true });
const sockets = new Set();
const server = createServer((socket) => {
sockets.add(socket);
socket.once("close", () => sockets.delete(socket));
socket.setEncoding("utf8");
const state = {
buffer: { value: "" },
@@ -45,6 +48,11 @@ else {
};
socket.on("data", (chunk) => onSocketData(socket, server, chunk, state));
});
process.on("message", (message) => {
if (message !== "disconnect") return;
for (const socket of sockets) socket.destroy();
process.send?.({ event: "disconnected" });
});
if (transport === "unix-socket") server.listen(endpointPath);
else {
server.listen(0, "127.0.0.1", async () => {