Files
orca/src/main/managed-server/install/orcad-remote-node-runtime.ts
T
Neil e7d77e863c refactor(orcad): name the remote install helpers for what they install (#31647)
The SFTP/exec-stdin transfer helpers, their options type and the install
timeout still carried relay names from before orcad; they install the orcad
bundle and the Node runtime. The timeout's derivation from the relay's
native-deps budget is replaced by its value and the reason for it.
2026-10-21 13:54:26 -07:00

379 lines
16 KiB
TypeScript

/**
* Puts the pinned Node beside the orcad slots, at `runtimes/node-<executableSha256>/bin/node`
* (design D2/D5), where `selectOrcadSlotRuntimeCommand` resolves a slot's `.runtime-node`.
* The OpenCode vault reader uses the same store on SSH and WSL hosts (design D4a).
*
* The official archive is uploaded as published and extracted on the host; the executable's
* hash is checked there before it is published. Promotion runs under the store lock that
* store GC also takes (design D5); nothing here deletes a runtime.
*/
import { randomBytes } from 'node:crypto'
import { rm } from 'node:fs/promises'
import { basename } from 'node:path'
import {
pinnedNodeRuntimeAsset,
NODE_RUNTIME_PIN,
nodeRuntimeExecutablePath,
type NodeRuntimeTarget
} from '../../../shared/node-runtime-pin'
import {
ORCAD_NODE_RUNTIME_DIR_PREFIX,
ORCAD_NODE_RUNTIME_POSIX_EXECUTABLE,
ORCAD_RUNTIMES_DIRNAME,
orcadNodeRuntimeExecutable
} from '../../../shared/orcad-artifacts'
import type { OrcadHostTransport } from '../../ssh/orcad-host-transport'
import { quotePosixShell } from '../../../shared/shell-argument-quoting'
import {
execCommand,
isUnconfirmedSshCommandTermination
} from '../../ssh/remote-exec/remote-exec-command'
import { uploadRemoteInstallDirectory } from './remote-install-transfers'
import { remoteSha256Command } from './remote-sha256-command'
import { stageNodeRuntimeArchive } from './remote-node-runtime-host-download'
import {
isWindowsRemoteHost,
joinRemotePath,
remoteDirname,
type RemoteHostPlatform
} from '../../ssh/ssh-remote-platform'
import { assertPosixOrcadHost } from './orcad-remote-host-support'
import { withRuntimeStoreLock, type HeldRuntimeStoreLock } from './remote-node-runtime-store-lock'
import {
assertRemoteNodeRuntimePromoted,
assertRemoteNodeRuntimeStoreExecutable,
REMOTE_NODE_RUNTIME_EXIT_PREFIX,
REMOTE_NODE_RUNTIME_NOEXEC,
REMOTE_NODE_RUNTIME_MISSING,
REMOTE_NODE_RUNTIME_READY,
REMOTE_NODE_RUNTIME_SELFTEST_FAILED,
REMOTE_NODE_RUNTIME_VERIFIED_MARKER
} from '../../ssh/orcad-remote-node-runtime-report'
import {
windowsNodeRuntimePresentCommand,
windowsNodeRuntimeProbeCommand,
windowsNodeRuntimePromoteCommand,
windowsNodeRuntimeStageCleanupCommand
} from './orcad-remote-node-runtime-windows'
const VERIFIED_MARKER = REMOTE_NODE_RUNTIME_VERIFIED_MARKER
/**
* Any step that reads the whole ~110 MiB executable (probe hash, extract, self-test) takes minutes
* on slow storage (HH-3), and Windows may fall back to Expand-Archive.
*/
export const NODE_RUNTIME_PROMOTE_TIMEOUT_MS = 300_000
/** Upload stages sit in the store beside the runtimes they become; store GC sweeps stale ones. */
export const RUNTIME_STORE_STAGE_PREFIX = '.stage-'
/** `<storeParent>/runtimes/node-<executableSha256>`, the one host layout (design D5). */
export function nodeRuntimeStoreDir(
host: RemoteHostPlatform,
storeParent: string,
target: NodeRuntimeTarget
): string {
return joinRemotePath(
host,
storeParent,
ORCAD_RUNTIMES_DIRNAME,
`${ORCAD_NODE_RUNTIME_DIR_PREFIX}${pinnedNodeRuntimeAsset(target).executableSha256}`
)
}
/**
* The runtime directory beside a version dir (an orcad slot or a relay install); the slot
* selector computes the same path, and the vault reader shares the store.
*/
export function remoteNodeRuntimeDir(
host: RemoteHostPlatform,
slotDir: string,
target: NodeRuntimeTarget
): string {
return nodeRuntimeStoreDir(host, remoteDirname(slotDir.replace(/\/+$/, ''), host), target)
}
/** The executable inside a POSIX runtime directory. */
export function posixNodeRuntimeExecutable(host: RemoteHostPlatform, runtimeDir: string): string {
return joinRemotePath(host, runtimeDir, ...ORCAD_NODE_RUNTIME_POSIX_EXECUTABLE.split('/'))
}
/** A per-installer stage beside the runtime; its dot prefix keeps it out of `node-*` listings. */
export function nodeRuntimeStageDir(
host: RemoteHostPlatform,
runtimeDir: string,
token: string
): string {
return joinRemotePath(
host,
remoteDirname(runtimeDir, host),
`${RUNTIME_STORE_STAGE_PREFIX}${basename(runtimeDir)}-${token}`
)
}
/**
* Creates the stage and runs a two-line script from it, so a store that can't run programs (a
* `noexec` home) is named before the ~40 MiB upload instead of failing the self-test after it.
*/
export function createExecProbedStageCommand(host: RemoteHostPlatform, stageDir: string): string {
const stage = quotePosixShell(stageDir)
const probe = quotePosixShell(joinRemotePath(host, stageDir, '.exec-probe'))
return (
// Why the probe write may fail quietly: a full disk is diagnosed by the upload that follows.
`mkdir -p ${stage} && { if printf '#!/bin/sh\\necho ok\\n' > ${probe} 2>/dev/null && chmod 700 ${probe}; then ` +
`orca_x=$(${probe} 2>&1); orca_x_status=$?; rm -f ${probe}; ` +
`[ "$orca_x_status" = 0 ] || { echo ${REMOTE_NODE_RUNTIME_NOEXEC}; ` +
`echo "${REMOTE_NODE_RUNTIME_EXIT_PREFIX}$orca_x_status"; printf '%s\\n' "$orca_x" | head -c 2000; }; ` +
`else rm -f ${probe}; fi; }`
)
}
/** Ready only when the executable hashes to the pin and reports the pinned version. */
export function probeRemoteNodeRuntimeCommand(
host: RemoteHostPlatform,
runtimeDir: string,
target: NodeRuntimeTarget
): string {
if (isWindowsRemoteHost(host)) {
return windowsNodeRuntimeProbeCommand(runtimeDir, target)
}
const executable = quotePosixShell(posixNodeRuntimeExecutable(host, runtimeDir))
const verified = quotePosixShell(joinRemotePath(host, runtimeDir, VERIFIED_MARKER))
return (
`if [ -f ${verified} ] && [ -x ${executable} ] && ` +
`[ "$(${remoteSha256Command(executable)})" = ${quotePosixShell(pinnedNodeRuntimeAsset(target).executableSha256)} ]; ` +
`then echo ${REMOTE_NODE_RUNTIME_READY}; else echo ${REMOTE_NODE_RUNTIME_MISSING}; fi`
)
}
/** Cheap warm-path check: a published runtime has its marker and an executable; no re-hash. */
/**
* READY when the verified runtime is in place. With `run`, the same command also runs it and
* reports a refusal the way promotion does, so a host whose exec policy or libraries changed since
* the install is caught before a launch, at no extra round trip.
*/
export function remoteNodeRuntimePresentCommand(
host: RemoteHostPlatform,
runtimeDir: string,
run = false
): string {
if (isWindowsRemoteHost(host)) {
return windowsNodeRuntimePresentCommand(runtimeDir, run)
}
const executable = quotePosixShell(
joinRemotePath(host, runtimeDir, ...ORCAD_NODE_RUNTIME_POSIX_EXECUTABLE.split('/'))
)
const verified = quotePosixShell(joinRemotePath(host, runtimeDir, VERIFIED_MARKER))
const ready = run
? `{ orca_rt_out=$(${executable} --version 2>&1); orca_rt_status=$?; ` +
`if [ "$orca_rt_out" = ${quotePosixShell(`v${NODE_RUNTIME_PIN.version}`)} ]; then echo ${REMOTE_NODE_RUNTIME_READY}; ` +
`else echo ${REMOTE_NODE_RUNTIME_SELFTEST_FAILED}; echo "${REMOTE_NODE_RUNTIME_EXIT_PREFIX}$orca_rt_status"; ` +
`printf '%s\\n' "$orca_rt_out" | head -c 4000; fi; }`
: `echo ${REMOTE_NODE_RUNTIME_READY}`
return (
`if [ -f ${verified} ] && [ -x ${executable} ]; ` +
`then ${ready}; else echo ${REMOTE_NODE_RUNTIME_MISSING}; fi`
)
}
/**
* Extract, verify, self-test and publish. The executable is renamed into place file-by-file,
* so a concurrent installer of the same pin only ever replaces identical verified bytes.
*/
export function promoteRemoteNodeRuntimeCommand(
host: RemoteHostPlatform,
args: {
stageDir: string
archive: string
runtimeDir: string
target: NodeRuntimeTarget
token: string
}
): string {
assertPosixOrcadHost(host)
const asset = pinnedNodeRuntimeAsset(args.target)
const member = nodeRuntimeExecutablePath(args.target, asset.archive)
const stage = quotePosixShell(args.stageDir)
const extracted = quotePosixShell(joinRemotePath(host, args.stageDir, ...member.split('/')))
const binDir = quotePosixShell(joinRemotePath(host, args.runtimeDir, 'bin'))
const temporary = quotePosixShell(
joinRemotePath(host, args.runtimeDir, 'bin', `node.${args.token}`)
)
const executable = quotePosixShell(posixNodeRuntimeExecutable(host, args.runtimeDir))
const verified = quotePosixShell(joinRemotePath(host, args.runtimeDir, VERIFIED_MARKER))
return [
`cd ${stage} || exit 1`,
`tar -xzf ${quotePosixShell(joinRemotePath(host, args.stageDir, args.archive))} ${quotePosixShell(member)} || { echo ORCA_NODE_RUNTIME_EXTRACT_FAILED; exit 1; }`,
`[ "$(${remoteSha256Command(extracted)})" = ${quotePosixShell(asset.executableSha256)} ] || { echo ORCA_NODE_RUNTIME_HASH_MISMATCH; exit 1; }`,
`chmod 755 ${extracted}`,
// Why run it: executing is the only reliable check for noexec mounts and a wrong libc.
// Exit 0 on refusal so the caller receives the loader's words to classify, not a bare exit 1.
`{ orca_rt_out=$(${extracted} --version 2>&1); orca_rt_status=$?; ` +
`[ "$orca_rt_out" = ${quotePosixShell(`v${NODE_RUNTIME_PIN.version}`)} ] || ` +
`{ echo ${REMOTE_NODE_RUNTIME_SELFTEST_FAILED}; echo "${REMOTE_NODE_RUNTIME_EXIT_PREFIX}$orca_rt_status"; ` +
`printf '%s\\n' "$orca_rt_out" | head -c 4000; exit 0; }; }`,
`mkdir -p ${binDir}`,
`mv -f ${extracted} ${temporary}`,
`mv -f ${temporary} ${executable}`,
`: > ${verified}`,
`echo ${REMOTE_NODE_RUNTIME_READY}`
].join(' && ')
}
/**
* The under-lock step as one host command: publish unless a sibling already did while this client
* uploaded. One command, so the host's lock release covers the re-hash as well as the promote.
*/
export function recheckAndPromoteRemoteNodeRuntimeCommand(
host: RemoteHostPlatform,
args: Parameters<typeof promoteRemoteNodeRuntimeCommand>[1]
): string {
const probe = probeRemoteNodeRuntimeCommand(host, args.runtimeDir, args.target)
return (
`if [ "$(${probe})" = ${REMOTE_NODE_RUNTIME_READY} ]; then echo ${REMOTE_NODE_RUNTIME_READY}; ` +
`else ${promoteRemoteNodeRuntimeCommand(host, args)}; fi`
)
}
/**
* Copies the archive at `$1` (a path the host can already read, e.g. a WSL view of the
* client's cache) into a fresh stage, promotes it, and removes the stage whatever happens.
*/
export function installNodeRuntimeFromHostArchiveCommand(
host: RemoteHostPlatform,
args: { runtimeDir: string; archive: string; target: NodeRuntimeTarget; token: string }
): string {
const stageDir = nodeRuntimeStageDir(host, args.runtimeDir, args.token)
const stage = quotePosixShell(stageDir)
const promote = promoteRemoteNodeRuntimeCommand(host, { ...args, stageDir })
const copy = `cp -- "$1" ${quotePosixShell(joinRemotePath(host, stageDir, args.archive))}`
// Subshell: promote's `exit 1` must still reach the stage cleanup.
return `(umask 077 && mkdir -p ${stage} && ${copy} && ${promote}); status=$?; rm -rf ${stage}; exit $status`
}
export type RemoteRuntimeStep = <T>(operation: () => Promise<T>) => Promise<T>
const runDirectly: RemoteRuntimeStep = (operation) => operation()
export type EnsuredRemoteNodeRuntime = {
executable: string
transfer: 'cached' | 'uploaded' | 'host-downloaded'
}
/**
* Ensures the runtime beside `slotDir` exists on the host, uploading the pinned archive only
* when needed, and returns its executable and whether this call uploaded it.
*/
export async function ensureRemoteOrcadNodeRuntime(options: {
conn: OrcadHostTransport
host: RemoteHostPlatform
slotDir: string
target: NodeRuntimeTarget
/** The locally verified pinned archive (pinned-runtime-materializer). */
archivePath: () => Promise<string>
/** Where a hand-copied archive makes `archivePath` succeed offline; named when every download fails. */
offlineArchivePath?: string
signal?: AbortSignal
/** Wraps each host round trip, so a caller can tell an unconfirmed channel from a local failure. */
remoteStep?: RemoteRuntimeStep
/** Replaces the POSIX first probe only; the under-lock recheck always hashes. */
posixProbeCommand?: typeof probeRemoteNodeRuntimeCommand
}): Promise<EnsuredRemoteNodeRuntime> {
const { conn, host, target, signal } = options
const remoteStep = options.remoteStep ?? runDirectly
const windows = isWindowsRemoteHost(host)
// Why unwrapped on Windows: these are already self-contained powershell.exe command lines.
const exec = (command: string, execOptions: { signal?: AbortSignal; timeoutMs?: number } = {}) =>
remoteStep(() => execCommand(conn, command, { ...execOptions, wrapCommand: !windows }))
const runtimeDir = remoteNodeRuntimeDir(host, options.slotDir, target)
const executable = joinRemotePath(
host,
runtimeDir,
...orcadNodeRuntimeExecutable(target).split('/')
)
const token = randomBytes(8).toString('hex')
const stageDir = nodeRuntimeStageDir(host, runtimeDir, token)
// Why the stage rides the probe on Windows: every exec there is a powershell.exe spawn.
const probe = await exec(
windows
? windowsNodeRuntimeProbeCommand(runtimeDir, target, stageDir)
: (options.posixProbeCommand ?? probeRemoteNodeRuntimeCommand)(host, runtimeDir, target),
{ signal, timeoutMs: NODE_RUNTIME_PROMOTE_TIMEOUT_MS }
)
if (probe.trim() === REMOTE_NODE_RUNTIME_READY) {
return { executable, transfer: 'cached' }
}
const cleanupStage = windows
? windowsNodeRuntimeStageCleanupCommand(stageDir)
: `rm -rf ${quotePosixShell(stageDir)}`
let localStage: string | undefined
let stageUnconfirmed = false
let hostRemovedStage = false
try {
// Why before the archive: fetching it may download, and a noexec store would waste both.
if (!windows) {
assertRemoteNodeRuntimeStoreExecutable(
await exec(createExecProbedStageCommand(host, stageDir), { signal }),
remoteDirname(runtimeDir, host)
)
}
const { archive } = await stageNodeRuntimeArchive({
...options,
exec,
stageDir,
upload: (dir) =>
remoteStep(() => uploadRemoteInstallDirectory(conn, dir, stageDir, host, { signal })),
onLocalStage: (dir) => (localStage = dir)
})
let promoteRan = false
// Why unwrapped on Windows: these are already self-contained powershell.exe command lines.
const runLocked = (command: string): Promise<string> =>
execCommand(conn, command, {
signal,
timeoutMs: NODE_RUNTIME_PROMOTE_TIMEOUT_MS,
wrapCommand: !windows
})
// Why re-probe under the lock: a sibling installer may have published this pin while we uploaded.
const recheckAndPromote = async (held: HeldRuntimeStoreLock): Promise<string> => {
if (!windows) {
return runLocked(
held.releasedByHostOnExit(
recheckAndPromoteRemoteNodeRuntimeCommand(host, {
stageDir,
archive,
runtimeDir,
target,
token
})
)
)
}
if (
(await runLocked(probeRemoteNodeRuntimeCommand(host, runtimeDir, target))).trim() ===
REMOTE_NODE_RUNTIME_READY
) {
return REMOTE_NODE_RUNTIME_READY
}
promoteRan = true
return runLocked(windowsNodeRuntimePromoteCommand({ stageDir, archive, runtimeDir, target }))
}
// Why the lock on Windows too: store GC collects there as well (design D5).
const promoted = await remoteStep(() =>
withRuntimeStoreLock(conn, host, remoteDirname(runtimeDir, host), recheckAndPromote, signal)
)
// Why: the Windows promote script removes its stage on every path; skip a second powershell.exe.
hostRemovedStage = windows && promoteRan
assertRemoteNodeRuntimePromoted(promoted)
return { executable, transfer: localStage ? 'uploaded' : 'host-downloaded' }
} catch (error) {
stageUnconfirmed = isUnconfirmedSshCommandTermination(error)
throw error
} finally {
if (localStage) {
await rm(localStage, { recursive: true, force: true }).catch(() => {})
}
// A transfer that may still be writing keeps its stage; loss of contact is not an exit.
if (!stageUnconfirmed && !hostRemovedStage) {
await exec(cleanupStage).catch(() => {})
}
}
}