mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(daemon): skip gateway port-release assertion when stopping a node-host LaunchAgent
The generic LaunchAgent stop path (assertGatewayPortReleasedAfterStop) resolves a gateway-relative port from the stopped service's own program arguments/env and then asserts that port is no longer bound after stop. `resolveNodeService()` reuses this exact same stop path for the node-host service, but a node-host's "--port" argument is the Gateway port it connects to *outward as a client* -- it never binds that port itself. On a host where the node-host and Gateway are co-located (a common, documented topology), this made `openclaw node stop`/`restart` fail with a false-positive "gateway port NNNN is still busy after LaunchAgent stop" error, because the assertion was inadvertently checking whether the co-located Gateway's own (legitimately still open) port had been released -- something the node-host stop has no business asserting. Skip the port-release assertion entirely when OPENCLAW_SERVICE_KIND indicates the node-host service, using the existing service-kind env marker already used elsewhere in the daemon lifecycle code for this exact gateway-vs-node distinction. Fixes openclaw/openclaw#124296 (secondary bug: node stop/restart false-positive port-collision guard when co-located with Gateway).
This commit is contained in:
@@ -4,6 +4,7 @@ import { inspectPortUsage } from "../infra/ports-inspect.js";
|
|||||||
import { probePortUsage } from "../infra/ports-probe.js";
|
import { probePortUsage } from "../infra/ports-probe.js";
|
||||||
import { cleanStaleGatewayProcessesSync } from "../infra/restart-stale-pids.js";
|
import { cleanStaleGatewayProcessesSync } from "../infra/restart-stale-pids.js";
|
||||||
import { sleep } from "../utils.js";
|
import { sleep } from "../utils.js";
|
||||||
|
import { NODE_SERVICE_KIND } from "./constants.js";
|
||||||
import { isCurrentProcessLaunchdServiceLabel } from "./launchd-current-service.js";
|
import { isCurrentProcessLaunchdServiceLabel } from "./launchd-current-service.js";
|
||||||
import {
|
import {
|
||||||
execLaunchctl,
|
execLaunchctl,
|
||||||
@@ -55,6 +56,15 @@ async function waitForGatewayPortRelease(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function assertGatewayPortReleasedAfterStop(env: GatewayServiceEnv): Promise<void> {
|
async function assertGatewayPortReleasedAfterStop(env: GatewayServiceEnv): Promise<void> {
|
||||||
|
// Node-host services only ever connect outward to a gateway as a client;
|
||||||
|
// they never bind the gateway's own port. Reusing the generic LaunchAgent
|
||||||
|
// stop path for `openclaw node stop`/`restart` on a host that is also
|
||||||
|
// running the Gateway would otherwise make this assertion see the
|
||||||
|
// co-located Gateway's own (legitimately still-open) port and report a
|
||||||
|
// false-positive "still busy" failure. See openclaw/openclaw#124296.
|
||||||
|
if (env.OPENCLAW_SERVICE_KIND?.trim() === NODE_SERVICE_KIND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { port, probeHosts } = await resolveLaunchAgentGatewayContext(env);
|
const { port, probeHosts } = await resolveLaunchAgentGatewayContext(env);
|
||||||
if (port === null) {
|
if (port === null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2732,6 +2732,53 @@ describe("launchd install", () => {
|
|||||||
expect(output).not.toContain("Stopped LaunchAgent");
|
expect(output).not.toContain("Stopped LaunchAgent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat a co-located Gateway's own port as busy when stopping a node-host LaunchAgent", async () => {
|
||||||
|
// Regression test for https://github.com/openclaw/openclaw/issues/124296:
|
||||||
|
// `openclaw node stop`/`restart` must not fail with a false-positive
|
||||||
|
// port-collision guard when the node-host and Gateway are co-located on
|
||||||
|
// the same host. The node-host only ever connects outward to the
|
||||||
|
// gateway port as a client and never binds it, so the generic
|
||||||
|
// LaunchAgent stop path must skip the port-release assertion entirely
|
||||||
|
// when stopping a service tagged with the node service kind.
|
||||||
|
const env = {
|
||||||
|
...createDefaultLaunchdEnv(),
|
||||||
|
OPENCLAW_SERVICE_KIND: "node",
|
||||||
|
OPENCLAW_GATEWAY_PORT: "18789",
|
||||||
|
};
|
||||||
|
const stdout = new PassThrough();
|
||||||
|
let output = "";
|
||||||
|
stdout.on("data", (chunk: Buffer) => {
|
||||||
|
output += chunk.toString();
|
||||||
|
});
|
||||||
|
// Simulate the Gateway's own port still being (legitimately) busy on the
|
||||||
|
// same host; this must not fail the node-host stop.
|
||||||
|
inspectPortUsage.mockResolvedValue({
|
||||||
|
port: 18789,
|
||||||
|
status: "busy",
|
||||||
|
listeners: [],
|
||||||
|
hints: [],
|
||||||
|
});
|
||||||
|
probePortUsage.mockResolvedValue("busy");
|
||||||
|
|
||||||
|
await withProcessEnv(
|
||||||
|
{
|
||||||
|
LAUNCH_JOB_LABEL: undefined,
|
||||||
|
LAUNCH_JOB_NAME: undefined,
|
||||||
|
XPC_SERVICE_NAME: undefined,
|
||||||
|
OPENCLAW_SERVICE_MARKER: undefined,
|
||||||
|
OPENCLAW_SERVICE_KIND: undefined,
|
||||||
|
OPENCLAW_LAUNCHD_LABEL: undefined,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await stopLaunchAgent({ env, stdout });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(inspectPortUsage).not.toHaveBeenCalled();
|
||||||
|
expect(cleanStaleGatewayProcessesSync).not.toHaveBeenCalled();
|
||||||
|
expect(output).toContain("Stopped LaunchAgent");
|
||||||
|
});
|
||||||
|
|
||||||
it("stops LaunchAgent with disable+stop when --disable is passed", async () => {
|
it("stops LaunchAgent with disable+stop when --disable is passed", async () => {
|
||||||
const env = createDefaultLaunchdEnv();
|
const env = createDefaultLaunchdEnv();
|
||||||
const stdout = new PassThrough();
|
const stdout = new PassThrough();
|
||||||
|
|||||||
Reference in New Issue
Block a user