mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
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:
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user