mirror of https://github.com/openclaw/openclaw.git
Status: suppress false dual-stack loopback port warning (#53398)
This commit is contained in:
parent
c7330eb716
commit
e816d0968a
|
|
@ -166,6 +166,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Plugins/Matrix: load bundled `@matrix-org/matrix-sdk-crypto-nodejs` through `createRequire(...)` so E2EE media send and receive keep the package-local native binding lookup working in packaged ESM builds. (#54566) thanks @joelnishanth.
|
||||
- Plugins/Matrix: encrypt E2EE image thumbnails with `thumbnail_file` while keeping unencrypted-room previews on `thumbnail_url`, so encrypted Matrix image events keep thumbnail metadata without leaking plaintext previews. (#54711) thanks @frischeDaten.
|
||||
- Telegram/forum topics: keep native `/new` and `/reset` routed to the active topic by preserving the topic target on forum-thread command context. (#35963)
|
||||
- Status/port diagnostics: treat single-process dual-stack loopback gateway listeners as healthy in `openclaw status --all`, suppressing false “port already in use” conflict warnings. (#53398) Thanks @DanWebb1949.
|
||||
|
||||
## 2026.3.24
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ProgressReporter } from "../../cli/progress.js";
|
||||
|
||||
vi.mock("../../daemon/launchd.js", () => ({
|
||||
resolveGatewayLogPaths: () => {
|
||||
throw new Error("skip log tail");
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./gateway.js", () => ({
|
||||
readFileTailLines: vi.fn(async () => []),
|
||||
summarizeLogTail: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
import { appendStatusAllDiagnosis } from "./diagnosis.js";
|
||||
|
||||
type DiagnosisParams = Parameters<typeof appendStatusAllDiagnosis>[0];
|
||||
|
||||
function createProgressReporter(): ProgressReporter {
|
||||
return {
|
||||
setLabel: () => {},
|
||||
setPercent: () => {},
|
||||
tick: () => {},
|
||||
done: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function createBaseParams(
|
||||
listeners: NonNullable<DiagnosisParams["portUsage"]>["listeners"],
|
||||
): DiagnosisParams {
|
||||
return {
|
||||
lines: [] as string[],
|
||||
progress: createProgressReporter(),
|
||||
muted: (text: string) => text,
|
||||
ok: (text: string) => text,
|
||||
warn: (text: string) => text,
|
||||
fail: (text: string) => text,
|
||||
connectionDetailsForReport: "ws://127.0.0.1:18789",
|
||||
snap: null,
|
||||
remoteUrlMissing: false,
|
||||
secretDiagnostics: [],
|
||||
sentinel: null,
|
||||
lastErr: null,
|
||||
port: 18789,
|
||||
portUsage: { port: 18789, status: "busy", listeners, hints: [] },
|
||||
tailscaleMode: "off",
|
||||
tailscale: {
|
||||
backendState: null,
|
||||
dnsName: null,
|
||||
ips: [],
|
||||
error: null,
|
||||
},
|
||||
tailscaleHttpsUrl: null,
|
||||
skillStatus: null,
|
||||
pluginCompatibility: [],
|
||||
channelsStatus: null,
|
||||
channelIssues: [],
|
||||
gatewayReachable: false,
|
||||
health: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("status-all diagnosis port checks", () => {
|
||||
it("treats same-process dual-stack loopback listeners as healthy", async () => {
|
||||
const params = createBaseParams([
|
||||
{ pid: 5001, commandLine: "openclaw-gateway", address: "127.0.0.1:18789" },
|
||||
{ pid: 5001, commandLine: "openclaw-gateway", address: "[::1]:18789" },
|
||||
]);
|
||||
|
||||
await appendStatusAllDiagnosis(params);
|
||||
|
||||
const output = params.lines.join("\n");
|
||||
expect(output).toContain("✓ Port 18789");
|
||||
expect(output).toContain("Detected dual-stack loopback listeners");
|
||||
expect(output).not.toContain("Port 18789 is already in use.");
|
||||
});
|
||||
|
||||
it("keeps warning for multi-process listener conflicts", async () => {
|
||||
const params = createBaseParams([
|
||||
{ pid: 5001, commandLine: "openclaw-gateway", address: "127.0.0.1:18789" },
|
||||
{ pid: 5002, commandLine: "openclaw-gateway", address: "[::1]:18789" },
|
||||
]);
|
||||
|
||||
await appendStatusAllDiagnosis(params);
|
||||
|
||||
const output = params.lines.join("\n");
|
||||
expect(output).toContain("! Port 18789");
|
||||
expect(output).toContain("Port 18789 is already in use.");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import type { ProgressReporter } from "../../cli/progress.js";
|
||||
import { formatConfigIssueLine } from "../../config/issue-format.js";
|
||||
import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
|
||||
import { formatPortDiagnostics } from "../../infra/ports.js";
|
||||
import {
|
||||
formatPortDiagnostics,
|
||||
isDualStackLoopbackGatewayListeners,
|
||||
type PortUsage,
|
||||
} from "../../infra/ports.js";
|
||||
import {
|
||||
type RestartSentinelPayload,
|
||||
summarizeRestartSentinel,
|
||||
|
|
@ -22,7 +26,7 @@ type ConfigSnapshotLike = {
|
|||
issues?: ConfigIssueLike[] | null;
|
||||
};
|
||||
|
||||
type PortUsageLike = { listeners: unknown[] };
|
||||
type PortUsageLike = Pick<PortUsage, "listeners" | "port" | "status" | "hints">;
|
||||
|
||||
type TailscaleStatusLike = {
|
||||
backendState: string | null;
|
||||
|
|
@ -139,12 +143,20 @@ export async function appendStatusAllDiagnosis(params: {
|
|||
}
|
||||
|
||||
if (params.portUsage) {
|
||||
const portOk = params.portUsage.listeners.length === 0;
|
||||
const benignDualStackLoopback = isDualStackLoopbackGatewayListeners(
|
||||
params.portUsage.listeners,
|
||||
params.port,
|
||||
);
|
||||
const portOk = params.portUsage.listeners.length === 0 || benignDualStackLoopback;
|
||||
emitCheck(`Port ${params.port}`, portOk ? "ok" : "warn");
|
||||
if (!portOk) {
|
||||
for (const line of formatPortDiagnostics(params.portUsage as never)) {
|
||||
for (const line of formatPortDiagnostics(params.portUsage)) {
|
||||
lines.push(` ${muted(line)}`);
|
||||
}
|
||||
} else if (benignDualStackLoopback) {
|
||||
lines.push(
|
||||
` ${muted("Detected dual-stack loopback listeners (127.0.0.1 + ::1) for one gateway process.")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
classifyPortListener,
|
||||
formatPortDiagnostics,
|
||||
formatPortListener,
|
||||
isDualStackLoopbackGatewayListeners,
|
||||
} from "./ports-format.js";
|
||||
|
||||
describe("ports-format", () => {
|
||||
|
|
@ -35,6 +36,17 @@ describe("ports-format", () => {
|
|||
expect(buildPortHints([], 18789)).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats single-process loopback dual-stack gateway listeners as benign", () => {
|
||||
const listeners = [
|
||||
{ pid: 4242, commandLine: "openclaw-gateway", address: "127.0.0.1:18789" },
|
||||
{ pid: 4242, commandLine: "openclaw-gateway", address: "[::1]:18789" },
|
||||
];
|
||||
expect(isDualStackLoopbackGatewayListeners(listeners, 18789)).toBe(true);
|
||||
expect(buildPortHints(listeners, 18789)).toEqual([
|
||||
expect.stringContaining("Gateway already running locally."),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
{ pid: 123, user: "alice", commandLine: "ssh -N", address: "::1" },
|
||||
|
|
|
|||
|
|
@ -19,6 +19,78 @@ export function classifyPortListener(listener: PortListener, port: number): Port
|
|||
return "unknown";
|
||||
}
|
||||
|
||||
function parseListenerAddress(address: string): { host: string; port: number } | null {
|
||||
const trimmed = address.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
const normalized = trimmed.replace(/^tcp6?\s+/i, "").replace(/\s*\(listen\)\s*$/i, "");
|
||||
const bracketMatch = normalized.match(/^\[([^\]]+)\]:(\d+)$/);
|
||||
if (bracketMatch) {
|
||||
const port = Number.parseInt(bracketMatch[2], 10);
|
||||
return Number.isFinite(port) ? { host: bracketMatch[1].toLowerCase(), port } : null;
|
||||
}
|
||||
const lastColon = normalized.lastIndexOf(":");
|
||||
if (lastColon <= 0 || lastColon >= normalized.length - 1) {
|
||||
return null;
|
||||
}
|
||||
const host = normalized.slice(0, lastColon).trim().toLowerCase();
|
||||
const portToken = normalized.slice(lastColon + 1).trim();
|
||||
if (!/^\d+$/.test(portToken)) {
|
||||
return null;
|
||||
}
|
||||
const port = Number.parseInt(portToken, 10);
|
||||
return Number.isFinite(port) ? { host, port } : null;
|
||||
}
|
||||
|
||||
function classifyLoopbackAddressFamily(host: string): "ipv4" | "ipv6" | null {
|
||||
if (host === "127.0.0.1" || host === "localhost") {
|
||||
return "ipv4";
|
||||
}
|
||||
if (host === "::1") {
|
||||
return "ipv6";
|
||||
}
|
||||
if (host.startsWith("::ffff:")) {
|
||||
const mapped = host.slice("::ffff:".length);
|
||||
return mapped === "127.0.0.1" ? "ipv6" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isDualStackLoopbackGatewayListeners(
|
||||
listeners: PortListener[],
|
||||
port: number,
|
||||
): boolean {
|
||||
if (listeners.length < 2) {
|
||||
return false;
|
||||
}
|
||||
const pids = new Set<number>();
|
||||
const families = new Set<"ipv4" | "ipv6">();
|
||||
for (const listener of listeners) {
|
||||
if (classifyPortListener(listener, port) !== "gateway") {
|
||||
return false;
|
||||
}
|
||||
const pid = listener.pid;
|
||||
if (typeof pid !== "number" || !Number.isFinite(pid)) {
|
||||
return false;
|
||||
}
|
||||
pids.add(pid);
|
||||
if (typeof listener.address !== "string") {
|
||||
return false;
|
||||
}
|
||||
const parsedAddress = parseListenerAddress(listener.address);
|
||||
if (!parsedAddress || parsedAddress.port !== port) {
|
||||
return false;
|
||||
}
|
||||
const family = classifyLoopbackAddressFamily(parsedAddress.host);
|
||||
if (!family) {
|
||||
return false;
|
||||
}
|
||||
families.add(family);
|
||||
}
|
||||
return pids.size === 1 && families.has("ipv4") && families.has("ipv6");
|
||||
}
|
||||
|
||||
export function buildPortHints(listeners: PortListener[], port: number): string[] {
|
||||
if (listeners.length === 0) {
|
||||
return [];
|
||||
|
|
@ -38,7 +110,7 @@ export function buildPortHints(listeners: PortListener[], port: number): string[
|
|||
if (kinds.has("unknown")) {
|
||||
hints.push("Another process is listening on this port.");
|
||||
}
|
||||
if (listeners.length > 1) {
|
||||
if (listeners.length > 1 && !isDualStackLoopbackGatewayListeners(listeners, port)) {
|
||||
hints.push(
|
||||
"Multiple listeners detected; ensure only one gateway/tunnel per port unless intentionally running isolated profiles.",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -86,5 +86,10 @@ export async function handlePortError(
|
|||
|
||||
export { PortInUseError };
|
||||
export type { PortListener, PortListenerKind, PortUsage, PortUsageStatus };
|
||||
export { buildPortHints, classifyPortListener, formatPortDiagnostics } from "./ports-format.js";
|
||||
export {
|
||||
buildPortHints,
|
||||
classifyPortListener,
|
||||
formatPortDiagnostics,
|
||||
isDualStackLoopbackGatewayListeners,
|
||||
} from "./ports-format.js";
|
||||
export { inspectPortUsage } from "./ports-inspect.js";
|
||||
|
|
|
|||
Loading…
Reference in New Issue