mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -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 { cleanStaleGatewayProcessesSync } from "../infra/restart-stale-pids.js";
|
||||
import { sleep } from "../utils.js";
|
||||
import { NODE_SERVICE_KIND } from "./constants.js";
|
||||
import { isCurrentProcessLaunchdServiceLabel } from "./launchd-current-service.js";
|
||||
import {
|
||||
execLaunchctl,
|
||||
@@ -55,6 +56,15 @@ async function waitForGatewayPortRelease(
|
||||
}
|
||||
|
||||
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);
|
||||
if (port === null) {
|
||||
return;
|
||||
|
||||
@@ -2732,6 +2732,53 @@ describe("launchd install", () => {
|
||||
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 () => {
|
||||
const env = createDefaultLaunchdEnv();
|
||||
const stdout = new PassThrough();
|
||||
|
||||
Reference in New Issue
Block a user