mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 13:34:49 +00:00
fix: improve gateway protocol mismatch diagnostics (#82908)
* fix: improve gateway protocol mismatch diagnostics * test: cover daemon deep connection diagnostics * fix: normalize mapped loopback gateway clients
This commit is contained in:
committed by
GitHub
parent
926a5a825f
commit
38b3e73622
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/media: accept HTTP(S) URLs in `openclaw infer image describe --file`, fetching remote images through the guarded media path instead of treating URLs as local files. Fixes #82837. (#82854) Thanks @neeravmakwana.
|
||||
- Agents/subagents: keep session-backed parent runs active when the child wait call times out before the child session has actually settled, so late subagent completions are reconciled instead of being lost. Fixes #82787. Thanks @ramitrkar-hash.
|
||||
- Control UI: advertise shared Gateway protocol constants in browser connect frames, fixing protocol mismatch handshakes after the protocol 5 bump. Fixes #82882. Thanks @galiniliev.
|
||||
- Gateway: add rollback protocol-mismatch diagnostics, including client protocol ranges in Gateway logs and deep status/doctor hints for stale client processes. Fixes #82841. (#82908)
|
||||
- Agents/subagents: route group/channel subagent completions through message-tool-only handoffs when required and keep active-requester wake failures from dropping completion delivery. Fixes #82803. Thanks @galiniliev, @yozakura-ava, and @moeedahmed.
|
||||
- Memory-core: scan persisted memory source sessions on startup, comparing on-disk transcripts against the index and marking only missing/newer/resized files dirty for incremental sync. Fixes #82341. (#82341) Thanks @giodl73-repo.
|
||||
- Telegram: keep the top-level default account in the account list when named accounts or bindings are added alongside top-level credentials, preserving default polling while still letting named-only configs resolve to a single account. Fixes #82794. (#82794) Thanks @giodl73-repo.
|
||||
|
||||
@@ -86,6 +86,32 @@ openclaw config get meta.lastTouchedVersion
|
||||
For intentional downgrade or emergency recovery only, set `OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS=1` for the single command. Leave it unset for normal operation.
|
||||
</Warning>
|
||||
|
||||
## Protocol mismatch after rollback
|
||||
|
||||
Use this when logs keep printing `protocol mismatch` after you downgrade or roll back OpenClaw. This means an older Gateway is running, but a newer local client process is still trying to reconnect with a protocol range that the older Gateway cannot speak.
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
which -a openclaw
|
||||
openclaw gateway status --deep
|
||||
openclaw doctor --deep
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- `protocol mismatch ... client=... v<version> min=<n> max=<n> expected=<n>` in Gateway logs.
|
||||
- `Established clients:` in `openclaw gateway status --deep` or `Gateway clients` in `openclaw doctor --deep`. This lists active TCP clients connected to the Gateway port, including PIDs and command lines when the OS allows it.
|
||||
- A client process whose command line points at the newer OpenClaw install or wrapper you rolled back from.
|
||||
|
||||
Fix:
|
||||
|
||||
1. Stop or restart the stale OpenClaw client process shown by `gateway status --deep`.
|
||||
2. Restart apps or wrappers that embed OpenClaw, such as local dashboards, editors, app-server helpers, or long-running `openclaw logs --follow` shells.
|
||||
3. Re-run `openclaw gateway status --deep` or `openclaw doctor --deep` and confirm the stale client PID is gone.
|
||||
|
||||
Do not make an older Gateway accept a newer incompatible protocol. Protocol bumps protect the wire contract; rollback recovery is a process/version cleanup problem.
|
||||
|
||||
## Skill symlink skipped as path escape
|
||||
|
||||
Use this when logs include:
|
||||
|
||||
@@ -28,6 +28,10 @@ const inspectPortUsage = vi.fn(async (port: number) => ({
|
||||
listeners: [],
|
||||
hints: [],
|
||||
}));
|
||||
const inspectPortConnections = vi.fn(async (port: number) => ({
|
||||
port,
|
||||
connections: [],
|
||||
}));
|
||||
|
||||
function collectMatching<T, U>(
|
||||
items: readonly T[],
|
||||
@@ -114,6 +118,7 @@ vi.mock("../daemon/inspect.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../infra/ports.js", () => ({
|
||||
inspectPortConnections: (port: number) => inspectPortConnections(port),
|
||||
inspectPortUsage: (port: number) => inspectPortUsage(port),
|
||||
formatPortDiagnostics: () => ["Port 18789 is already in use."],
|
||||
}));
|
||||
@@ -192,6 +197,7 @@ describe("daemon-cli coverage", () => {
|
||||
serviceReadCommand.mockResolvedValue(null);
|
||||
resolveGatewayProbeAuthSafeWithSecretInputs.mockClear();
|
||||
findExtraGatewayServices.mockClear();
|
||||
inspectPortConnections.mockClear();
|
||||
buildGatewayInstallPlan.mockClear();
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
|
||||
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
|
||||
import type { PortConnections } from "../../infra/ports.js";
|
||||
import type { GatewayRestartHandoff } from "../../infra/restart-handoff.js";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
@@ -38,6 +39,12 @@ const inspectPortUsage = vi.fn(async (port: number) => ({
|
||||
listeners: [],
|
||||
hints: [],
|
||||
}));
|
||||
const inspectPortConnections = vi.fn<(port: number) => Promise<PortConnections>>(
|
||||
async (port: number) => ({
|
||||
port,
|
||||
connections: [],
|
||||
}),
|
||||
);
|
||||
const readLastGatewayErrorLine = vi.fn(async (_env?: NodeJS.ProcessEnv) => null);
|
||||
const readGatewayRestartHandoffSync = vi.fn<
|
||||
(_env?: NodeJS.ProcessEnv) => GatewayRestartHandoff | null
|
||||
@@ -166,6 +173,7 @@ vi.mock("../../gateway/net.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/ports.js", () => ({
|
||||
inspectPortConnections: (port: number) => inspectPortConnections(port),
|
||||
inspectPortUsage: (port: number) => inspectPortUsage(port),
|
||||
formatPortDiagnostics: () => [],
|
||||
}));
|
||||
@@ -222,6 +230,7 @@ describe("gatherDaemonStatus", () => {
|
||||
findStaleOpenClawUpdateLaunchdJobs.mockResolvedValue([]);
|
||||
loadGatewayTlsRuntime.mockClear();
|
||||
inspectGatewayRestart.mockClear();
|
||||
inspectPortConnections.mockClear();
|
||||
readGatewayRestartHandoffSync.mockClear();
|
||||
readConfigFileSnapshotCalls.mockClear();
|
||||
loadConfigCalls.mockClear();
|
||||
@@ -484,6 +493,59 @@ describe("gatherDaemonStatus", () => {
|
||||
|
||||
expect(readGatewayRestartHandoffSync).not.toHaveBeenCalled();
|
||||
expect(findStaleOpenClawUpdateLaunchdJobs).not.toHaveBeenCalled();
|
||||
expect(inspectPortConnections).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces established gateway connections during deep status", async () => {
|
||||
inspectPortConnections.mockResolvedValueOnce({
|
||||
port: 19001,
|
||||
connections: [
|
||||
{
|
||||
pid: 4242,
|
||||
ppid: 1,
|
||||
command: "node",
|
||||
commandLine: "node /tmp/newer-openclaw/dist/index.js logs --follow",
|
||||
address: "TCP 127.0.0.1:50123->127.0.0.1:19001 (ESTABLISHED)",
|
||||
direction: "client",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const status = await gatherDaemonStatus({
|
||||
rpc: {},
|
||||
probe: false,
|
||||
deep: true,
|
||||
});
|
||||
|
||||
expect(inspectPortConnections).toHaveBeenCalledWith(19001);
|
||||
expect(status.connections?.established).toEqual([
|
||||
{
|
||||
pid: 4242,
|
||||
ppid: 1,
|
||||
command: "node",
|
||||
commandLine: "node /tmp/newer-openclaw/dist/index.js logs --follow",
|
||||
address: "TCP 127.0.0.1:50123->127.0.0.1:19001 (ESTABLISHED)",
|
||||
direction: "client",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips established gateway connection scans for remote gateway status", async () => {
|
||||
daemonLoadedConfig = {
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: { url: "wss://gateway.example" },
|
||||
},
|
||||
};
|
||||
|
||||
const status = await gatherDaemonStatus({
|
||||
rpc: {},
|
||||
probe: false,
|
||||
deep: true,
|
||||
});
|
||||
|
||||
expect(inspectPortConnections).not.toHaveBeenCalled();
|
||||
expect(status.connections).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses the fast config path for plain same-file status reads", async () => {
|
||||
|
||||
@@ -26,7 +26,9 @@ import {
|
||||
import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js";
|
||||
import {
|
||||
formatPortDiagnostics,
|
||||
inspectPortConnections,
|
||||
inspectPortUsage,
|
||||
type PortConnection,
|
||||
type PortListener,
|
||||
type PortUsageStatus,
|
||||
} from "../../infra/ports.js";
|
||||
@@ -294,6 +296,10 @@ export type DaemonStatus = {
|
||||
listeners: PortListener[];
|
||||
hints: string[];
|
||||
};
|
||||
connections?: {
|
||||
port: number;
|
||||
established: PortConnection[];
|
||||
};
|
||||
lastError?: string;
|
||||
rpc?: {
|
||||
ok: boolean;
|
||||
@@ -460,6 +466,27 @@ async function inspectDaemonPortStatuses(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function inspectEstablishedGatewayClients(params: {
|
||||
daemonPort: number;
|
||||
deep?: boolean;
|
||||
gatewayMode?: string;
|
||||
}): Promise<DaemonStatus["connections"] | undefined> {
|
||||
if (params.deep !== true || params.gatewayMode === "remote") {
|
||||
return undefined;
|
||||
}
|
||||
const result = await inspectPortConnections(params.daemonPort).catch(() => null);
|
||||
const establishedClients = result?.connections.filter(
|
||||
(connection) => connection.direction !== "server",
|
||||
);
|
||||
if (!result || !establishedClients || establishedClients.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
port: result.port,
|
||||
established: establishedClients,
|
||||
};
|
||||
}
|
||||
|
||||
export async function gatherDaemonStatus(
|
||||
opts: {
|
||||
rpc: GatewayRpcOpts;
|
||||
@@ -508,6 +535,11 @@ export async function gatherDaemonStatus(
|
||||
daemonPort,
|
||||
cliPort,
|
||||
});
|
||||
const establishedClients = await inspectEstablishedGatewayClients({
|
||||
daemonPort,
|
||||
deep: opts.deep,
|
||||
gatewayMode: daemonCfg.gateway?.mode,
|
||||
});
|
||||
|
||||
const extraServices = opts.deep
|
||||
? await loadDaemonInspectModule()
|
||||
@@ -618,6 +650,7 @@ export async function gatherDaemonStatus(
|
||||
gateway,
|
||||
port: portStatus,
|
||||
...(portCliStatus ? { portCli: portCliStatus } : {}),
|
||||
...(establishedClients ? { connections: establishedClients } : {}),
|
||||
lastError,
|
||||
...(rpc
|
||||
? {
|
||||
|
||||
@@ -131,6 +131,48 @@ describe("printDaemonStatus", () => {
|
||||
expectMockLineContains(runtime.error, formatCliCommand("openclaw gateway restart"));
|
||||
});
|
||||
|
||||
it("prints established gateway client guidance gathered by deep status", () => {
|
||||
printDaemonStatus(
|
||||
{
|
||||
service: {
|
||||
label: "LaunchAgent",
|
||||
loaded: true,
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
runtime: { status: "running", pid: 8000 },
|
||||
},
|
||||
gateway: {
|
||||
bindMode: "loopback",
|
||||
bindHost: "127.0.0.1",
|
||||
port: 18789,
|
||||
portSource: "env/config",
|
||||
probeUrl: "ws://127.0.0.1:18789",
|
||||
},
|
||||
connections: {
|
||||
port: 18789,
|
||||
established: [
|
||||
{
|
||||
pid: 4242,
|
||||
ppid: 1,
|
||||
command: "node",
|
||||
commandLine: "/tmp/newer-openclaw/bin/openclaw logs --follow",
|
||||
address: "TCP 127.0.0.1:50123->127.0.0.1:18789 (ESTABLISHED)",
|
||||
direction: "client",
|
||||
},
|
||||
],
|
||||
},
|
||||
extraServices: [],
|
||||
},
|
||||
{ json: false },
|
||||
);
|
||||
|
||||
expectMockLineContains(runtime.log, "Established clients: 1");
|
||||
expectMockLineContains(runtime.log, "pid=4242");
|
||||
expectMockLineContains(runtime.log, "newer-openclaw");
|
||||
expectMockLineContains(runtime.log, "client");
|
||||
expectMockLineContains(runtime.log, "protocol mismatch after rollback");
|
||||
});
|
||||
|
||||
it("prints stale updater launchd job guidance", () => {
|
||||
printDaemonStatus(
|
||||
{
|
||||
|
||||
@@ -72,6 +72,20 @@ function formatCliVersionLine(cli: DaemonStatus["cli"]): string | null {
|
||||
return cli.entrypoint ? `${cli.version} (${shortenHomePath(cli.entrypoint)})` : cli.version;
|
||||
}
|
||||
|
||||
function formatConnectionLine(
|
||||
connection: NonNullable<DaemonStatus["connections"]>["established"][number],
|
||||
) {
|
||||
const pid = connection.pid ? `pid=${connection.pid}` : "pid=?";
|
||||
const ppid = connection.ppid ? ` ppid=${connection.ppid}` : "";
|
||||
const direction = ` ${connection.direction}`;
|
||||
const command = connection.command ? ` ${connection.command}` : "";
|
||||
const address = connection.address ? ` ${connection.address}` : "";
|
||||
const commandLine = connection.commandLine
|
||||
? ` cmd=${shortenHomePath(connection.commandLine)}`
|
||||
: "";
|
||||
return `${pid}${ppid}${direction}${command}${address}${commandLine}`;
|
||||
}
|
||||
|
||||
export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
||||
if (opts.json) {
|
||||
const sanitized = sanitizeDaemonStatusForJson(status);
|
||||
@@ -285,6 +299,26 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
|
||||
spacer();
|
||||
}
|
||||
|
||||
if (status.connections?.established.length) {
|
||||
defaultRuntime.log(
|
||||
`${label("Established clients:")} ${infoText(String(status.connections.established.length))}`,
|
||||
);
|
||||
for (const connection of status.connections.established.slice(0, 8)) {
|
||||
defaultRuntime.log(` ${infoText(formatConnectionLine(connection))}`);
|
||||
}
|
||||
if (status.connections.established.length > 8) {
|
||||
defaultRuntime.log(
|
||||
` ${infoText(`... ${status.connections.established.length - 8} more connection(s)`)}`,
|
||||
);
|
||||
}
|
||||
defaultRuntime.log(
|
||||
warnText(
|
||||
"If logs show protocol mismatch after rollback, stop stale OpenClaw client processes listed here and re-run gateway status.",
|
||||
),
|
||||
);
|
||||
spacer();
|
||||
}
|
||||
|
||||
const systemdUnavailable =
|
||||
process.platform === "linux" && isSystemdUnavailableDetail(service.runtime?.detail);
|
||||
if (systemdUnavailable) {
|
||||
|
||||
@@ -21,6 +21,7 @@ const service = vi.hoisted(() => ({
|
||||
const note = vi.hoisted(() => vi.fn());
|
||||
const sleep = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const healthCommand = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const inspectPortConnections = vi.hoisted(() => vi.fn());
|
||||
const inspectPortUsage = vi.hoisted(() => vi.fn());
|
||||
const formatPortDiagnostics = vi.hoisted(() => vi.fn(() => ["Port 18789 is already in use."]));
|
||||
const isExpectedGatewayListeners = vi.hoisted(() => vi.fn(() => false));
|
||||
@@ -88,6 +89,7 @@ vi.mock("../daemon/systemd.js", async () => {
|
||||
});
|
||||
|
||||
vi.mock("../infra/ports.js", () => ({
|
||||
inspectPortConnections,
|
||||
inspectPortUsage,
|
||||
formatPortDiagnostics,
|
||||
isExpectedGatewayListeners,
|
||||
@@ -164,6 +166,10 @@ describe("maybeRepairGatewayDaemon", () => {
|
||||
listeners: [],
|
||||
hints: [],
|
||||
});
|
||||
inspectPortConnections.mockResolvedValue({
|
||||
port: 18789,
|
||||
connections: [],
|
||||
});
|
||||
isExpectedGatewayListeners.mockReturnValue(false);
|
||||
});
|
||||
|
||||
@@ -324,6 +330,72 @@ describe("maybeRepairGatewayDaemon", () => {
|
||||
await runNonInteractiveRepair();
|
||||
|
||||
expect(readGatewayRestartHandoffSync).not.toHaveBeenCalled();
|
||||
expect(inspectPortConnections).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports established gateway clients during deep doctor", async () => {
|
||||
setPlatform("linux");
|
||||
inspectPortConnections.mockResolvedValueOnce({
|
||||
port: 18789,
|
||||
connections: [
|
||||
{
|
||||
pid: 4242,
|
||||
command: "node",
|
||||
commandLine: "/tmp/newer-openclaw/bin/openclaw logs --follow",
|
||||
address: "TCP 127.0.0.1:50123->127.0.0.1:18789 (ESTABLISHED)",
|
||||
direction: "client",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await maybeRepairGatewayDaemon({
|
||||
cfg: { gateway: {} },
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
prompter: createDoctorPrompter({
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
options: { deep: true, nonInteractive: true },
|
||||
}),
|
||||
options: { deep: true, nonInteractive: true },
|
||||
gatewayDetailsMessage: "details",
|
||||
healthOk: false,
|
||||
});
|
||||
|
||||
const gatewayClientNote = note.mock.calls.find(([, label]) => label === "Gateway clients");
|
||||
expect(gatewayClientNote?.[0]).toContain("pid=4242");
|
||||
expect(gatewayClientNote?.[0]).toContain("protocol mismatch after rollback");
|
||||
});
|
||||
|
||||
it("reports established gateway clients during healthy deep doctor", async () => {
|
||||
setPlatform("linux");
|
||||
inspectPortConnections.mockResolvedValueOnce({
|
||||
port: 18789,
|
||||
connections: [
|
||||
{
|
||||
pid: 5151,
|
||||
command: "node",
|
||||
commandLine: "/tmp/newer-openclaw/bin/openclaw logs --follow",
|
||||
address: "TCP 127.0.0.1:50123->127.0.0.1:18789 (ESTABLISHED)",
|
||||
direction: "client",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await maybeRepairGatewayDaemon({
|
||||
cfg: { gateway: {} },
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
prompter: createDoctorPrompter({
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() },
|
||||
options: { deep: true, nonInteractive: true },
|
||||
}),
|
||||
options: { deep: true, nonInteractive: true },
|
||||
gatewayDetailsMessage: "details",
|
||||
healthOk: true,
|
||||
});
|
||||
|
||||
expect(inspectPortUsage).not.toHaveBeenCalled();
|
||||
const gatewayClientNote = note.mock.calls.find(([, label]) => label === "Gateway clients");
|
||||
expect(gatewayClientNote?.[0]).toContain("pid=5151");
|
||||
expect(gatewayClientNote?.[0]).toContain("protocol mismatch after rollback");
|
||||
});
|
||||
|
||||
it("suppresses busy-port note for expected Gateway listeners", async () => {
|
||||
|
||||
@@ -17,8 +17,10 @@ import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js";
|
||||
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||
import {
|
||||
formatPortDiagnostics,
|
||||
inspectPortConnections,
|
||||
inspectPortUsage,
|
||||
isExpectedGatewayListeners,
|
||||
type PortConnection,
|
||||
} from "../infra/ports.js";
|
||||
import {
|
||||
formatGatewayRestartHandoffDiagnostic,
|
||||
@@ -111,6 +113,40 @@ function renderBlockingSystemGatewayServices(services: ExtraGatewayService[]): s
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderEstablishedGatewayConnections(connections: PortConnection[]): string {
|
||||
return [
|
||||
"Established Gateway TCP clients detected:",
|
||||
...connections.slice(0, 8).map((connection) => {
|
||||
const pid = connection.pid ? `pid=${connection.pid}` : "pid=?";
|
||||
const direction = connection.direction;
|
||||
const command = connection.command ? ` ${connection.command}` : "";
|
||||
const address = connection.address ? ` ${connection.address}` : "";
|
||||
const commandLine = connection.commandLine ? ` cmd=${connection.commandLine}` : "";
|
||||
return `- ${pid} ${direction}${command}${address}${commandLine}`;
|
||||
}),
|
||||
...(connections.length > 8 ? [`- ... ${connections.length - 8} more connection(s)`] : []),
|
||||
"If logs show protocol mismatch after rollback, stop stale OpenClaw client processes listed here and rerun doctor.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function maybeReportEstablishedGatewayClients(params: {
|
||||
cfg: OpenClawConfig;
|
||||
deep: boolean;
|
||||
port?: number;
|
||||
}): Promise<void> {
|
||||
if (!params.deep || params.cfg.gateway?.mode === "remote") {
|
||||
return;
|
||||
}
|
||||
const port = params.port ?? resolveGatewayPort(params.cfg, process.env);
|
||||
const connections = await inspectPortConnections(port).catch(() => null);
|
||||
const establishedClients = connections?.connections.filter(
|
||||
(connection) => connection.direction !== "server",
|
||||
);
|
||||
if (establishedClients && establishedClients.length > 0) {
|
||||
note(renderEstablishedGatewayConnections(establishedClients), "Gateway clients");
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeRepairGatewayDaemon(params: {
|
||||
cfg: OpenClawConfig;
|
||||
runtime: RuntimeEnv;
|
||||
@@ -120,6 +156,10 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
healthOk: boolean;
|
||||
}) {
|
||||
if (params.healthOk) {
|
||||
await maybeReportEstablishedGatewayClients({
|
||||
cfg: params.cfg,
|
||||
deep: params.options.deep ?? false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,6 +222,11 @@ export async function maybeRepairGatewayDaemon(params: {
|
||||
if (params.cfg.gateway?.mode !== "remote") {
|
||||
const port = resolveGatewayPort(params.cfg, process.env);
|
||||
const diagnostics = await inspectPortUsage(port);
|
||||
await maybeReportEstablishedGatewayClients({
|
||||
cfg: params.cfg,
|
||||
deep: params.options.deep ?? false,
|
||||
port,
|
||||
});
|
||||
if (
|
||||
diagnostics.status === "busy" &&
|
||||
!isExpectedGatewayListeners(diagnostics.listeners, diagnostics.port)
|
||||
|
||||
@@ -548,7 +548,7 @@ export function attachGatewayWsMessageHandler(params: GatewayWsMessageHandlerPar
|
||||
minimumProbeProtocol: MIN_PROBE_PROTOCOL_VERSION,
|
||||
});
|
||||
logWsControl.warn(
|
||||
`protocol mismatch conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version}`,
|
||||
`protocol mismatch conn=${connId} peer=${formatForLog(peerLabel)} remote=${remoteAddr ?? "?"} remotePort=${remotePort ?? "?"} client=${formatForLog(clientLabel)} ${connectParams.client.mode} v${formatForLog(connectParams.client.version)} min=${minProtocol} max=${maxProtocol} expected=${PROTOCOL_VERSION} probeMin=${MIN_PROBE_PROTOCOL_VERSION} instance=${formatForLog(connectParams.client.instanceId ?? "n/a")}`,
|
||||
);
|
||||
sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, "protocol mismatch", {
|
||||
details: {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import os from "node:os";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { isErrno } from "./errors.js";
|
||||
import { buildPortHints } from "./ports-format.js";
|
||||
import { resolveLsofCommand } from "./ports-lsof.js";
|
||||
import { tryListenOnPort } from "./ports-probe.js";
|
||||
import type { PortListener, PortUsage, PortUsageStatus } from "./ports-types.js";
|
||||
import type {
|
||||
PortConnection,
|
||||
PortConnectionDirection,
|
||||
PortConnections,
|
||||
PortListener,
|
||||
PortUsage,
|
||||
PortUsageStatus,
|
||||
} from "./ports-types.js";
|
||||
|
||||
type CommandResult = {
|
||||
stdout: string;
|
||||
@@ -58,6 +66,148 @@ function parseLsofFieldOutput(output: string): PortListener[] {
|
||||
return listeners;
|
||||
}
|
||||
|
||||
function normalizeTcpHost(host: string): string {
|
||||
const normalized = host.toLowerCase();
|
||||
return normalized.startsWith("::ffff:") ? normalized.slice("::ffff:".length) : normalized;
|
||||
}
|
||||
|
||||
function parseTcpEndpoint(raw: string): { host: string; port: number } | null {
|
||||
const endpoint = raw.trim();
|
||||
const bracketMatch = endpoint.match(/^\[([^\]]+)\]:(\d+)$/);
|
||||
if (bracketMatch) {
|
||||
const port = Number.parseInt(bracketMatch[2], 10);
|
||||
return Number.isFinite(port) ? { host: normalizeTcpHost(bracketMatch[1]), port } : null;
|
||||
}
|
||||
const lastColon = endpoint.lastIndexOf(":");
|
||||
if (lastColon <= 0 || lastColon >= endpoint.length - 1) {
|
||||
return null;
|
||||
}
|
||||
const port = Number.parseInt(endpoint.slice(lastColon + 1), 10);
|
||||
if (!Number.isFinite(port)) {
|
||||
return null;
|
||||
}
|
||||
return { host: normalizeTcpHost(endpoint.slice(0, lastColon)), port };
|
||||
}
|
||||
|
||||
function parseLsofTcpConnectionAddress(
|
||||
address: string | undefined,
|
||||
): { local: { host: string; port: number }; remote: { host: string; port: number } } | null {
|
||||
const normalized = address
|
||||
?.replace(/^tcp\s+/i, "")
|
||||
.replace(/\s*\([^)]*\)\s*$/i, "")
|
||||
.trim();
|
||||
if (!normalized?.includes("->")) {
|
||||
return null;
|
||||
}
|
||||
const [localRaw, remoteRaw] = normalized.split("->", 2);
|
||||
const local = parseTcpEndpoint(localRaw ?? "");
|
||||
const remote = parseTcpEndpoint(remoteRaw ?? "");
|
||||
return local && remote ? { local, remote } : null;
|
||||
}
|
||||
|
||||
function resolveLocalNetworkAddresses(): Set<string> {
|
||||
const addresses = new Set(["127.0.0.1", "::1", "localhost", "0.0.0.0", "::"]);
|
||||
for (const entries of Object.values(os.networkInterfaces())) {
|
||||
for (const entry of entries ?? []) {
|
||||
addresses.add(entry.address.toLowerCase());
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
function isGatewayConnectionAddress(
|
||||
address: string | undefined,
|
||||
port: number,
|
||||
localAddresses: Set<string>,
|
||||
): boolean {
|
||||
const parsed = parseLsofTcpConnectionAddress(address);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.local.port === port) {
|
||||
return true;
|
||||
}
|
||||
return parsed.remote.port === port && localAddresses.has(parsed.remote.host);
|
||||
}
|
||||
|
||||
function resolveLsofTcpDirection(
|
||||
address: string | undefined,
|
||||
port: number,
|
||||
): PortConnectionDirection {
|
||||
const parsed = parseLsofTcpConnectionAddress(address);
|
||||
if (!parsed) {
|
||||
return "unknown";
|
||||
}
|
||||
if (parsed.local.port === port) {
|
||||
return "server";
|
||||
}
|
||||
return parsed.remote.port === port ? "client" : "unknown";
|
||||
}
|
||||
|
||||
function parseLsofConnectionFieldOutput(output: string, port: number): PortConnection[] {
|
||||
const connections: PortConnection[] = [];
|
||||
const localAddresses = resolveLocalNetworkAddresses();
|
||||
for (const entry of parseLsofFieldOutput(output)) {
|
||||
if (!isGatewayConnectionAddress(entry.address, port, localAddresses)) {
|
||||
continue;
|
||||
}
|
||||
const connection = entry as PortConnection;
|
||||
connection.direction = resolveLsofTcpDirection(entry.address, port);
|
||||
connections.push(connection);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
function parseSsConnectionEndpoint(raw: string): string | null {
|
||||
if (raw.startsWith("users:")) {
|
||||
return null;
|
||||
}
|
||||
if (raw.includes(":")) {
|
||||
return raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseSsConnections(output: string, port: number): PortConnection[] {
|
||||
const connections: PortConnection[] = [];
|
||||
const localAddresses = resolveLocalNetworkAddresses();
|
||||
for (const rawLine of output.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
const endpoints = line
|
||||
.split(/\s+/)
|
||||
.map(parseSsConnectionEndpoint)
|
||||
.filter((endpoint): endpoint is string => Boolean(endpoint));
|
||||
if (endpoints.length < 2) {
|
||||
continue;
|
||||
}
|
||||
const [local, remote] = endpoints.slice(-2);
|
||||
const address = `TCP ${local}->${remote} (ESTABLISHED)`;
|
||||
if (!isGatewayConnectionAddress(address, port, localAddresses)) {
|
||||
continue;
|
||||
}
|
||||
const connection: PortConnection = {
|
||||
address,
|
||||
direction: resolveLsofTcpDirection(address, port),
|
||||
};
|
||||
const pidMatch = line.match(/pid=(\d+)/);
|
||||
if (pidMatch) {
|
||||
const pid = Number.parseInt(pidMatch[1], 10);
|
||||
if (Number.isFinite(pid)) {
|
||||
connection.pid = pid;
|
||||
}
|
||||
}
|
||||
const commandMatch = line.match(/users:\(\("([^"]+)"/);
|
||||
if (commandMatch?.[1]) {
|
||||
connection.command = commandMatch[1];
|
||||
}
|
||||
connections.push(connection);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
async function enrichUnixListenerProcessInfo(listeners: PortListener[]): Promise<void> {
|
||||
await Promise.all(
|
||||
listeners.map(async (listener) => {
|
||||
@@ -82,6 +232,71 @@ async function enrichUnixListenerProcessInfo(listeners: PortListener[]): Promise
|
||||
);
|
||||
}
|
||||
|
||||
async function readUnixEstablishedConnectionsFromSs(
|
||||
port: number,
|
||||
): Promise<{ connections: PortConnection[]; detail?: string; errors: string[] }> {
|
||||
const errors: string[] = [];
|
||||
const res = await runCommandSafe([
|
||||
"ss",
|
||||
"-H",
|
||||
"-tnp",
|
||||
"state",
|
||||
"established",
|
||||
`( sport = :${port} or dport = :${port} )`,
|
||||
]);
|
||||
if (res.code === 0) {
|
||||
const connections = parseSsConnections(res.stdout, port);
|
||||
await enrichUnixListenerProcessInfo(connections);
|
||||
return { connections, detail: res.stdout.trim() || undefined, errors };
|
||||
}
|
||||
const stderr = res.stderr.trim();
|
||||
if (res.code === 1 && !res.error && !stderr) {
|
||||
return { connections: [], detail: undefined, errors };
|
||||
}
|
||||
if (res.error) {
|
||||
errors.push(res.error);
|
||||
}
|
||||
const detail = [stderr, res.stdout.trim()].filter(Boolean).join("\n");
|
||||
if (detail) {
|
||||
errors.push(detail);
|
||||
}
|
||||
return { connections: [], detail: undefined, errors };
|
||||
}
|
||||
|
||||
async function readUnixEstablishedConnections(
|
||||
port: number,
|
||||
): Promise<{ connections: PortConnection[]; detail?: string; errors: string[] }> {
|
||||
const lsof = await resolveLsofCommand();
|
||||
const res = await runCommandSafe([lsof, "-nP", `-iTCP:${port}`, "-sTCP:ESTABLISHED", "-FpFcn"]);
|
||||
if (res.code === 0) {
|
||||
const connections = parseLsofConnectionFieldOutput(res.stdout, port);
|
||||
await enrichUnixListenerProcessInfo(connections);
|
||||
return { connections, detail: res.stdout.trim() || undefined, errors: [] };
|
||||
}
|
||||
const stderr = res.stderr.trim();
|
||||
if (res.code === 1 && !res.error && !stderr) {
|
||||
return { connections: [], detail: undefined, errors: [] };
|
||||
}
|
||||
const errors: string[] = [];
|
||||
if (res.error) {
|
||||
errors.push(res.error);
|
||||
}
|
||||
const detail = [stderr, res.stdout.trim()].filter(Boolean).join("\n");
|
||||
if (detail) {
|
||||
errors.push(detail);
|
||||
}
|
||||
|
||||
const ssFallback = await readUnixEstablishedConnectionsFromSs(port);
|
||||
if (ssFallback.connections.length > 0) {
|
||||
return ssFallback;
|
||||
}
|
||||
return {
|
||||
connections: [],
|
||||
detail: undefined,
|
||||
errors: [...errors, ...ssFallback.errors],
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveUnixCommandLine(pid: number): Promise<string | undefined> {
|
||||
const res = await runCommandSafe(["ps", "-p", String(pid), "-o", "command="]);
|
||||
if (res.code !== 0) {
|
||||
@@ -233,6 +448,41 @@ function parseNetstatListeners(output: string, port: number): PortListener[] {
|
||||
return listeners;
|
||||
}
|
||||
|
||||
function parseNetstatConnections(output: string, port: number): PortConnection[] {
|
||||
const connections: PortConnection[] = [];
|
||||
const localAddresses = resolveLocalNetworkAddresses();
|
||||
for (const rawLine of output.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || !normalizeLowercaseStringOrEmpty(line).includes("established")) {
|
||||
continue;
|
||||
}
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 5) {
|
||||
continue;
|
||||
}
|
||||
const local = parts[1];
|
||||
const remote = parts[2];
|
||||
const pidRaw = parts.at(-1);
|
||||
if (!local || !remote || !pidRaw) {
|
||||
continue;
|
||||
}
|
||||
const address = `TCP ${local}->${remote} (ESTABLISHED)`;
|
||||
if (!isGatewayConnectionAddress(address, port, localAddresses)) {
|
||||
continue;
|
||||
}
|
||||
const connection: PortConnection = {
|
||||
address,
|
||||
direction: resolveLsofTcpDirection(address, port),
|
||||
};
|
||||
const pid = Number.parseInt(pidRaw, 10);
|
||||
if (Number.isFinite(pid)) {
|
||||
connection.pid = pid;
|
||||
}
|
||||
connections.push(connection);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
async function resolveWindowsImageName(pid: number): Promise<string | undefined> {
|
||||
const res = await runCommandSafe(["tasklist", "/FI", `PID eq ${pid}`, "/FO", "LIST"]);
|
||||
if (res.code !== 0) {
|
||||
@@ -322,6 +572,42 @@ async function readWindowsListeners(
|
||||
return { listeners, detail: res.stdout.trim() || undefined, errors };
|
||||
}
|
||||
|
||||
async function readWindowsEstablishedConnections(
|
||||
port: number,
|
||||
): Promise<{ connections: PortConnection[]; detail?: string; errors: string[] }> {
|
||||
const errors: string[] = [];
|
||||
const res = await runCommandSafe(["netstat", "-ano", "-p", "tcp"]);
|
||||
if (res.code !== 0) {
|
||||
if (res.error) {
|
||||
errors.push(res.error);
|
||||
}
|
||||
const detail = [res.stderr.trim(), res.stdout.trim()].filter(Boolean).join("\n");
|
||||
if (detail) {
|
||||
errors.push(detail);
|
||||
}
|
||||
return { connections: [], errors };
|
||||
}
|
||||
const connections = parseNetstatConnections(res.stdout, port);
|
||||
await Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
if (!connection.pid) {
|
||||
return;
|
||||
}
|
||||
const [imageName, commandLine] = await Promise.all([
|
||||
resolveWindowsImageName(connection.pid),
|
||||
resolveWindowsCommandLine(connection.pid),
|
||||
]);
|
||||
if (imageName) {
|
||||
connection.command = imageName;
|
||||
}
|
||||
if (commandLine) {
|
||||
connection.commandLine = commandLine;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return { connections, detail: res.stdout.trim() || undefined, errors };
|
||||
}
|
||||
|
||||
async function tryListenOnHost(port: number, host: string): Promise<PortUsageStatus | "skip"> {
|
||||
try {
|
||||
await tryListenOnPort({ port, host, exclusive: true });
|
||||
@@ -380,3 +666,16 @@ export async function inspectPortUsage(port: number): Promise<PortUsage> {
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function inspectPortConnections(port: number): Promise<PortConnections> {
|
||||
const result =
|
||||
process.platform === "win32"
|
||||
? await readWindowsEstablishedConnections(port)
|
||||
: await readUnixEstablishedConnections(port);
|
||||
return {
|
||||
port,
|
||||
connections: result.connections,
|
||||
detail: result.detail,
|
||||
errors: result.errors.length > 0 ? result.errors : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ export type PortListener = {
|
||||
address?: string;
|
||||
};
|
||||
|
||||
export type PortConnectionDirection = "client" | "server" | "unknown";
|
||||
|
||||
export type PortConnection = PortListener & {
|
||||
direction: PortConnectionDirection;
|
||||
};
|
||||
|
||||
export type PortUsageStatus = "free" | "busy" | "unknown";
|
||||
|
||||
export type PortUsage = {
|
||||
@@ -19,3 +25,10 @@ export type PortUsage = {
|
||||
};
|
||||
|
||||
export type PortListenerKind = "gateway" | "ssh" | "unknown";
|
||||
|
||||
export type PortConnections = {
|
||||
port: number;
|
||||
connections: PortConnection[];
|
||||
detail?: string;
|
||||
errors?: string[];
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||
}));
|
||||
|
||||
let inspectPortConnections: typeof import("./ports-inspect.js").inspectPortConnections;
|
||||
let inspectPortUsage: typeof import("./ports-inspect.js").inspectPortUsage;
|
||||
let ensurePortAvailable: typeof import("./ports.js").ensurePortAvailable;
|
||||
let handlePortError: typeof import("./ports.js").handlePortError;
|
||||
@@ -53,7 +54,7 @@ async function listenServer(
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
({ inspectPortUsage } = await import("./ports-inspect.js"));
|
||||
({ inspectPortConnections, inspectPortUsage } = await import("./ports-inspect.js"));
|
||||
({ ensurePortAvailable, handlePortError, PortInUseError } = await import("./ports.js"));
|
||||
});
|
||||
|
||||
@@ -197,9 +198,156 @@ describeUnix("inspectPortUsage", () => {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it("reports established gateway client connections from lsof", async () => {
|
||||
runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
|
||||
const command = argv[0];
|
||||
if (typeof command !== "string") {
|
||||
return { stdout: "", stderr: "", code: 1 };
|
||||
}
|
||||
if (command.includes("lsof")) {
|
||||
return {
|
||||
stdout:
|
||||
"p111\ncnode\nnTCP 127.0.0.1:50123->127.0.0.1:18789 (ESTABLISHED)\n" +
|
||||
"p222\ncnode\nnTCP 127.0.0.1:18789->127.0.0.1:50123 (ESTABLISHED)\n" +
|
||||
"p444\ncnode\nnTCP 127.0.0.1:50125->[::ffff:127.0.0.1]:18789 (ESTABLISHED)\n" +
|
||||
"p333\ncBrowser\nnTCP 127.0.0.1:50124->198.51.100.7:18789 (ESTABLISHED)\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (command === "ps") {
|
||||
const pid = argv[2];
|
||||
if (argv.includes("command=")) {
|
||||
return {
|
||||
stdout:
|
||||
pid === "111"
|
||||
? "node /tmp/newer-openclaw/dist/index.js logs --follow\n"
|
||||
: pid === "222"
|
||||
? "node /tmp/older-openclaw/dist/index.js gateway run\n"
|
||||
: "browser https://example.invalid/\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (argv.includes("user=")) {
|
||||
return { stdout: "tester\n", stderr: "", code: 0 };
|
||||
}
|
||||
if (argv.includes("ppid=")) {
|
||||
return { stdout: "1\n", stderr: "", code: 0 };
|
||||
}
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 1 };
|
||||
});
|
||||
|
||||
const result = await inspectPortConnections(18789);
|
||||
|
||||
expect(result.connections).toHaveLength(3);
|
||||
expect(result.connections[0]).toMatchObject({
|
||||
pid: 111,
|
||||
direction: "client",
|
||||
commandLine: "node /tmp/newer-openclaw/dist/index.js logs --follow",
|
||||
});
|
||||
expect(result.connections[1]).toMatchObject({
|
||||
pid: 222,
|
||||
direction: "server",
|
||||
});
|
||||
expect(result.connections[2]).toMatchObject({
|
||||
pid: 444,
|
||||
direction: "client",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to ss for established gateway client connections", async () => {
|
||||
runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
|
||||
const command = argv[0];
|
||||
if (typeof command !== "string") {
|
||||
return { stdout: "", stderr: "", code: 1 };
|
||||
}
|
||||
if (command.includes("lsof")) {
|
||||
return { stdout: "", stderr: "lsof: not found\n", code: 1 };
|
||||
}
|
||||
if (command === "ss") {
|
||||
return {
|
||||
stdout:
|
||||
'0 0 127.0.0.1:50123 127.0.0.1:18789 users:(("node",pid=111,fd=12))\n' +
|
||||
'0 0 127.0.0.1:50124 198.51.100.7:18789 users:(("browser",pid=333,fd=9))\n',
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (command === "ps") {
|
||||
const pid = argv[2];
|
||||
if (argv.includes("command=")) {
|
||||
return {
|
||||
stdout:
|
||||
pid === "111"
|
||||
? "node /tmp/newer-openclaw/dist/index.js logs --follow\n"
|
||||
: "browser https://example.invalid/\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (argv.includes("user=")) {
|
||||
return { stdout: "tester\n", stderr: "", code: 0 };
|
||||
}
|
||||
if (argv.includes("ppid=")) {
|
||||
return { stdout: "1\n", stderr: "", code: 0 };
|
||||
}
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 1 };
|
||||
});
|
||||
|
||||
const result = await inspectPortConnections(18789);
|
||||
|
||||
expect(result.connections).toHaveLength(1);
|
||||
expect(result.connections[0]).toMatchObject({
|
||||
pid: 111,
|
||||
direction: "client",
|
||||
commandLine: "node /tmp/newer-openclaw/dist/index.js logs --follow",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("inspectPortUsage on Windows", () => {
|
||||
it("reports established gateway client connections from netstat", async () => {
|
||||
setPlatform("win32");
|
||||
runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
|
||||
const [command] = argv;
|
||||
if (command === "netstat") {
|
||||
return {
|
||||
stdout:
|
||||
" TCP 127.0.0.1:50123 127.0.0.1:18789 ESTABLISHED 4242\r\n" +
|
||||
" TCP 127.0.0.1:50124 198.51.100.7:18789 ESTABLISHED 5000\r\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
if (command === "tasklist") {
|
||||
return { stdout: "Image Name: node.exe\r\n", stderr: "", code: 0 };
|
||||
}
|
||||
if (command === "powershell") {
|
||||
return {
|
||||
stdout:
|
||||
'"C:\\Program Files\\nodejs\\node.exe" C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\openclaw\\dist\\index.js logs --follow\r\n',
|
||||
stderr: "",
|
||||
code: 0,
|
||||
};
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 1 };
|
||||
});
|
||||
|
||||
const result = await inspectPortConnections(18789);
|
||||
|
||||
expect(result.connections).toHaveLength(1);
|
||||
expect(result.connections[0]).toMatchObject({
|
||||
pid: 4242,
|
||||
command: "node.exe",
|
||||
direction: "client",
|
||||
});
|
||||
expect(result.connections[0]?.commandLine).toContain("openclaw");
|
||||
});
|
||||
|
||||
it("uses PowerShell process command lines to classify OpenClaw listeners", async () => {
|
||||
setPlatform("win32");
|
||||
runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
|
||||
|
||||
@@ -6,7 +6,14 @@ import { isErrno } from "./errors.js";
|
||||
import { formatPortDiagnostics } from "./ports-format.js";
|
||||
import { inspectPortUsage } from "./ports-inspect.js";
|
||||
import { tryListenOnPort } from "./ports-probe.js";
|
||||
import type { PortListener, PortListenerKind, PortUsage, PortUsageStatus } from "./ports-types.js";
|
||||
import type {
|
||||
PortConnection,
|
||||
PortConnections,
|
||||
PortListener,
|
||||
PortListenerKind,
|
||||
PortUsage,
|
||||
PortUsageStatus,
|
||||
} from "./ports-types.js";
|
||||
|
||||
class PortInUseError extends Error {
|
||||
port: number;
|
||||
@@ -85,7 +92,14 @@ export async function handlePortError(
|
||||
}
|
||||
|
||||
export { PortInUseError };
|
||||
export type { PortListener, PortListenerKind, PortUsage, PortUsageStatus };
|
||||
export type {
|
||||
PortConnection,
|
||||
PortConnections,
|
||||
PortListener,
|
||||
PortListenerKind,
|
||||
PortUsage,
|
||||
PortUsageStatus,
|
||||
};
|
||||
export {
|
||||
buildPortHints,
|
||||
classifyPortListener,
|
||||
@@ -94,4 +108,4 @@ export {
|
||||
isExpectedGatewayListeners,
|
||||
isSingleExpectedGatewayListener,
|
||||
} from "./ports-format.js";
|
||||
export { inspectPortUsage } from "./ports-inspect.js";
|
||||
export { inspectPortConnections, inspectPortUsage } from "./ports-inspect.js";
|
||||
|
||||
Reference in New Issue
Block a user