fix(orcad): deploy the glibc 2.17 runtime on old-glibc hosts, and fall back to the pinned relay (#25180)

* fix(orcad): deploy the glibc 2.17 runtime on old-glibc hosts, and fall back to the pinned relay

Managed orcad picked its runtime from the host's libc flavour alone, so a CentOS 7 host
(glibc 2.17) got the default linux-x64-glibc Node, whose self-test fails there. The deploy
now picks the runtime by glibc the same way the relay ladder picks rung A or B, and the
host-side slot checks accept the compat target it ships.

When orcad still can't run, the host is recorded as such and the relay connect that
follows now runs the pinned-runtime ladder instead of defaulting to the host-Node relay,
so a host with no Node lands on rung B rather than failing.

The hostile-host lane deploys managed orcad on a fresh CentOS 7 host and asserts it runs on
the compat runtime with no default runtime uploaded.

* test(ssh): CentOS 7 cell asserts the compat runtime pick, then the refusal and relay rung B fallback

The compat template still ships the base @parcel/watcher binary, which needs GLIBCXX_3.4.20; CentOS 7's
libstdc++ stops at 3.4.19, so the candidate's preflight refuses. The cell now pins that end-to-end
behaviour: compat runtime picked and uploaded alone, refusal classified native_preflight, and the relay
that follows settles on rung B with no runtime setting.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-04 05:17:10 -07:00
committed by GitHub
co-authored by m4air
parent 9a885a86ca
commit da325ea7e3
25 changed files with 394 additions and 62 deletions
@@ -7,7 +7,7 @@
*/
import { readdir, rm, stat } from 'node:fs/promises'
import { join } from 'node:path'
import { NODE_RUNTIME_ASSETS } from '../../shared/node-runtime-pin'
import { nodeRuntimeAsset } from '../../shared/node-runtime-pin'
import { ORCAD_LOCK_FILE_NAME, readOrcadInstanceLockRecord } from './orcad-instance-lock'
import { materializedOrcadArtifactVersions } from '../ssh/orcad-artifact-materializer'
import { isProcessAlive } from '../daemon/daemon-process-inspection'
@@ -33,7 +33,7 @@ export async function pruneOrcadArtifactCache(
const removed: string[] = []
for (const target of await listNames(cacheRoot)) {
// Only target directories hold slots; `node/` is the runtime archive cache.
if (!Object.hasOwn(NODE_RUNTIME_ASSETS, target)) {
if (!nodeRuntimeAsset(target)) {
continue
}
const targetRoot = join(cacheRoot, target)
+2 -2
View File
@@ -8,13 +8,13 @@ import {
ORCAD_VERSION,
orcadArtifactFilenames
} from '../../shared/orcad-artifacts'
import { SERVER_TARGETS } from '../../shared/node-runtime-pin'
import { COMPAT_SERVER_TARGETS, SERVER_TARGETS } from '../../shared/node-runtime-pin'
import { orcadAgentBrowserNativeName } from '../../shared/orcad-agent-browser-name'
/** Hash installed bytes in the build's order; a version marker is not proof of delivery. */
export async function readOrcadArtifactIdentity(directory: string): Promise<string> {
const target = z
.enum(SERVER_TARGETS)
.enum([...SERVER_TARGETS, ...COMPAT_SERVER_TARGETS])
.parse((await readFile(join(directory, ORCAD_SERVER_TARGET_FILENAME), 'utf8')).trim())
const platform = target.startsWith('win32-')
? 'win32'
+25 -1
View File
@@ -2,7 +2,11 @@ import { EventEmitter } from 'node:events'
import { join, resolve } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { handoffToBundledOrcad, resolveBundledOrcadSlot } from './orcad-bundled-runtime'
import { NODE_RUNTIME_ASSETS, NODE_RUNTIME_PIN } from '../../shared/node-runtime-pin'
import {
NODE_RUNTIME_ASSETS,
NODE_RUNTIME_COMPAT_ASSETS,
NODE_RUNTIME_PIN
} from '../../shared/node-runtime-pin'
import {
ORCAD_NODE_RUNTIME_MARKER_FILENAME,
ORCAD_SERVER_TARGET_FILENAME,
@@ -84,6 +88,26 @@ describe('bundled Orca runtime handoff', () => {
expect(fixture.spawn).not.toHaveBeenCalled()
})
it('hands a glibc 2.17 compat slot to the compat runtime it names', () => {
const compatSha = NODE_RUNTIME_COMPAT_ASSETS['linux-x64-glibc217'].executableSha256
fixture.read.mockImplementation((path) =>
path.endsWith(ORCAD_SERVER_TARGET_FILENAME) ? 'linux-x64-glibc217\n' : `${compatSha}\n`
)
expect(handoffToBundledOrcad()).toBe(true)
expect(fixture.spawn).toHaveBeenCalledWith(
expect.objectContaining({
program: join('/slot', '..', 'runtimes', `node-${compatSha}`, 'bin', 'node')
})
)
})
it('refuses a compat slot that names the default runtime', () => {
fixture.read.mockImplementation((path) =>
path.endsWith(ORCAD_SERVER_TARGET_FILENAME) ? 'linux-x64-glibc217\n' : `${SHA}\n`
)
expect(() => handoffToBundledOrcad()).toThrow(`does not name Node ${NODE_RUNTIME_PIN.version}`)
})
it('refuses a slot without its runtime reference', () => {
fixture.exists.mockImplementation((path) => !path.endsWith(ORCAD_NODE_RUNTIME_MARKER_FILENAME))
expect(() => handoffToBundledOrcad()).toThrow('bundled Orca runtime reference is missing')
+5 -12
View File
@@ -9,20 +9,11 @@ import {
ORCAD_VERSION_FILENAME,
orcadNodeRuntimeRelativePath
} from '../../shared/orcad-artifacts'
import {
NODE_RUNTIME_ASSETS,
NODE_RUNTIME_PIN,
SERVER_TARGETS,
type ServerTarget
} from '../../shared/node-runtime-pin'
import { NODE_RUNTIME_PIN, nodeRuntimeAsset } from '../../shared/node-runtime-pin'
export class OrcadBundledRuntimeError extends Error {}
export const ORCAD_BUNDLED_LAUNCHER_ENV = 'ORCA_BUNDLED_LAUNCHER_CHANNEL'
function isServerTarget(value: string): value is ServerTarget {
return SERVER_TARGETS.some((target) => target === value)
}
/**
* The pinned Node a packaged slot in `directory` runs on, or null for an unpackaged entry.
* Throws when the slot is packaged but its runtime reference is torn or foreign.
@@ -42,13 +33,15 @@ export function resolveBundledOrcadRuntime(directory: string): string | null {
throw new OrcadBundledRuntimeError('The bundled Orca runtime reference is missing')
}
const target = readFileSync(targetPath, 'utf8').trim()
if (!isServerTarget(target)) {
// A compat target (design D6 rung B) runs the same Node version built for an older glibc.
const asset = nodeRuntimeAsset(target)
if (!asset) {
throw new OrcadBundledRuntimeError(`The bundled Orca runtime target is invalid: ${target}`)
}
const executableSha256 = readFileSync(markerPath, 'utf8').trim()
// Why the pin and not only a digest shape: the marker becomes a path segment, and a slot
// naming another runtime was not built by this code.
if (executableSha256 !== NODE_RUNTIME_ASSETS[target].executableSha256) {
if (executableSha256 !== asset.executableSha256) {
throw new OrcadBundledRuntimeError(
`The bundled Orca runtime reference does not name Node ${NODE_RUNTIME_PIN.version}`
)
@@ -188,6 +188,10 @@ describe('assembleOrcadArtifact', () => {
expect(
readFileSync(join(artifactDir, 'node_modules/node-pty/build/Release/pty.node'), 'utf8')
).toBe(`${target}:node_modules/node-pty/build/Release/pty.node`)
// The host-side preflight hashes it the same way, so managed orcad can run it.
expect(await readOrcadArtifactIdentity(artifactDir)).toBe(
readFileSync(join(artifactDir, ORCAD_VERSION_FILENAME), 'utf8').trim()
)
})
it('refuses a compat slot that names the default runtime', async () => {
+6 -1
View File
@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
parseGlibcVersion,
parseOrcadLinuxLibc,
resolveOrcadDeploymentTarget,
resolveOrcadDeploymentTargetFacts
} from './orcad-deployment-target'
import { SshConnection } from './ssh-connection'
@@ -11,6 +10,12 @@ import { execCommand } from './ssh-relay-deploy-helpers'
import { getRemoteHostPlatform } from './ssh-remote-platform'
vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() }))
async function resolveOrcadDeploymentTarget(
options: Parameters<typeof resolveOrcadDeploymentTargetFacts>[0]
): Promise<string> {
return (await resolveOrcadDeploymentTargetFacts(options)).target
}
beforeEach(() => vi.mocked(execCommand).mockReset())
describe('deployment C library selection', () => {
+9 -15
View File
@@ -1,6 +1,10 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { SERVER_TARGETS, type ServerTarget } from '../../shared/node-runtime-pin'
import {
isNodeRuntimeTarget,
type NodeRuntimeTarget,
type ServerTarget
} from '../../shared/node-runtime-pin'
import { ORCAD_SERVER_TARGET_FILENAME } from '../../shared/orcad-artifacts'
import type { SshConnection } from './ssh-connection'
import { execCommand } from './ssh-relay-deploy-helpers'
@@ -39,15 +43,6 @@ export function parseGlibcVersion(output: string): GlibcVersion | null {
return match ? { major: Number(match[1]), minor: Number(match[2]) } : null
}
export async function resolveOrcadDeploymentTarget(options: {
conn: SshConnection
host: RemoteHostPlatform
signal?: AbortSignal
exec?: (command: string) => Promise<string>
}): Promise<ServerTarget> {
return (await resolveOrcadDeploymentTargetFacts(options)).target
}
export async function resolveOrcadDeploymentTargetFacts(options: {
conn: SshConnection
host: RemoteHostPlatform
@@ -81,12 +76,11 @@ function linuxTargetFacts(host: RemoteHostPlatform, output: string): OrcadDeploy
}
}
/** The server target an assembled bundle was built for. */
export function readOrcadBundleTarget(localOrcadDir: string): ServerTarget {
/** The runtime target an assembled bundle was built for, a compat one included. */
export function readOrcadBundleTarget(localOrcadDir: string): NodeRuntimeTarget {
const recorded = readFileSync(join(localOrcadDir, ORCAD_SERVER_TARGET_FILENAME), 'utf8').trim()
const target = SERVER_TARGETS.find((candidate) => candidate === recorded)
if (!target) {
if (!isNodeRuntimeTarget(recorded)) {
throw new Error(`The orcad bundle names no known server target: ${recorded}`)
}
return target
return recorded
}
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ServeReadiness } from '../server/serve-readiness'
import { NODE_RUNTIME_ASSETS, NODE_RUNTIME_COMPAT_ASSETS } from '../../shared/node-runtime-pin'
const { gcMock, readRecordMock } = vi.hoisted(() => ({
gcMock: vi.fn(),
@@ -59,7 +60,11 @@ describe('managed orcad version GC after a deploy', () => {
expect.objectContaining({
liveDaemonVersion: '1.0.0+a',
record: { active: '2.0.0+b' },
nodeRuntimePins: [expect.stringMatching(/^[0-9a-f]{64}$/u)]
// The compat runtime stays pinned: a compat orcad and a rung A relay share the store.
nodeRuntimePins: [
NODE_RUNTIME_ASSETS['linux-x64-glibc'].executableSha256,
NODE_RUNTIME_COMPAT_ASSETS['linux-x64-glibc217'].executableSha256
]
})
)
+5 -3
View File
@@ -4,11 +4,12 @@
* rollback target, journaled versions, and the version the live terminal daemon was forked
* from. Anything the host cannot answer about is kept.
*/
import { NODE_RUNTIME_ASSETS, type ServerTarget } from '../../shared/node-runtime-pin'
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import type { ServeReadiness } from '../server/serve-readiness'
import { readOrcadActivationRecord } from './orcad-activation-record-store'
import { orcadSlotDir, type OrcadSlotOptions } from './orcad-recovery-slot'
import { gcOldOrcadVersions } from './orcad-remote-gc'
import { relayRuntimeStorePins } from './ssh-relay-runtime-ladder'
/** The daemon's build when the readiness proves it; undefined when it cannot say. */
export function provenLiveDaemonVersion(readiness: ServeReadiness): string | null | undefined {
@@ -22,7 +23,7 @@ export function provenLiveDaemonVersion(readiness: ServeReadiness): string | nul
export async function pruneManagedOrcadVersions(args: {
slot: OrcadSlotOptions
serverTarget: ServerTarget
serverTarget: NodeRuntimeTarget
activeVersion: string
readiness: ServeReadiness
}): Promise<void> {
@@ -39,7 +40,8 @@ export async function pruneManagedOrcadVersions(args: {
currentDirAbsPath: orcadSlotDir(args.slot, args.activeVersion),
record: await readOrcadActivationRecord(args.slot),
liveDaemonVersion,
nodeRuntimePins: [NODE_RUNTIME_ASSETS[args.serverTarget].executableSha256],
// Why the relay's pins: a compat orcad and a rung A relay share one runtime store.
nodeRuntimePins: relayRuntimeStorePins(args.serverTarget),
signal: args.slot.signal
})
} catch (error) {
+5 -4
View File
@@ -1,9 +1,9 @@
/** Everything a managed-orcad operation needs to know about one SSH host before it acts. */
import type { ServerTarget } from '../../shared/node-runtime-pin'
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import type { SshTarget } from '../../shared/ssh-types'
import type { OrcadActivationRecord } from './orcad-activation-record'
import { readOrcadActivationRecord } from './orcad-activation-record-store'
import { resolveOrcadDeploymentTarget } from './orcad-deployment-target'
import { resolveOrcadRuntimeTarget } from './orcad-runtime-target'
import { prepareWindowsOrcadHost } from './orcad-windows-host-preparation'
import { execOrcadRemote } from './orcad-remote-runtime-control'
import type { SshConnection } from './ssh-connection'
@@ -21,7 +21,8 @@ import { rememberSshHostPlatform } from './ssh-host-platform-memo'
export type OrcadRemoteContext = {
activationRecord: OrcadActivationRecord
serverTarget: ServerTarget
/** A compat runtime on hosts below the default runtime's glibc floor (design D6 rung B). */
serverTarget: NodeRuntimeTarget
connection: SshConnection
host: RemoteHostPlatform
remoteHome: string
@@ -47,7 +48,7 @@ export async function resolveOrcadRemoteContext(
if (!validateRemoteHome(remoteHome, host)) {
throw new Error(`Remote home is not a valid path: ${remoteHome.slice(0, 100)}`)
}
const serverTarget = await resolveOrcadDeploymentTarget({ conn: connection, host, signal })
const serverTarget = await resolveOrcadRuntimeTarget({ conn: connection, host, signal })
rememberSshHostPlatform(target.id, host, serverTarget)
if (isWindowsRemoteHost(host)) {
// Every Windows host op, the activation record read included, runs on the pinned node.exe.
+6 -5
View File
@@ -16,10 +16,11 @@ import type { OrcadTerminalCensus } from './orcad-update-plan'
import type { RemoteHostPlatform } from './ssh-remote-platform'
import { installOrcadBundle } from './orcad-remote-install'
import { getAppEnvironment } from '../../shared/app-environment'
import type { ServerTarget } from '../../shared/node-runtime-pin'
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import { ORCAD_STARTUP_READINESS_TIMEOUT_MS } from '../../shared/orcad-profile-preflight'
import { materializeOrcadArtifact } from './orcad-artifact-materializer'
import { readOrcadBundleTarget, resolveOrcadDeploymentTarget } from './orcad-deployment-target'
import { readOrcadBundleTarget } from './orcad-deployment-target'
import { resolveOrcadRuntimeTarget } from './orcad-runtime-target'
import { materializeNodeRuntimeArchive } from './pinned-runtime-materializer'
import {
resolveOrcadActivationReadinessTimeout,
@@ -33,8 +34,8 @@ export type OrcadDeployOptions = {
remoteHome: string
/** An already assembled bundle; otherwise materialize the packaged template for this host. */
localOrcadDir?: string
/** The bundle's server target; read from `localOrcadDir` or probed when absent. */
target?: ServerTarget
/** The bundle's runtime target; read from `localOrcadDir` or probed when absent. */
target?: NodeRuntimeTarget
/** Where the pinned runtime archive is cached; defaults beside the orcad artifact cache. */
runtimeCacheRoot?: string
nodePath: string
@@ -67,7 +68,7 @@ export async function deployOrcad(input: OrcadDeployOptions): Promise<OrcadDeplo
input.target ??
(input.localOrcadDir
? readOrcadBundleTarget(input.localOrcadDir)
: await resolveOrcadDeploymentTarget(input))
: await resolveOrcadRuntimeTarget(input))
const options = {
...input,
target,
+3 -3
View File
@@ -1,6 +1,6 @@
import { orcadNodePtyNativeArtifacts, orcadRipgrepArtifact } from '../../shared/orcad-artifacts'
import { orcadAgentBrowserNativeName } from '../../shared/orcad-agent-browser-name'
import type { ServerTarget } from '../../shared/node-runtime-pin'
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import { ensureRemoteOrcadNodeRuntime } from './orcad-remote-node-runtime'
import { execCommand } from './ssh-relay-deploy-helpers'
import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command'
@@ -22,7 +22,7 @@ export async function installOrcadBundle(
conn: SshConnection
host: RemoteHostPlatform
localOrcadDir: string
target: ServerTarget
target: NodeRuntimeTarget
/** The locally verified pinned Node archive, fetched only when the host lacks the runtime. */
nodeRuntimeArchive: () => Promise<string>
signal?: AbortSignal
@@ -94,7 +94,7 @@ export async function installOrcadBundle(
function executablePermissionsCommand(
host: RemoteHostPlatform,
directory: string,
target: ServerTarget
target: NodeRuntimeTarget
): string {
const required = [orcadRipgrepArtifact(target), ...orcadNodePtyNativeArtifacts(target)]
.filter((artifact) => /\/(?:rg|spawn-helper)$/.test(artifact))
+53
View File
@@ -0,0 +1,53 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { resolveOrcadRuntimeTarget } from './orcad-runtime-target'
import { SshConnection } from './ssh-connection'
import { createCallbacks, createTarget } from './ssh-connection-test-fixtures'
import { execCommand } from './ssh-relay-deploy-helpers'
import { getRemoteHostPlatform } from './ssh-remote-platform'
vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() }))
beforeEach(() => vi.mocked(execCommand).mockReset())
function resolveOn(platform: Parameters<typeof getRemoteHostPlatform>[0], ldd: string) {
vi.mocked(execCommand).mockResolvedValueOnce(ldd)
return resolveOrcadRuntimeTarget({
conn: new SshConnection(createTarget(), createCallbacks()),
host: getRemoteHostPlatform(platform)
})
}
describe('managed orcad runtime target', () => {
it.each([
['CentOS 7 (glibc 2.17)', 'ldd (GNU libc) 2.17', 'linux-x64-glibc217'],
['glibc 2.27, still below the default floor', 'ldd (GNU libc) 2.27', 'linux-x64-glibc217'],
['Debian 10 (glibc 2.28)', 'ldd (Debian GLIBC 2.28-10+deb10u2) 2.28', 'linux-x64-glibc'],
['Ubuntu 22.04 (glibc 2.35)', 'ldd (Ubuntu GLIBC 2.35-0ubuntu3.8) 2.35', 'linux-x64-glibc'],
['Alpine (musl)', 'musl libc (x86_64)\nVersion 1.2.5', 'linux-x64-musl']
])('deploys %s on the runtime the relay ladder would pick', async (_host, ldd, target) => {
await expect(resolveOn('linux-x64', ldd)).resolves.toBe(target)
})
it('keeps the host target when the glibc version is unreadable; the self-test decides', async () => {
await expect(resolveOn('linux-x64', 'ldd (GNU libc) unknown')).resolves.toBe('linux-x64-glibc')
})
it('refuses as unsupported where no runtime serves the glibc, so the connect keeps the relay', async () => {
await expect(resolveOn('linux-arm64', 'ldd (GNU libc) 2.17')).rejects.toMatchObject({
name: 'OrcadHostUnsupportedError',
message: expect.stringContaining('linux-arm64-glibc with glibc 2.17')
})
await expect(resolveOn('linux-x64', 'ldd (GNU libc) 2.12')).rejects.toMatchObject({
name: 'OrcadHostUnsupportedError'
})
})
it('needs no libc probe off Linux', async () => {
await expect(
resolveOrcadRuntimeTarget({
conn: new SshConnection(createTarget(), createCallbacks()),
host: getRemoteHostPlatform('darwin-arm64')
})
).resolves.toBe('darwin-arm64')
expect(execCommand).not.toHaveBeenCalled()
})
})
+27
View File
@@ -0,0 +1,27 @@
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import { resolveOrcadDeploymentTargetFacts } from './orcad-deployment-target'
import { OrcadHostUnsupportedError } from './orcad-host-unavailable'
import type { SshConnection } from './ssh-connection'
import { pinnedRuntimeTargetForHost } from './ssh-relay-runtime-ladder'
import type { RemoteHostPlatform } from './ssh-remote-platform'
/**
* The runtime target managed orcad deploys on a host, picked by glibc exactly as the relay ladder
* picks rung A or B: the host's own target, or its compat runtime below the default's glibc floor.
*/
export async function resolveOrcadRuntimeTarget(options: {
conn: SshConnection
host: RemoteHostPlatform
signal?: AbortSignal
exec?: (command: string) => Promise<string>
}): Promise<NodeRuntimeTarget> {
const facts = await resolveOrcadDeploymentTargetFacts(options)
const target = pinnedRuntimeTargetForHost(facts)
if (!target) {
const glibc = facts.glibc ? `${facts.glibc.major}.${facts.glibc.minor}` : 'unknown'
throw new OrcadHostUnsupportedError(
`No Orca runtime supports ${facts.target} with glibc ${glibc}.`
)
}
return target
}
@@ -7,7 +7,7 @@
*/
import { join } from 'node:path'
import { getAppEnvironment } from '../../shared/app-environment'
import type { ServerTarget } from '../../shared/node-runtime-pin'
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import { ensureRemoteOrcadNodeRuntime } from './orcad-remote-node-runtime'
import { installOrcadWindowsHostScript, orcadRemoteBaseDir } from './orcad-remote-windows-node'
import { materializeNodeRuntimeArchive } from './pinned-runtime-materializer'
@@ -18,7 +18,7 @@ export async function prepareWindowsOrcadHost(options: {
conn: SshConnection
host: RemoteHostPlatform
remoteHome: string
serverTarget: ServerTarget
serverTarget: NodeRuntimeTarget
signal?: AbortSignal
}): Promise<void> {
const baseDir = orcadRemoteBaseDir(options.host, options.remoteHome)
+3 -3
View File
@@ -2,7 +2,7 @@
* The platform each SSH host last reported this app session, so connect telemetry can name it
* without a probe of its own. Keyed by the local target id, which never leaves this process.
*/
import type { ServerTarget } from '../../shared/node-runtime-pin'
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import type { RemoteHostPlatform } from './ssh-remote-platform'
export type SshHostPlatformFacts = {
@@ -15,7 +15,7 @@ const known = new Map<string, SshHostPlatformFacts>()
function libcOf(
host: RemoteHostPlatform,
target: ServerTarget | null
target: NodeRuntimeTarget | null
): SshHostPlatformFacts['libc'] {
if (host.os !== 'linux') {
return 'none'
@@ -29,7 +29,7 @@ function libcOf(
export function rememberSshHostPlatform(
targetId: string,
host: RemoteHostPlatform,
target: ServerTarget | null
target: NodeRuntimeTarget | null
): void {
const libc = libcOf(host, target)
const previous = known.get(targetId)
@@ -68,6 +68,12 @@ describe('hostile-host cells', () => {
).toEqual(new Set(['A', 'B']))
})
it('deploys managed orcad on CentOS 7 with the compat runtime its relay rung B runs', () => {
const centos = cell('centos7-glibc217')
expect(centos.managed?.runtime).toBe('linux-x64-glibc217')
expect(centos.expect).toMatchObject({ runtime: centos.managed?.runtime })
})
it('pins every base image by digest and installs no compiler or host Node for rung A', () => {
for (const { dockerfile, expect: expectation } of DOCKER_CELLS) {
for (const from of dockerfile.filter((line) => line.startsWith('FROM '))) {
+15
View File
@@ -47,8 +47,15 @@ export type HostileHostExpectation =
export type HostileHostCellCore = {
id: string
expect: HostileHostExpectation
/** Also deploy managed orcad on a fresh host, on this runtime target. */
managed?: ManagedOrcadExpectation
}
export type ManagedOrcadExpectation =
| { outcome: 'activated'; runtime: NodeRuntimeTarget }
/** The candidate is refused with this deferral code, and the relay that follows settles on `relayRung`. */
| { outcome: 'refused'; runtime: NodeRuntimeTarget; code: string; relayRung: 'A' | 'B' }
export type DockerHostileHostCell = HostileHostCellCore & {
host?: 'docker'
/** Dockerfile lines, FROM included; the harness appends sshd and the toolchain shims. */
@@ -177,6 +184,14 @@ export const HOSTILE_HOST_CELLS: readonly HostileHostCell[] = [
target: 'linux-x64-glibc',
runtime: 'linux-x64-glibc217',
refusals: [{ step: 'A', reason: 'libc_floor' }]
},
// Managed orcad picks the same compat runtime, but the template's @parcel/watcher needs a newer
// libstdc++ than CentOS 7 ships, so its preflight refuses and the host keeps relay rung B.
managed: {
outcome: 'refused',
runtime: 'linux-x64-glibc217',
code: 'orcad_candidate_preflight_failed',
relayRung: 'B'
}
},
{
@@ -0,0 +1,119 @@
/**
* The managed-orcad half of a hostile-host cell: on a fresh host, resolve the context, deploy
* orcad, and prove it picked the cell's runtime and uploaded no other. An activated server must
* be live; a refused one must be classified as unable to run, so the relay that follows runs the
* pinned ladder and lands on the cell's rung.
*/
import { posix } from 'node:path'
import { expect } from 'vitest'
import { NODE_RUNTIME_ASSETS, pinnedNodeRuntimeAsset } from '../../shared/node-runtime-pin'
import { ORCAD_MANAGED_REMOTE_PORT } from '../../shared/orcad-managed-runtime'
import { ORCAD_RUNTIMES_DIRNAME } from '../../shared/orcad-artifacts'
import type { SshTarget } from '../../shared/ssh-types'
import { readOrcadActivationRecord } from './orcad-activation-record-store'
import { classifyOrcadHostUnavailable } from './orcad-host-unavailable'
import { managedOrcadSlot } from './orcad-managed-runtime-context'
import { orcadSlotDir, type OrcadSlotOptions } from './orcad-recovery-slot'
import { resolveOrcadRemoteContext } from './orcad-remote-context'
import { deployOrcad } from './orcad-remote-deploy'
import { orcadLivenessProbeCommand, parseOrcadLiveness } from './orcad-remote-launch'
import { execOrcadRemote } from './orcad-remote-runtime-control'
import { decommissionRemoteOrcad } from './orcad-remote-stop'
import type { HostileHostCellCore, ManagedOrcadExpectation } from './ssh-hostile-host-cells'
import { connectHostileHost, deployOnce } from './ssh-hostile-host-test-harness'
import { hostExecStatus, type HostileHostTarget } from './ssh-hostile-host-test-fixture'
const EMPTY_CENSUS = { liveSessions: 0, startedSinceActivation: 0, daemonProtocolVersion: null }
export async function proveManagedOrcadCell(
cell: HostileHostCellCore,
target: HostileHostTarget,
sshTarget: SshTarget
): Promise<void> {
const expected = cell.managed
if (!expected) {
throw new Error(`${cell.id} names no managed expectation`)
}
const conn = await connectHostileHost(sshTarget)
try {
const context = await resolveOrcadRemoteContext(sshTarget, conn)
expect(context.serverTarget).toBe(expected.runtime)
const slot = managedOrcadSlot(context, ORCAD_MANAGED_REMOTE_PORT)
const deployed = await deployOrcad({
...slot,
target: context.serverTarget,
census: EMPTY_CENSUS
})
const slotDir = orcadSlotDir(slot, deployed.fullVersion)
await assertOnlyRuntimeUploaded(target, slotDir, expected)
if (expected.outcome === 'refused') {
// The message carries the full reason, which toMatchObject's diff omits.
expect(deployed, JSON.stringify(deployed)).toMatchObject({
outcome: 'installed-not-activated',
code: expected.code
})
expect(classifyOrcadHostUnavailable(deployed)).toBe('native_preflight')
await assertRelayFallback(sshTarget, expected.relayRung)
return
}
expect(deployed, JSON.stringify(deployed)).toMatchObject({ outcome: 'installed-and-activated' })
await assertLiveThenDecommissioned(slot, slotDir, deployed.fullVersion)
} finally {
await conn.disconnect().catch(() => {})
}
}
/** A default runtime beside the expected one is the wrong upload this cell guards against. */
async function assertOnlyRuntimeUploaded(
target: HostileHostTarget,
slotDir: string,
expected: ManagedOrcadExpectation
): Promise<void> {
const runtimes = posix.join(posix.dirname(slotDir), ORCAD_RUNTIMES_DIRNAME)
const runtimeDir = (sha: string): string => posix.join(runtimes, `node-${sha}`)
const { executableSha256 } = pinnedNodeRuntimeAsset(expected.runtime)
expect(await hostExecStatus(target, `test -x '${runtimeDir(executableSha256)}/bin/node'`)).toBe(0)
for (const asset of Object.values(NODE_RUNTIME_ASSETS)) {
if (asset.executableSha256 !== executableSha256) {
expect(
await hostExecStatus(target, `test -e '${runtimeDir(asset.executableSha256)}'`)
).not.toBe(0)
}
}
}
/** The connect's next step: no runtime setting, but orcad recorded unable to run on this host. */
async function assertRelayFallback(sshTarget: SshTarget, rung: 'A' | 'B'): Promise<void> {
const { remoteRuntime: _setting, ...withoutSetting } = sshTarget
const conn = await connectHostileHost({
...withoutSetting,
managedServerUnavailable: { reason: 'native_preflight', appVersion: 'hostile-hosts' }
})
try {
const attempt = await deployOnce(conn)
expect(attempt.error).toBeNull()
expect(attempt.settledRung).toBe(rung)
} finally {
await conn.disconnect().catch(() => {})
}
}
async function assertLiveThenDecommissioned(
slot: OrcadSlotOptions,
slotDir: string,
version: string
): Promise<void> {
const liveness = async (): Promise<string> =>
parseOrcadLiveness(await execOrcadRemote(slot, orcadLivenessProbeCommand(slot.host, slotDir)))
expect(await liveness()).toBe('LIVE')
const decommission = await decommissionRemoteOrcad({
...slot,
record: await readOrcadActivationRecord(slot),
census: EMPTY_CENSUS
})
expect(decommission, JSON.stringify(decommission)).toMatchObject({
outcome: 'decommissioned',
version
})
expect(await liveness()).toBe('DEAD')
}
@@ -358,6 +358,35 @@ describe('deployAndLaunchRelay on the pinned Node runtime', () => {
warn.mockRestore()
})
it('runs the ladder to rung B where this connect just recorded that managed orcad cannot run', async () => {
const registry = {
getTarget: vi.fn(() => ({
id: 'target-1',
managedServerUnavailable: { reason: 'runtime_self_test', appVersion: '1.0.0' }
})),
updateTarget: vi.fn(() => null)
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the ladder reads and writes only these two registry members.
vi.mocked(getSshTargetRegistryStore).mockReturnValue(registry as unknown as SshConnectionStore)
vi.spyOn(console, 'warn').mockImplementation(() => {})
const oldGlibc = { major: 2, minor: 17 }
vi.mocked(resolvePinnedRelayTargetFacts).mockResolvedValue({
target: 'linux-x64-glibc',
glibc: oldGlibc
})
vi.mocked(planPinnedNodeRelay)
.mockResolvedValueOnce({ kind: 'host-node', fallbackReason: 'libc_floor' })
.mockResolvedValueOnce({ ...pinnedPlan(), target: 'linux-x64-glibc217', glibc: oldGlibc })
queueInstalledPinnedLaunch()
// The connection's own target predates the record and carries no runtime setting.
const result = await deployAndLaunchRelay(makeConnection(), undefined, undefined, 'target-1')
expect(resolveRemoteNodePath).not.toHaveBeenCalled()
expect(result.nodePath).toBe(COMPAT_NODE)
expect(planHostNodeAddonRelay).not.toHaveBeenCalled()
})
it('skips rung B on a current glibc when rung A refused for a reason B cannot answer', async () => {
const conn = makeConnection('pinned-node')
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+4 -2
View File
@@ -2,7 +2,7 @@
import { existsSync } from 'node:fs'
import { app } from 'electron'
import { relayBundleCandidates } from './relay-bundle-paths'
import { PinnedRelayFallbackError, resolveSshRemoteRuntime } from './ssh-relay-pinned-node'
import { PinnedRelayFallbackError, resolveConnectRemoteRuntime } from './ssh-relay-pinned-node'
import {
ensurePinnedRelayRuntime,
prebuiltRelayNodePath,
@@ -424,8 +424,10 @@ async function deployAndLaunchRelayInner(
deploySignal?: AbortSignal
): Promise<RelayDeployResult> {
const target = typeof conn.getTarget === 'function' ? conn.getTarget() : undefined
const ladder = relayRuntimeLadder(resolveSshRemoteRuntime(target))
const registry = getSshTargetRegistryStore()
const ladder = relayRuntimeLadder(
resolveConnectRemoteRuntime(target, target && registry?.getTarget(target.id))
)
const run = new RelayRuntimeLadderRun(
target?.id ?? relayInstanceId ?? '',
target && registry ? sshTargetRelayRuntimeDecisionStore(registry) : null
@@ -1,6 +1,6 @@
// Design D5/D6 hostile-host matrix: the real client-side relay deploy against container SSH
// targets, and against a macOS runner's own loopback sshd, asserting which rung of the runtime
// ladder each host lands on.
// ladder each host lands on, and for cells naming one, the runtime managed orcad deploys on.
//
// Run: ORCA_RUN_SSH_HOSTILE_HOSTS=1 pnpm test src/main/ssh/ssh-relay-hostile-hosts.docker.test.ts
// Needs `pnpm build:relay` and an orcad template holding each selected cell's slot. Docker cells
@@ -17,6 +17,7 @@ import { posix } from 'node:path'
import { NODE_RUNTIME_PIN } from '../../shared/node-runtime-pin'
import type { SshConnection } from './ssh-connection'
import { HOSTILE_HOST_CELLS, selectHostileHostCells } from './ssh-hostile-host-cells'
import { proveManagedOrcadCell } from './ssh-hostile-host-managed-orcad'
import {
assertCell,
connectHostileHost,
@@ -121,4 +122,21 @@ describe('SSH relay hostile-host matrix', () => {
CELL_TIMEOUT_MS
)
}
// A fresh host per cell: the relay pass above leaves a relay and its runtime behind.
for (const cell of HOSTILE_HOST_CELLS.filter((candidate) => candidate.managed)) {
it.skipIf(!SELECTED.has(cell.id))(
`${cell.id} deploys managed orcad on ${cell.managed?.runtime} (${cell.managed?.outcome})`,
async () => {
let target: HostileHostTarget | null = null
try {
target = await startHostileHostTarget(cell)
await proveManagedOrcadCell(cell, target, hostileHostSshTarget(target))
} finally {
await stopHostileHostTarget(target)
}
},
CELL_TIMEOUT_MS
)
}
})
@@ -78,6 +78,25 @@ describe('remote runtime setting', () => {
)
).toBe('legacy')
})
it('runs the pinned ladder where managed orcad was found unable to run, unless the host opts out', () => {
const unavailable = { reason: 'runtime_self_test', appVersion: '1.0.0' }
expect(resolveSshRemoteRuntime({ managedServerUnavailable: unavailable }, {})).toBe(
'pinned-node'
)
expect(
resolveSshRemoteRuntime(
{ managedServerUnavailable: unavailable, remoteRuntime: 'legacy' },
{}
)
).toBe('legacy')
expect(
resolveSshRemoteRuntime(
{ managedServerUnavailable: unavailable },
{ [SSH_REMOTE_RUNTIME_ENV]: 'legacy' }
)
).toBe('legacy')
})
})
describe('pinned relay version (D8.1)', () => {
+16 -5
View File
@@ -2,9 +2,9 @@
* Plan a relay launch on Orca's pinned Node with the orcad slot's prebuilt addons, instead
* of the host's Node plus a host-side npm install (design D5, D6 rung A, D8.1).
*
* Opt-in per host (`SshTarget.remoteRuntime`). Anything this module cannot
* establish on the client, and every classified refusal from the host, falls back to the
* legacy host-Node path with a logged reason.
* Opt-in per host (`SshTarget.remoteRuntime`), and on by default where managed orcad can't run.
* Anything this module cannot establish on the client, and every classified refusal from the
* host, falls back to the legacy host-Node path with a logged reason.
*/
import { createHash } from 'node:crypto'
import { chmod, copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
@@ -52,14 +52,25 @@ export const PINNED_NODE_GLIBC_FLOOR: GlibcVersion = { major: 2, minor: 28 }
export const SSH_REMOTE_RUNTIME_ENV = 'ORCA_SSH_REMOTE_RUNTIME'
export function resolveSshRemoteRuntime(
target: Pick<SshTarget, 'remoteRuntime'> | undefined,
target: Pick<SshTarget, 'remoteRuntime' | 'managedServerUnavailable'> | undefined,
env: NodeJS.ProcessEnv = process.env
): SshRemoteRuntime {
if (target?.remoteRuntime) {
return target.remoteRuntime
}
const fromEnv = SSH_REMOTE_RUNTIMES.find((runtime) => runtime === env[SSH_REMOTE_RUNTIME_ENV])
return fromEnv ?? DEFAULT_SSH_REMOTE_RUNTIME
// Why: where managed orcad can't run the relay is the host's server; the ladder still ends at legacy.
return fromEnv ?? (target?.managedServerUnavailable ? 'pinned-node' : DEFAULT_SSH_REMOTE_RUNTIME)
}
/** Why `recorded`: this connect's host-server decision may have just recorded orcad unavailable. */
export function resolveConnectRemoteRuntime(
target: SshTarget | undefined,
recorded: SshTarget | undefined
): SshRemoteRuntime {
const managedServerUnavailable =
recorded?.managedServerUnavailable ?? target?.managedServerUnavailable
return resolveSshRemoteRuntime(target && { ...target, managedServerUnavailable })
}
export function isGlibcBelow(version: GlibcVersion, floor: GlibcVersion): boolean {
+4
View File
@@ -153,6 +153,10 @@ export function isCompatServerTarget(target: string): target is CompatServerTarg
return COMPAT_SERVER_TARGETS.some((known) => known === target)
}
export function isNodeRuntimeTarget(target: string): target is NodeRuntimeTarget {
return isCompatServerTarget(target) || SERVER_TARGETS.some((known) => known === target)
}
/** The pinned asset for a known default or compat target. */
export function pinnedNodeRuntimeAsset(target: NodeRuntimeTarget): NodeRuntimeAsset {
return isCompatServerTarget(target)