mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
fix(ssh): say when this computer can't download Node for a managed server, and where an offline copy goes (#30911)
This commit is contained in:
@@ -151,3 +151,74 @@ describe('pinned runtime archive request ownership', () => {
|
||||
expect(await readdir(join(cacheRoot, 'node', 'archives'))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// The desktop downloads Node and uploads it, so an offline SSH host is fine but an offline desktop
|
||||
// used to see only Chromium's bare `net::ERR_CONNECTION_REFUSED` under "couldn't start its server".
|
||||
describe('a pinned runtime archive this computer cannot download', () => {
|
||||
it('says this computer could not reach the download, and where an offline copy goes', async () => {
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
||||
try {
|
||||
const cacheRoot = join(root, 'cache')
|
||||
const fetcher = vi.fn<typeof fetch>(async () => {
|
||||
throw new Error('net::ERR_CONNECTION_REFUSED')
|
||||
})
|
||||
let settled = false
|
||||
const failure = materializeNodeRuntimeArchive(TARGET, cacheRoot, { fetcher }).catch(
|
||||
(error: unknown) => error
|
||||
)
|
||||
void failure.finally(() => (settled = true))
|
||||
// Real file I/O runs between the retry pauses, so yield to it before skipping each pause.
|
||||
for (let turn = 0; !settled && turn < 1_000; turn += 1) {
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
vi.advanceTimersByTime(10_000)
|
||||
}
|
||||
const error = await failure
|
||||
expect(fetcher).toHaveBeenCalledTimes(4)
|
||||
const offlinePath = join(cacheRoot, 'node', 'archives', originalAsset.archive)
|
||||
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
const message = error instanceof Error ? error.message : ''
|
||||
expect(message).toContain("This computer couldn't download Node from https://")
|
||||
expect(message).toContain(`/v${NODE_RUNTIME_PIN.version}/${originalAsset.archive}`)
|
||||
expect(message).toContain('(net::ERR_CONNECTION_REFUSED)')
|
||||
expect(message).toContain("the host doesn't need internet access")
|
||||
expect(message).toContain(`copy it to ${offlinePath}`)
|
||||
expect(error instanceof Error ? error.cause : null).toMatchObject({
|
||||
message: 'net::ERR_CONNECTION_REFUSED'
|
||||
})
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'uses a copy placed at that path without downloading',
|
||||
async () => {
|
||||
const archive = await nodeDistArchiveFixture(new TextEncoder().encode('node'))
|
||||
Object.assign(NODE_RUNTIME_ASSETS[TARGET], { archiveSha256: sha256(archive) })
|
||||
const cacheRoot = join(root, 'cache')
|
||||
const offlinePath = join(cacheRoot, 'node', 'archives', originalAsset.archive)
|
||||
await mkdir(join(offlinePath, '..'), { recursive: true })
|
||||
await writeFile(offlinePath, archive)
|
||||
const fetcher = vi.fn<typeof fetch>(async () => {
|
||||
throw new Error('net::ERR_CONNECTION_REFUSED')
|
||||
})
|
||||
|
||||
expect(await materializeNodeRuntimeArchive(TARGET, cacheRoot, { fetcher })).toBe(offlinePath)
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps a local filesystem failure as it is', async () => {
|
||||
const cacheRoot = join(root, 'cache')
|
||||
const archiveRoot = join(cacheRoot, 'node', 'archives')
|
||||
const fetcher = vi.fn<typeof fetch>(async () => {
|
||||
const stage = (await readdir(archiveRoot)).find((name) => name.startsWith('.download-'))
|
||||
await mkdir(join(archiveRoot, stage ?? 'missing', originalAsset.archive))
|
||||
return new Response(new ReadableStream(), { status: 200 })
|
||||
})
|
||||
await expect(
|
||||
materializeNodeRuntimeArchive(TARGET, cacheRoot, { fetcher })
|
||||
).rejects.toMatchObject({ code: 'EEXIST' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,11 @@ import { createReadStream } from 'node:fs'
|
||||
import { chmod, link, mkdir, rm } from 'node:fs/promises'
|
||||
import { basename, join } from 'node:path'
|
||||
import { getMainHttpClient, type MainHttpClient } from '../network/http-client'
|
||||
import { downloadVerifiedArchive, extractRuntimeArchive } from './runtime-archive-download'
|
||||
import {
|
||||
downloadVerifiedArchive,
|
||||
extractRuntimeArchive,
|
||||
ReleaseMismatchError
|
||||
} from './runtime-archive-download'
|
||||
import { findOrcadCachePath } from './orcad-cache-path'
|
||||
import { orcadNodeRuntimeExecutable } from '../../shared/orcad-artifacts'
|
||||
import {
|
||||
@@ -66,7 +70,9 @@ export async function materializeVerifiedRuntimeArchive(
|
||||
downloaded,
|
||||
options.fetcher ?? getMainHttpClient().fetch,
|
||||
options.signal
|
||||
)
|
||||
).catch((error: unknown) => {
|
||||
throw unreachableArchiveError(archive, cached.path, error, options.signal)
|
||||
})
|
||||
options.signal?.throwIfAborted()
|
||||
await publishVerified(
|
||||
downloaded,
|
||||
@@ -80,6 +86,32 @@ export async function materializeVerifiedRuntimeArchive(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Why: the bare network error (e.g. net::ERR_CONNECTION_REFUSED) reads as the host failing, but
|
||||
* only this computer downloads the archive. Its cache path is the offline way in, since a file
|
||||
* placed there with the pinned hash is used without any download.
|
||||
*/
|
||||
function unreachableArchiveError(
|
||||
archive: PinnedRuntimeArchive,
|
||||
cachePath: string,
|
||||
error: unknown,
|
||||
signal: AbortSignal | undefined
|
||||
): unknown {
|
||||
const localFailure = error instanceof Error && 'code' in error && typeof error.code === 'string'
|
||||
if (signal?.aborted || error instanceof ReleaseMismatchError || localFailure) {
|
||||
return error
|
||||
}
|
||||
const cause = error instanceof Error ? error.message : String(error)
|
||||
return new Error(
|
||||
`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 ` +
|
||||
`and copy it to ${cachePath}`,
|
||||
{ cause: error }
|
||||
)
|
||||
}
|
||||
|
||||
export async function materializeCachedRuntimeExecutable(
|
||||
runtime: PinnedRuntimeExecutable,
|
||||
options: PinnedRuntimeMaterializeOptions
|
||||
|
||||
Reference in New Issue
Block a user