mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(gateway): distinguish reachable gateway from failed status probe (#98183)
* fix(gateway): distinguish reachable gateway from failed status probe * fix(status): gate owns-port RPC recovery guidance on no stale gateway PIDs inspectGatewayRestart can set health.healthy from bare reachability after ownership attribution failed, while still returning a non-empty staleGatewayPids. Printing owns-port recovery guidance in that case contradicted the dedicated stale-PID diagnostic below it. Require staleGatewayPids to be empty before treating healthy as owns-port proof.
This commit is contained in:
@@ -866,4 +866,162 @@ describe("printDaemonStatus", () => {
|
||||
expect(errors).toContain("Service is loaded but not running (likely exited immediately).");
|
||||
expect(errors).not.toContain("systemd stopped restarting the gateway");
|
||||
});
|
||||
|
||||
it("steers a failed RPC probe to credentials/config when the gateway process owns the port", () => {
|
||||
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",
|
||||
},
|
||||
rpc: {
|
||||
ok: false,
|
||||
error: "gateway closed (1008 policy violation: invalid token)",
|
||||
url: "ws://127.0.0.1:18789",
|
||||
},
|
||||
health: {
|
||||
healthy: true,
|
||||
staleGatewayPids: [],
|
||||
},
|
||||
extraServices: [],
|
||||
},
|
||||
{ json: false },
|
||||
);
|
||||
|
||||
expectMockLineContains(
|
||||
runtime.log,
|
||||
"Gateway process is running and owns the gateway port, so this is not a warm-up delay",
|
||||
);
|
||||
expectMockLineContains(runtime.log, "Check the probe credentials/config");
|
||||
const logged = runtime.log.mock.calls.map(([line]) => line).join("\n");
|
||||
expect(logged).not.toContain("Warm-up: launch agents");
|
||||
});
|
||||
|
||||
it("keeps the warm-up hint (not owns-port guidance) when healthy is reachability-only and a stale gateway PID is still held", () => {
|
||||
// inspectGatewayRestart can set healthy from reachability after ownership failed,
|
||||
// while still returning non-empty staleGatewayPids. That must not be treated as
|
||||
// owns-port proof, or this message would contradict the stale-PID diagnostic below.
|
||||
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",
|
||||
},
|
||||
rpc: {
|
||||
ok: false,
|
||||
error: "gateway closed (1008 policy violation: invalid token)",
|
||||
url: "ws://127.0.0.1:18789",
|
||||
},
|
||||
health: {
|
||||
healthy: true,
|
||||
staleGatewayPids: [9000],
|
||||
},
|
||||
extraServices: [],
|
||||
},
|
||||
{ json: false },
|
||||
);
|
||||
|
||||
const logged = runtime.log.mock.calls.map(([line]) => line).join("\n");
|
||||
expect(logged).toContain("Warm-up: launch agents can take a few seconds");
|
||||
expect(logged).not.toContain("Gateway process is running and owns the gateway port");
|
||||
const errors = runtime.error.mock.calls.map(([line]) => line).join("\n");
|
||||
expect(errors).toContain("Gateway runtime PID does not own the listening port");
|
||||
});
|
||||
|
||||
it("keeps the warm-up hint for an unhealthy gateway, even with the port held", () => {
|
||||
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",
|
||||
},
|
||||
rpc: {
|
||||
ok: false,
|
||||
error: "gateway closed (1006 abnormal closure (no close frame))",
|
||||
url: "ws://127.0.0.1:18789",
|
||||
},
|
||||
port: {
|
||||
port: 18789,
|
||||
status: "busy",
|
||||
listeners: [],
|
||||
hints: [],
|
||||
},
|
||||
health: {
|
||||
healthy: false,
|
||||
staleGatewayPids: [],
|
||||
},
|
||||
extraServices: [],
|
||||
},
|
||||
{ json: false },
|
||||
);
|
||||
|
||||
// health.healthy === false is ambiguous (not-yet-bound / foreign port conflict / stale
|
||||
// PID), so it keeps the warm-up hint rather than steering to a restart; the dedicated
|
||||
// stale-PID / port-not-listening / port-conflict blocks own those cases.
|
||||
expectMockLineContains(runtime.log, "Warm-up: launch agents can take a few seconds");
|
||||
const logged = runtime.log.mock.calls.map(([line]) => line).join("\n");
|
||||
expect(logged).not.toContain("Gateway process is");
|
||||
});
|
||||
|
||||
it("keeps the warm-up hint when gateway health is unknown", () => {
|
||||
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",
|
||||
},
|
||||
rpc: {
|
||||
ok: false,
|
||||
error: "gateway closed (1006 abnormal closure (no close frame))",
|
||||
url: "ws://127.0.0.1:18789",
|
||||
},
|
||||
extraServices: [],
|
||||
},
|
||||
{ json: false },
|
||||
);
|
||||
|
||||
expectMockLineContains(runtime.log, "Warm-up: launch agents can take a few seconds");
|
||||
const logged = runtime.log.mock.calls.map(([line]) => line).join("\n");
|
||||
expect(logged).not.toContain("Gateway process is");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,9 +264,30 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean; d
|
||||
}
|
||||
|
||||
if (rpc && !rpc.ok && service.loaded && service.runtime?.status === "running") {
|
||||
defaultRuntime.log(
|
||||
warnText("Warm-up: launch agents can take a few seconds. Try again shortly."),
|
||||
);
|
||||
// The RPC probe failed while the service is loaded and running. Only the case where
|
||||
// the gateway process is up and owns the listening port (health.healthy === true with
|
||||
// no stale gateway PIDs, deep status only) is an unambiguous "not warm-up" signal, so it
|
||||
// gets recovery guidance. `healthy` can also be set from bare reachability after
|
||||
// ownership failed (see restart-health.ts), which can coexist with a non-empty
|
||||
// staleGatewayPids; treat that combination as ambiguous rather than owns-port so it
|
||||
// doesn't contradict the dedicated stale-PID diagnostic below. Every other
|
||||
// health.healthy === false sub-case — a just-started gateway that has not bound the port
|
||||
// yet, a foreign process holding the port, or a stale gateway PID — is either a normal
|
||||
// warm-up window or is already covered by the dedicated stale-PID / port-not-listening /
|
||||
// port-conflict diagnostics below, so it keeps the warm-up hint (as does unknown health
|
||||
// from shallow status). A wedged gateway that owns the port is reported as healthy ===
|
||||
// true with no stale gateway PIDs, so it is steered by the first branch.
|
||||
if (status.health?.healthy === true && status.health.staleGatewayPids.length === 0) {
|
||||
defaultRuntime.log(
|
||||
warnText(
|
||||
"Gateway process is running and owns the gateway port, so this is not a warm-up delay. Check the probe credentials/config, or restart the gateway and inspect its logs if it stays unresponsive.",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
defaultRuntime.log(
|
||||
warnText("Warm-up: launch agents can take a few seconds. Try again shortly."),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (rpc) {
|
||||
const probeLabel = formatProbeKindLabel(rpc.kind);
|
||||
|
||||
Reference in New Issue
Block a user