fix(ports): prevent duplicate macOS listener probes during status checks (#115813)

* fix(ports): batch macOS lsof listener inspection to one spawn per cycle

(cherry picked from commit edd01c72ae)

* fix(ports): keep single-port lsof checks targeted

(cherry picked from commit 18ece86a57)

* test(daemon): mock batched port usage in coverage

(cherry picked from commit 917ef0d416)

* test(daemon): model batched listener status

* refactor(ports): isolate lsof listener parsing

---------

Co-authored-by: 狼哥 <hanwanlonga@gmail.com>
This commit is contained in:
Vincent Koc
2026-07-29 21:33:55 +08:00
committed by GitHub
parent 859fd0b11e
commit 7d74e45f7f
7 changed files with 414 additions and 62 deletions
+55 -18
View File
@@ -55,6 +55,22 @@ const inspectPortUsage = vi.fn<(port: number) => Promise<PortUsageTestSummary>>(
hints: [],
}),
);
const inspectPortUsages = vi.fn<
(ports: readonly number[]) => Promise<Map<number, PortUsageTestSummary>>
>(
async (ports) =>
new Map(
ports.map((port) => [
port,
{
port,
status: "free",
listeners: [],
hints: [],
},
]),
),
);
const inspectPortConnections = vi.fn<(port: number) => Promise<PortConnections>>(
async (port: number) => ({
port,
@@ -232,6 +248,7 @@ vi.mock("../../gateway/probe-auth.js", async (importOriginal) => {
vi.mock("../../infra/ports.js", () => ({
inspectPortConnections: (port: number) => inspectPortConnections(port),
inspectPortUsage: (port: number) => inspectPortUsage(port),
inspectPortUsages: (ports: readonly number[]) => inspectPortUsages(ports),
formatPortDiagnostics: () => [],
}));
@@ -314,6 +331,20 @@ describe("gatherDaemonStatus", () => {
listeners: [],
hints: [],
}));
inspectPortUsages.mockReset();
inspectPortUsages.mockImplementation(async (ports: readonly number[]) => {
return new Map(
ports.map((port) => [
port,
{
port,
status: "free" as const,
listeners: [],
hints: [],
},
]),
);
});
inspectPortConnections.mockClear();
inspectWindowsGatewayFirewall.mockClear();
inspectWindowsGatewayFirewall.mockResolvedValue({
@@ -382,6 +413,17 @@ describe("gatherDaemonStatus", () => {
expect(inspectWindowsGatewayFirewall).not.toHaveBeenCalled();
});
it("batches daemon and CLI port status inspection when ports differ", async () => {
await gatherDaemonStatus({
rpc: {},
probe: true,
deep: false,
});
expect(inspectPortUsages).toHaveBeenCalledWith([19001, 18789]);
expect(inspectPortUsage).not.toHaveBeenCalled();
});
it("reports the heap limit from the installed Gateway service", async () => {
serviceReadCommand.mockResolvedValueOnce({
programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],
@@ -1149,12 +1191,19 @@ describe("gatherDaemonStatus", () => {
});
it("includes the last gateway error when the service is listening but the RPC probe fails", async () => {
inspectPortUsage.mockResolvedValueOnce({
port: 19001,
status: "busy",
listeners: [{ pid: 8000, ppid: 1, commandLine: "openclaw gateway" }],
hints: [],
});
inspectPortUsages.mockResolvedValueOnce(
new Map([
[
19001,
{
port: 19001,
status: "busy",
listeners: [{ pid: 8000, ppid: 1, commandLine: "openclaw gateway" }],
hints: [],
},
],
]),
);
callGatewayStatusProbe.mockResolvedValueOnce({
ok: false,
url: "wss://127.0.0.1:19001",
@@ -1185,12 +1234,6 @@ describe("gatherDaemonStatus", () => {
});
it("does not read local gateway errors for an explicit probe URL", async () => {
inspectPortUsage.mockResolvedValueOnce({
port: 19001,
status: "busy",
listeners: [{ pid: 8000, ppid: 1, commandLine: "openclaw gateway" }],
hints: [],
});
callGatewayStatusProbe.mockResolvedValueOnce({
ok: false,
url: "wss://remote.example:18790",
@@ -1215,12 +1258,6 @@ describe("gatherDaemonStatus", () => {
auth: { token: "daemon-token" },
},
};
inspectPortUsage.mockResolvedValueOnce({
port: 19001,
status: "busy",
listeners: [{ pid: 8000, ppid: 1, commandLine: "openclaw gateway" }],
hints: [],
});
callGatewayStatusProbe.mockResolvedValueOnce({
ok: false,
url: "wss://remote.example:18790",
+13 -8
View File
@@ -39,6 +39,7 @@ import {
formatPortDiagnostics,
inspectPortConnections,
inspectPortUsage,
inspectPortUsages,
type PortConnection,
type PortListener,
type PortUsageStatus,
@@ -495,15 +496,19 @@ async function inspectDaemonPortStatuses(params: {
daemonPort: number;
cliPort: number;
}): Promise<{ portStatus?: PortStatusSummary; portCliStatus?: PortStatusSummary }> {
const [portDiagnostics, portCliDiagnostics] = await Promise.all([
inspectPortUsage(params.daemonPort).catch(() => null),
params.cliPort !== params.daemonPort
? inspectPortUsage(params.cliPort).catch(() => null)
: null,
]);
if (params.cliPort === params.daemonPort) {
const portDiagnostics = await inspectPortUsage(params.daemonPort).catch(() => null);
return {
portStatus: toPortStatusSummary(portDiagnostics),
portCliStatus: undefined,
};
}
const portDiagnosticsByPort = await inspectPortUsages([params.daemonPort, params.cliPort]).catch(
() => new Map(),
);
return {
portStatus: toPortStatusSummary(portDiagnostics),
portCliStatus: toPortStatusSummary(portCliDiagnostics),
portStatus: toPortStatusSummary(portDiagnosticsByPort.get(params.daemonPort) ?? null),
portCliStatus: toPortStatusSummary(portDiagnosticsByPort.get(params.cliPort) ?? null),
};
}