feat(ssh): let a nodejs.org mirror serve the pinned Node on this computer and the host (#30981)

* feat(ssh): let a nodejs.org mirror serve the pinned Node on both machines

* fix(ssh): share one deadline across every host download URL

* fix(ssh): give each host download URL its share of the deadline and cap its size

* refactor(ssh): one tool per host, labelled sources and a portable wget watchdog

* fix(ssh): quote this computer's short download reason in the combined error

* fix(ssh): fetch only validated mirror URLs on the host

* fix(ssh): drop an empty query or fragment from the mirror URL

* fix(ssh): keep a cancel or a local error as it is after the mirror fails
This commit is contained in:
Neil
2026-10-19 20:52:44 -07:00
committed by GitHub
parent 0ee0290cc3
commit 66d37178da
18 changed files with 728 additions and 112 deletions
@@ -0,0 +1,15 @@
/** This computer's Node mirror: the Settings value, else the conventional environment variables. */
import { resolveNodeDistMirror } from '../../shared/node-dist-mirror'
let readMirrorSetting: (() => string | undefined) | null = null
/** Registered once the settings store exists; read on every download, so edits apply at once. */
export function setNodeDistMirrorSettingReader(read: (() => string | undefined) | null): void {
readMirrorSetting = read
}
export function configuredNodeDistMirror(
env: Record<string, string | undefined> = process.env
): string | null {
return resolveNodeDistMirror(readMirrorSetting?.(), env)
}
@@ -0,0 +1,117 @@
import { createHash } from 'node:crypto'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { NODE_RUNTIME_ASSETS, NODE_RUNTIME_PIN } from '../../shared/node-runtime-pin'
import {
DesktopDownloadUnreachableError,
materializeNodeRuntimeArchive
} from './pinned-runtime-materializer'
import { ReleaseMismatchError } from './runtime-archive-download'
import { setNodeDistMirrorSettingReader } from './node-dist-mirror-setting'
const TARGET = 'linux-x64-glibc'
const MIRROR = 'https://artifacts.example.internal/nodejs/dist'
const BODY = new TextEncoder().encode('pinned archive')
const originalAsset = { ...NODE_RUNTIME_ASSETS[TARGET] }
const mirrorUrl = `${MIRROR}/v${NODE_RUNTIME_PIN.version}/${originalAsset.archive}`
let root = ''
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-node-mirror-'))
Object.assign(NODE_RUNTIME_ASSETS[TARGET], {
archiveSha256: createHash('sha256').update(BODY).digest('hex')
})
setNodeDistMirrorSettingReader(() => `${MIRROR}/`)
})
afterEach(async () => {
setNodeDistMirrorSettingReader(null)
Object.assign(NODE_RUNTIME_ASSETS[TARGET], originalAsset)
await rm(root, { recursive: true, force: true })
})
function fetcher(serves: (url: string) => boolean) {
return vi.fn<typeof fetch>(async (url) =>
serves(String(url))
? new Response(Buffer.from(BODY), { status: 200 })
: new Response('not here', { status: 404 })
)
}
describe('the pinned Node download with a mirror configured', () => {
it('downloads from the mirror and never asks nodejs.org', async () => {
const fetch = fetcher((url) => url === mirrorUrl)
await materializeNodeRuntimeArchive(TARGET, root, { fetcher: fetch })
expect(fetch.mock.calls.map(([url]) => String(url))).toEqual([mirrorUrl])
})
it('falls back to the release URL when the mirror lacks the file', async () => {
const fetch = fetcher((url) => url.startsWith('https://nodejs.org/'))
await materializeNodeRuntimeArchive(TARGET, root, { fetcher: fetch })
const urls = fetch.mock.calls.map(([url]) => String(url))
expect(urls[0]).toBe(mirrorUrl)
expect(urls.at(-1)).toMatch(/^https:\/\/nodejs\.org\/dist\//)
})
it('tries an unreachable mirror once, without its retry pauses, before nodejs.org', async () => {
const fetch = vi.fn<typeof fetch>(async (url) => {
if (String(url) === mirrorUrl) {
throw new Error('net::ERR_CONNECTION_TIMED_OUT')
}
return new Response(Buffer.from(BODY), { status: 200 })
})
await materializeNodeRuntimeArchive(TARGET, root, { fetcher: fetch })
expect(fetch.mock.calls.map(([url]) => String(url) === mirrorUrl)).toEqual([true, false])
})
it('rejects a mirror file that fails the pin, whatever the mirror says', async () => {
const fetch = vi.fn<typeof fetch>(
async () => new Response('<html>blocked</html>', { status: 200 })
)
await expect(
materializeNodeRuntimeArchive(TARGET, root, { fetcher: fetch })
).rejects.toBeInstanceOf(ReleaseMismatchError)
})
it('names both addresses when neither is reachable', async () => {
const fetch = vi.fn<typeof fetch>(async () => {
throw new Error('net::ERR_CONNECTION_REFUSED')
})
const error = await materializeNodeRuntimeArchive(TARGET, root, {
fetcher: fetch,
retryDelaysMs: []
}).catch((failure: unknown) => failure)
expect(error).toBeInstanceOf(DesktopDownloadUnreachableError)
expect(String(error)).toMatch(/the mirror .* failed too/)
})
it('keeps a cancel during the release attempt a cancel', async () => {
const controller = new AbortController()
const fetch = vi.fn<typeof fetch>(async (url) => {
if (String(url) !== mirrorUrl) {
controller.abort()
}
throw new Error('net::ERR_CONNECTION_REFUSED')
})
const error = await materializeNodeRuntimeArchive(TARGET, root, {
fetcher: fetch,
signal: controller.signal,
retryDelaysMs: []
}).catch((failure: unknown) => failure)
expect(error).not.toBeInstanceOf(DesktopDownloadUnreachableError)
expect(controller.signal.aborted).toBe(true)
})
})
@@ -11,7 +11,10 @@ import {
} from './runtime-archive-download'
import { findOrcadCachePath } from './orcad-cache-path'
import { orcadNodeRuntimeExecutable } from '../../shared/orcad-artifacts'
import { nodeDistMirrorUrl } from '../../shared/node-dist-mirror'
import { configuredNodeDistMirror } from './node-dist-mirror-setting'
import {
NODE_RUNTIME_PIN,
pinnedNodeRuntimeAsset,
nodeRuntimeExecutablePath,
nodeRuntimeReleaseUrl,
@@ -21,6 +24,7 @@ import {
export type PinnedRuntimeMaterializeOptions = {
fetcher?: MainHttpClient['fetch']
signal?: AbortSignal
/** Pauses between download attempts; empty makes a single attempt. */
retryDelaysMs?: readonly number[]
}
@@ -104,16 +108,30 @@ function unreachableArchiveError(
return error
}
const cause = error instanceof Error ? error.message : String(error)
return new Error(
return new DesktopDownloadUnreachableError(
`This computer couldn't download ${archive.label} from ${archive.url} (${cause}). ` +
`Orca uploads ${archive.label} to the host from this computer, so the host doesn't need ` +
`internet access, but this computer must reach that address once. Check this computer's ` +
`network connection or proxy and connect again, or download that file on another machine ` +
`network connection or proxy and connect again, set a mirror in Settings > Advanced > ` +
`Network > Node.js download mirror, or download that file on another machine ` +
`and copy it to ${cachePath}`,
{ cause: error }
`${archive.url}: ${cause}`,
error
)
}
/** This computer's download failed on the network; `reason` is the short form for combined errors. */
export class DesktopDownloadUnreachableError extends Error {
constructor(
message: string,
readonly reason: string,
cause: unknown
) {
super(message, { cause })
this.name = 'DesktopDownloadUnreachableError'
}
}
export async function materializeCachedRuntimeExecutable(
runtime: PinnedRuntimeExecutable,
options: PinnedRuntimeMaterializeOptions
@@ -199,16 +217,61 @@ function nodeRuntimeArchive(target: NodeRuntimeTarget): PinnedRuntimeArchive {
}
}
/** Tries the configured mirror before the release URL; the pinned hash decides what is kept. */
async function withNodeDistMirror<T>(
target: NodeRuntimeTarget,
options: PinnedRuntimeMaterializeOptions,
run: (archive: PinnedRuntimeArchive, options: PinnedRuntimeMaterializeOptions) => Promise<T>
): Promise<T> {
const archive = nodeRuntimeArchive(target)
const asset = pinnedNodeRuntimeAsset(target)
// Mirrors carry the official tree only; unofficial builds live at one address.
const mirror = asset.source === 'official' ? configuredNodeDistMirror() : null
if (!mirror) {
return run(archive, options)
}
const mirrorUrl = nodeDistMirrorUrl(mirror, NODE_RUNTIME_PIN.version, asset.archive)
try {
// One attempt: an unreachable mirror must not hold up the release URL behind its retries.
return await run({ ...archive, url: mirrorUrl }, { ...options, retryDelaysMs: [] })
} catch (mirrorError) {
if (options.signal?.aborted) {
throw mirrorError
}
return run(archive, options).catch((error: unknown) => {
// Only two network failures combine; a cancel or a local error keeps its own identity.
if (options.signal?.aborted || !(error instanceof DesktopDownloadUnreachableError)) {
throw error
}
const mirrorReason =
mirrorError instanceof DesktopDownloadUnreachableError
? mirrorError.reason
: `${mirrorUrl}: ${errorText(mirrorError)}`
throw new DesktopDownloadUnreachableError(
`${errorText(error)} (the mirror ${mirrorReason} failed too)`,
`${mirrorReason}; ${errorReason(error)}`,
error
)
})
}
}
function errorText(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
function errorReason(error: unknown): string {
return error instanceof DesktopDownloadUnreachableError ? error.reason : errorText(error)
}
/** The pinned official archive, uploaded to hosts as published (design D5). */
export function materializeNodeRuntimeArchive(
target: NodeRuntimeTarget,
cacheRoot: string,
options: PinnedRuntimeMaterializeOptions
): Promise<string> {
return materializeVerifiedRuntimeArchive(
nodeRuntimeArchive(target),
nodeRuntimeArchiveDir(cacheRoot),
options
return withNodeDistMirror(target, options, (archive, download) =>
materializeVerifiedRuntimeArchive(archive, nodeRuntimeArchiveDir(cacheRoot), download)
)
}
@@ -232,21 +295,23 @@ export function materializeCachedNodeRuntime(
): Promise<string> {
const asset = pinnedNodeRuntimeAsset(target)
const executableName = basename(orcadNodeRuntimeExecutable(target))
return materializeCachedRuntimeExecutable(
{
...nodeRuntimeArchive(target),
executableSha256: asset.executableSha256,
member: nodeRuntimeExecutablePath(target, asset.archive),
isWindows: target.startsWith('win32-'),
cachePath: (attempt) =>
join(
cacheRoot,
'node',
asset.executableSha256,
`${attempt ? `repair-${attempt}-` : ''}${executableName}`
)
},
options
return withNodeDistMirror(target, options, (archive, download) =>
materializeCachedRuntimeExecutable(
{
...archive,
executableSha256: asset.executableSha256,
member: nodeRuntimeExecutablePath(target, asset.archive),
isWindows: target.startsWith('win32-'),
cachePath: (attempt) =>
join(
cacheRoot,
'node',
asset.executableSha256,
`${attempt ? `repair-${attempt}-` : ''}${executableName}`
)
},
download
)
)
}