mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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 '))) {
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user