diff --git a/scripts/install-cli.sh b/scripts/install-cli.sh index 2bbdef363d1a..494720d43e27 100755 --- a/scripts/install-cli.sh +++ b/scripts/install-cli.sh @@ -137,7 +137,11 @@ download_file() { detect_downloader fi if [[ "$DOWNLOADER" == "curl" ]]; then - curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url" + # Bound post-connect stalls without imposing a total download duration. + curl -fsSL --proto '=https' --tlsv1.2 \ + --speed-limit 1 --speed-time 30 \ + --retry 3 --retry-delay 1 --retry-connrefused \ + -o "$output" "$url" return fi wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url" diff --git a/scripts/install.sh b/scripts/install.sh index 43ba90b46d49..435d3445cf05 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -114,7 +114,11 @@ download_file() { detect_downloader fi if [[ "$DOWNLOADER" == "curl" ]]; then - curl -fsSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 1 --retry-connrefused -o "$output" "$url" + # Bound post-connect stalls without imposing a total download duration. + curl -fsSL --proto '=https' --tlsv1.2 \ + --speed-limit 1 --speed-time 30 \ + --retry 3 --retry-delay 1 --retry-connrefused \ + -o "$output" "$url" return fi wget -q --https-only --secure-protocol=TLSv1_2 --tries=3 --timeout=20 -O "$output" "$url" diff --git a/test/scripts/install-cli.test.ts b/test/scripts/install-cli.test.ts index af5ea93013d1..a84aab2782f1 100644 --- a/test/scripts/install-cli.test.ts +++ b/test/scripts/install-cli.test.ts @@ -41,6 +41,27 @@ function linkRequiredShellTools(bin: string) { describe("install-cli.sh", () => { const script = readFileSync(SCRIPT_PATH, "utf8"); + it("bounds stalled curl downloads and propagates timeout failures", () => { + const result = runInstallCliShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + curl() { + printf 'curl=%s\n' "$*" + return 28 + } + DOWNLOADER=curl + set +e + download_file "https://example.invalid/node.tar.gz" "/tmp/node.tar.gz" + printf 'status=%s\n' "$?" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("--speed-limit 1 --speed-time 30"); + expect(result.stdout).not.toContain("--connect-timeout"); + expect(result.stdout).toContain("--retry 3 --retry-delay 1 --retry-connrefused"); + expect(result.stdout).toContain("status=28"); + }); + it("does not clean an unrelated legacy checkout during the default npm install", () => { const main = script.slice(script.indexOf("\nmain() {")); expect(main).not.toContain("cleanup_legacy_submodules"); diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index 316f46bf90f6..74273564788f 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -82,6 +82,27 @@ describe("install.sh", () => { } }); + it("bounds stalled curl downloads and propagates timeout failures", () => { + const result = runInstallShell(` + set -euo pipefail + source "${SCRIPT_PATH}" + curl() { + printf 'curl=%s\n' "$*" + return 28 + } + DOWNLOADER=curl + set +e + download_file "https://example.invalid/archive.tgz" "/tmp/archive.tgz" + printf 'status=%s\n' "$?" + `); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("--speed-limit 1 --speed-time 30"); + expect(result.stdout).not.toContain("--connect-timeout"); + expect(result.stdout).toContain("--retry 3 --retry-delay 1 --retry-connrefused"); + expect(result.stdout).toContain("status=28"); + }); + it("runs apt-get through noninteractive wrappers", () => { expect(script).toContain("apt_get()"); expect(script).toContain('DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"');