diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 18522211bf..8cf0295750 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -596,6 +596,7 @@ import { createPolicySelectionPromptHelpers } from "./onboard/policy-selection-p import { printLowMemoryWarning, printMessagingProviderMissing, + printPortUnavailableError, printSwapCreationFailed, } from "./onboard/preflight-messages"; import { shouldSkipPreRecreateBackup } from "./onboard/sandbox-backup-on-recreate"; @@ -1649,7 +1650,7 @@ async function preflight( else if (outcome.kind !== "not-openshell") continue; } console.error(""); - console.error(` !! Port ${port} is not available.`); + printPortUnavailableError(port); console.error(` ${label} needs this port.`); console.error(""); if (portCheck.process && portCheck.process !== "unknown") { diff --git a/src/lib/onboard/gateway-reuse.test.ts b/src/lib/onboard/gateway-reuse.test.ts new file mode 100644 index 0000000000..aae0b86912 --- /dev/null +++ b/src/lib/onboard/gateway-reuse.test.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it, vi } from "vitest"; + +import { createGatewayReuseHelpers } from "./gateway-reuse"; + +describe("gateway reuse inspection", () => { + it("bounds every OpenShell metadata probe so a foreign listener cannot stall preflight (#6752)", () => { + const runCaptureOpenshell = vi.fn((_args: string[], _options?: Record) => ""); + const helpers = createGatewayReuseHelpers({ + gatewayName: "nemoclaw", + runCaptureOpenshell, + runOpenshell: vi.fn(() => ({ status: 0 })), + cliDisplayName: () => "NemoClaw", + }); + + helpers.getGatewayReuseSnapshot(); + + expect(runCaptureOpenshell).toHaveBeenCalledTimes(3); + for (const [, options] of runCaptureOpenshell.mock.calls) { + expect(options).toMatchObject({ ignoreError: true, timeout: 5_000 }); + } + }); +}); diff --git a/src/lib/onboard/gateway-reuse.ts b/src/lib/onboard/gateway-reuse.ts index 8dd41c8522..3c73115ad6 100644 --- a/src/lib/onboard/gateway-reuse.ts +++ b/src/lib/onboard/gateway-reuse.ts @@ -3,6 +3,8 @@ import { getGatewayReuseState, shouldSelectNamedGatewayForReuse } from "../state/gateway"; +const GATEWAY_INSPECTION_TIMEOUT_MS = 5_000; + export type GatewayReuseSnapshot = { gatewayStatus: string; gwInfo: string; @@ -28,11 +30,18 @@ export function createGatewayReuseHelpers(deps: GatewayReuseDeps): GatewayReuseH function getGatewayReuseSnapshot(): GatewayReuseSnapshot { const gatewayName = currentGatewayName(); - const gatewayStatus = deps.runCaptureOpenshell(["status"], { ignoreError: true }); + const gatewayStatus = deps.runCaptureOpenshell(["status"], { + ignoreError: true, + timeout: GATEWAY_INSPECTION_TIMEOUT_MS, + }); const gwInfo = deps.runCaptureOpenshell(["gateway", "info", "-g", gatewayName], { ignoreError: true, + timeout: GATEWAY_INSPECTION_TIMEOUT_MS, + }); + const activeGatewayInfo = deps.runCaptureOpenshell(["gateway", "info"], { + ignoreError: true, + timeout: GATEWAY_INSPECTION_TIMEOUT_MS, }); - const activeGatewayInfo = deps.runCaptureOpenshell(["gateway", "info"], { ignoreError: true }); return { gatewayStatus, gwInfo, diff --git a/src/lib/onboard/preflight-messages.test.ts b/src/lib/onboard/preflight-messages.test.ts index f416401cee..f9d50e0a81 100644 --- a/src/lib/onboard/preflight-messages.test.ts +++ b/src/lib/onboard/preflight-messages.test.ts @@ -7,6 +7,7 @@ import { printDockerNotReachableError, printLowMemoryWarning, printMessagingProviderMissing, + printPortUnavailableError, printSwapCreationFailed, printUnderProvisionedRuntimeWarning, printUnsupportedRuntimeError, @@ -65,6 +66,14 @@ describe("onboard preflight severity messages (#6004)", () => { }); }); + it("renders port conflicts as failures without ANSI on plain stderr (#6752)", () => { + withStderrColorDepth(1, () => { + const err = vi.spyOn(console, "error").mockImplementation(() => undefined); + printPortUnavailableError(8080); + expect(lines(err)).toEqual([" ✗ Port 8080 is not available."]); + }); + }); + it("prints the unsupported-runtime failure to stderr with a ✗ marker", () => { const err = vi.spyOn(console, "error").mockImplementation(() => undefined); printUnsupportedRuntimeError(); diff --git a/src/lib/onboard/preflight-messages.ts b/src/lib/onboard/preflight-messages.ts index a8dff7330b..16a052070b 100644 --- a/src/lib/onboard/preflight-messages.ts +++ b/src/lib/onboard/preflight-messages.ts @@ -20,6 +20,11 @@ export function printDockerNotReachableError(): void { console.error(failLine("Docker is not reachable. Please fix Docker and try again.")); } +/** A required onboarding port is already occupied. */ +export function printPortUnavailableError(port: number): void { + console.error(failLine(`Port ${port} is not available.`)); +} + /** Podman under the Linux Docker-driver path is unsupported. */ export function printUnsupportedRuntimeError(): void { console.error(failLine(`${cliDisplayName()} onboarding now uses OpenShell's Docker driver.`));