refactor(orcad): orcad artifacts live under orcad/ and node-runtime/ (#27963)

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-12 04:16:28 -07:00
committed by GitHub
co-authored by m4air
parent 7c13365f03
commit 4ad0129984
52 changed files with 169 additions and 282 deletions
+3
View File
@@ -13,6 +13,9 @@ on:
- 'src/main/ssh/ssh-relay-*'
- 'src/main/ssh/*runtime*'
- 'src/main/ssh/orcad-*'
- 'src/main/orcad/orcad-artifact-materializer.ts'
- 'src/main/orcad/orcad-host-unavailable.ts'
- 'src/main/node-runtime/**'
- 'src/main/ssh/remote-install-*'
- 'src/main/ssh/ssh-remote-*'
- 'src/main/ssh/ssh-hostile-host-*'
+3
View File
@@ -11,6 +11,9 @@ on:
- 'src/main/ssh/ssh-relay-*'
- 'src/main/ssh/*runtime*'
- 'src/main/ssh/orcad-*'
- 'src/main/orcad/orcad-artifact-materializer.ts'
- 'src/main/orcad/orcad-host-unavailable.ts'
- 'src/main/node-runtime/**'
- 'src/main/ssh/remote-install-*'
- 'src/main/ssh/ssh-remote-*'
- 'src/main/ssh/ssh-hostile-host-*'
@@ -15,8 +15,8 @@ export const RUNTIME_LAUNCHER_PATHS = [
'src/main/orcad/launcher.ts',
'config/scripts/orcad-entry-build.mjs',
'src/shared/node-runtime-pin.ts',
'src/main/ssh/pinned-runtime-materializer.ts',
'src/main/ssh/runtime-archive-download.ts',
'src/main/node-runtime/pinned-runtime-materializer.ts',
'src/main/node-runtime/runtime-archive-download.ts',
'src/main/ssh/orcad-remote-node-runtime.ts',
// orcad slot layout: which runtime file a packaged slot carries.
'src/shared/orcad-artifacts.ts',
+5 -5
View File
@@ -5281,7 +5281,7 @@
"src/main/ssh/orcad-activation-outcome-log.test.ts": 55,
"src/main/ssh/orcad-activation-record.test.ts": 254,
"src/main/ssh/orcad-activation-transaction.test.ts": 398,
"src/main/ssh/orcad-artifact-materializer.test.ts": 722,
"src/main/orcad/orcad-artifact-materializer.test.ts": 722,
"src/main/ssh/orcad-decommission-released-peer-recovery.test.ts": 295,
"src/main/ssh/orcad-decommission-transaction.test.ts": 232,
"src/main/ssh/orcad-deployment-target.test.ts": 234,
@@ -5290,7 +5290,7 @@
"src/main/ssh/orcad-exited-own-install-lock.test.ts": 4492,
"src/main/ssh/orcad-exited-own-lock-windows.test.ts": 350,
"src/main/ssh/orcad-fence-lost-classification.test.ts": 287,
"src/main/ssh/orcad-host-unavailable.test.ts": 325,
"src/main/orcad/orcad-host-unavailable.test.ts": 325,
"src/main/ssh/orcad-managed-auto-update.test.ts": 319,
"src/main/ssh/orcad-managed-bound-port.test.ts": 386,
"src/main/ssh/orcad-managed-serving.test.ts": 480,
@@ -5364,8 +5364,8 @@
"src/main/ssh/orcad-windows-host-fence-ops.test.ts": 763,
"src/main/ssh/orcad-windows-host-lane.test.ts": 745,
"src/main/ssh/orcad-windows-host-state-ops.test.ts": 2035,
"src/main/ssh/pinned-runtime-materializer-node.test.ts": 114,
"src/main/ssh/pinned-runtime-materializer.test.ts": 3192,
"src/main/node-runtime/pinned-runtime-materializer-node.test.ts": 114,
"src/main/node-runtime/pinned-runtime-materializer.test.ts": 3192,
"src/main/ssh/relay-bundle-paths.test.ts": 62,
"src/main/ssh/relay-protocol.test.ts": 120,
"src/main/ssh/relay-socket-path-limit-shell.integration.test.ts": 107,
@@ -5378,7 +5378,7 @@
"src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts": 2390,
"src/main/ssh/remote-node-runtime-store-windows.test.ts": 12643,
"src/main/ssh/removed-ssh-target-tombstone-retention.test.ts": 54,
"src/main/ssh/runtime-archive-download.test.ts": 99,
"src/main/node-runtime/runtime-archive-download.test.ts": 99,
"src/main/ssh/runtime-ssh-access-verification.test.ts": 228,
"src/main/ssh/runtime-ssh-access.test.ts": 548,
"src/main/ssh/sftp-namespace-resolution.test.ts": 91,
+8 -1
View File
@@ -17,7 +17,14 @@ const BUILD_PREFIXES = [
'.github/actions/prepare-orcad-prebuilds/'
]
// A remote target's OS does not identify the client platform that builds its commands.
const CROSS_HOST_PREFIXES = ['src/main/ssh/', 'src/main/providers/', 'src/wsl-guest/']
const CROSS_HOST_PREFIXES = [
'src/main/ssh/',
'src/main/node-runtime/',
'src/main/orcad/orcad-artifact-materializer.ts',
'src/main/orcad/orcad-host-unavailable.ts',
'src/main/providers/',
'src/wsl-guest/'
]
const PLATFORM_PREFIXES = [
'src/main/persistence/',
'src/main/sqlite/',
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* The orcad deployment template inside desktop builds (design D2): JS plus every target's
* addons, never a Node runtime. SSH relays and managed orcad deploys materialize a target's
* slot from it (src/main/ssh/orcad-artifact-materializer.ts, `process.resourcesPath`).
* slot from it (src/main/orcad/orcad-artifact-materializer.ts, `process.resourcesPath`).
*
* node config/scripts/packaged-orcad-template.cjs --reseal-signed <appDir> <signedListFile>
*
@@ -24,7 +24,7 @@ vi.mock('../wsl/wsl-runner', () => ({ runWslProcess: mocks.run }))
vi.mock('../wsl-running-path-filter', () => ({
filterPathsToRunningWslDistrosAsync: mocks.running
}))
vi.mock('../ssh/pinned-runtime-materializer', () => ({
vi.mock('../node-runtime/pinned-runtime-materializer', () => ({
materializeNodeRuntimeArchive: mocks.download
}))
vi.mock('../ssh/relay-bundle-paths', () => ({ relayBundleCandidates: mocks.bundles }))
@@ -8,7 +8,7 @@ import { OPENCODE_SQLITE_READER_FILENAME } from '../../shared/remote-bundle-arti
import { parseWslUncPath, toWindowsWslUncPath } from '../../shared/wsl-paths'
import { NODE_SQLITE_READER_API_SOURCE } from '../sqlite/node-sqlite-reader-api'
import { relayBundleCandidates } from '../ssh/relay-bundle-paths'
import { materializeNodeRuntimeArchive } from '../ssh/pinned-runtime-materializer'
import { materializeNodeRuntimeArchive } from '../node-runtime/pinned-runtime-materializer'
import { parseOrcadLinuxLibc } from '../ssh/orcad-deployment-target'
import {
installNodeRuntimeFromHostArchiveCommand,
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { SshTarget } from '../../shared/ssh-types'
import { closeTestStores, createSqliteTestStore } from '../persistence-test-harness'
import { Store } from '../persistence/loading-store/store'
import { OrcadHostUnsupportedError } from '../ssh/orcad-host-unavailable'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import { allowsDirectSshRelay, SshConnectionStore } from '../ssh/ssh-connection-store'
import { resolveHostServerOnConnect } from '../ssh/ssh-host-server-on-connect'
@@ -30,7 +30,7 @@ vi.mock('../ssh/orcad-runtime-conversion', () => ({
convertSshTargetToManagedOrcad: mocks.convert
}))
vi.mock('../ssh/orcad-managed-tunnel', () => ({ ensureOrcadManagedTunnel: mocks.tunnel }))
vi.mock('../ssh/orcad-artifact-materializer', () => ({ hasOrcadTemplate: () => true }))
vi.mock('../orcad/orcad-artifact-materializer', () => ({ hasOrcadTemplate: () => true }))
vi.mock('./ssh-renderer-broadcast', () => ({ broadcastSshState: mocks.broadcast }))
vi.mock('./ssh-ipc-context', () => ({ getCurrentMainWindow: () => null }))
vi.mock('./ssh-session-teardown', () => ({ disconnectRegisteredSshTarget: vi.fn() }))
@@ -8,7 +8,7 @@ import { listEnvironments } from '../../shared/runtime-environment-store'
import { findOrcadMigrationSourceCutoverForTarget } from '../ssh/orcad-migration-cutover-journal'
import { abandonOrcadConversion } from '../ssh/orcad-conversion-abandon'
import { retainOrcadMigrationSource } from '../ssh/orcad-migration-source-retention'
import { hasOrcadTemplate } from '../ssh/orcad-artifact-materializer'
import { hasOrcadTemplate } from '../orcad/orcad-artifact-materializer'
import { managedServerUpdateDeps } from '../ssh/managed-server-update-deps'
import { ensureOrcadManagedTunnel } from '../ssh/orcad-managed-tunnel'
import { verifyOrcadManagedServing } from '../ssh/orcad-managed-serving-verify'
@@ -0,0 +1,97 @@
/**
* Diagnose a host's C/C++ build toolchain from a POSIX-sh probe. Pure: it builds a shell
* command and parses its output, so the Node-only orcad bundle runs it over a local shell.
*/
// Why: node-pty@1.1.0 ships no Linux prebuild, so the remote `npm install` falls
// back to `node-gyp rebuild` and needs a C/C++ toolchain. A missing toolchain is
// the dominant first-connect failure on Linux relays (#1693); node-gyp surfaces
// it as an opaque `not found: make`. We probe for the tools so we can replace
// that with an actionable "install build-essential" message.
const PROBED_TOOLS = [
'make',
'gcc',
'g++',
'cc',
'c++',
'clang',
'clang++',
'python3',
'python'
] as const
// Package managers mapped to the one-liner that installs a C/C++ toolchain on
// the matching distro family. Ordered by detection priority.
const PACKAGE_MANAGER_HINTS: readonly { bin: string; install: string }[] = [
{ bin: 'apt-get', install: 'sudo apt-get install -y build-essential python3' },
{ bin: 'dnf', install: 'sudo dnf install -y make gcc gcc-c++ python3' },
{ bin: 'yum', install: 'sudo yum install -y make gcc gcc-c++ python3' },
{ bin: 'pacman', install: 'sudo pacman -S --needed base-devel python' },
{ bin: 'apk', install: 'sudo apk add build-base python3' },
{ bin: 'zypper', install: 'sudo zypper install -y gcc gcc-c++ make python3' }
]
type BuildToolchainStatus = {
present: string[]
packageManager: string | null
// node-gyp needs make, Python, and a C++ compiler. The caller only uses this
// verdict after npm/node-gyp output already points at a native-build failure,
// so custom Python paths do not make unrelated npm failures look toolchainy.
toolchainMissing: boolean
}
function hasCxxCompiler(present: ReadonlySet<string>): boolean {
return present.has('g++') || present.has('c++') || present.has('clang++')
}
function hasPython(present: ReadonlySet<string>): boolean {
return present.has('python3') || present.has('python')
}
// POSIX-sh probe: echo a `HAVE <tool>` line per resolvable build tool and a
// single `PKG <manager>` line for the host's package manager. Runs under
// `/bin/sh -c` (see wrapRemoteCommandForPosixShell), so it stays portable.
export function buildToolchainProbeCommand(): string {
const toolLoop = `for t in ${PROBED_TOOLS.join(
' '
)}; do if command -v "$t" >/dev/null 2>&1; then echo "HAVE $t"; fi; done`
const pkgList = PACKAGE_MANAGER_HINTS.map((hint) => hint.bin).join(' ')
const pkgLoop = `for p in ${pkgList}; do if command -v "$p" >/dev/null 2>&1; then echo "PKG $p"; break; fi; done`
return `${toolLoop}; ${pkgLoop}`
}
export function parseBuildToolchainProbe(output: string): BuildToolchainStatus {
const present = new Set<string>()
let packageManager: string | null = null
for (const line of output.split('\n')) {
const haveMatch = line.trim().match(/^HAVE (\S+)$/)
if (haveMatch) {
present.add(haveMatch[1])
continue
}
const pkgMatch = line.trim().match(/^PKG (\S+)$/)
if (pkgMatch && !packageManager) {
packageManager = pkgMatch[1]
}
}
return {
present: PROBED_TOOLS.filter((tool) => present.has(tool)),
packageManager,
toolchainMissing: !present.has('make') || !hasCxxCompiler(present) || !hasPython(present)
}
}
/** Install hint for the host's package manager, or the cross-distro list when it is unknown. */
export function toolchainInstallHintLines(status: BuildToolchainStatus): string[] {
const tailored = status.packageManager
? PACKAGE_MANAGER_HINTS.find((hint) => hint.bin === status.packageManager)?.install
: null
if (tailored) {
return [` ${tailored}`]
}
return [
' Debian/Ubuntu: sudo apt-get install -y build-essential python3',
' Fedora/RHEL: sudo dnf install -y make gcc gcc-c++ python3',
' Arch: sudo pacman -S --needed base-devel python',
' Alpine: sudo apk add build-base python3'
]
}
+1 -1
View File
@@ -25,7 +25,7 @@ import {
buildToolchainProbeCommand,
parseBuildToolchainProbe,
toolchainInstallHintLines
} from '../ssh/build-toolchain-diagnosis'
} from './build-toolchain-diagnosis'
import { detectNativeHostAbi, nativeSlotName, type NativeHostAbi } from './native-host-abi'
import { classifyNodePtyLoaderMessage, firstErrorLine } from './node-pty-loader-diagnosis'
import { installPrebuiltSlot, type PrebuiltSlotOutcome } from './node-pty-prebuilt-slot'
@@ -10,7 +10,7 @@ import { readdir, rm, stat } from 'node:fs/promises'
import { join } from 'node:path'
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 { materializedOrcadArtifactVersions } from './orcad-artifact-materializer'
import { isProcessAlive } from '../daemon/daemon-process-inspection'
import { liveDaemonOrcadSlots } from './orcad-daemon-slot-pins'
@@ -13,7 +13,7 @@ import {
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { pruneOrcadArtifactCache } from '../orcad/orcad-artifact-cache-retention'
import { pruneOrcadArtifactCache } from './orcad-artifact-cache-retention'
import { z } from 'zod'
import {
NODE_RUNTIME_ASSETS,
@@ -33,7 +33,7 @@ import {
orcadTemplateCommonFilenames,
orcadTemplateTargetFilenames
} from '../../shared/orcad-artifacts'
import { readOrcadArtifactIdentity } from '../orcad/orcad-artifact-identity'
import { readOrcadArtifactIdentity } from './orcad-artifact-identity'
import {
getAppEnvironment,
hasAppEnvironment,
@@ -17,12 +17,12 @@ import {
orcadTemplateTargetFilenames
} from '../../shared/orcad-artifacts'
import { pinnedNodeRuntimeAsset, type NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import { findOrcadCachePath } from './orcad-cache-path'
import { findOrcadCachePath } from '../node-runtime/orcad-cache-path'
import {
fileSha256,
verifyFileSha256,
type PinnedRuntimeMaterializeOptions
} from './pinned-runtime-materializer'
} from '../node-runtime/pinned-runtime-materializer'
import { OrcadArtifactsUnavailableError, OrcadHostUnsupportedError } from './orcad-host-unavailable'
const Sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u)
@@ -3,8 +3,8 @@ import {
classifyOrcadHostUnavailable,
orcadCandidateLaunchFailureCode
} from './orcad-host-unavailable'
import { OrcadWindowsLaunchRefusedError } from './orcad-remote-launch-windows'
import { OrcadWindowsCommandLineError } from './orcad-remote-windows-node'
import { OrcadWindowsLaunchRefusedError } from '../ssh/orcad-remote-launch-windows'
import { OrcadWindowsCommandLineError } from '../ssh/orcad-remote-windows-node'
describe('classifyOrcadHostUnavailable', () => {
it('sends a Windows host that cannot launch orcad back to the relay', () => {
@@ -29,8 +29,8 @@ import {
} from '../../shared/orcad-local-serve-selection'
import { resolveBundledOrcadRuntime } from './orcad-bundled-runtime'
import { detectNativeHostAbi, nativeSlotName } from './native-host-abi'
import { materializeOrcadArtifact } from '../ssh/orcad-artifact-materializer'
import { materializeCachedNodeRuntime } from '../ssh/pinned-runtime-materializer'
import { materializeOrcadArtifact } from './orcad-artifact-materializer'
import { materializeCachedNodeRuntime } from '../node-runtime/pinned-runtime-materializer'
import { serveProfileHasSshTargets } from './serve-profile-ssh-targets'
import { errorMessage } from '../../shared/error-message'
@@ -36,7 +36,7 @@ import {
} from '../../shared/remote-runtime-client'
import { claudeSessionIdForOrcaSession } from '../claude/claude-structured-session-id'
import { shellEscape } from '../ssh/ssh-connection-utils'
import { materializeOrcadArtifact } from '../ssh/orcad-artifact-materializer'
import { materializeOrcadArtifact } from './orcad-artifact-materializer'
import {
hostServerTarget,
installPackagedOrcadSlotForTests,
-228
View File
@@ -1,228 +0,0 @@
/**
* Diagnose a host's C/C++ build toolchain from a POSIX-sh probe.
*
* Why this is separate from `ssh-relay-build-toolchain.ts`: everything here is pure —
* it builds a shell command, parses its output, and formats messages. The relay reaches
* it over SSH; `orcad` reaches it over a local shell. Keeping it transport-free is what
* lets the Node-only bundle reuse it without pulling `ssh2` in behind it.
*/
// Why: node-pty@1.1.0 ships no Linux prebuild, so the remote `npm install` falls
// back to `node-gyp rebuild` and needs a C/C++ toolchain. A missing toolchain is
// the dominant first-connect failure on Linux relays (#1693); node-gyp surfaces
// it as an opaque `not found: make`. We probe for the tools so we can replace
// that with an actionable "install build-essential" message.
const PROBED_TOOLS = [
'make',
'gcc',
'g++',
'cc',
'c++',
'clang',
'clang++',
'python3',
'python'
] as const
// Package managers mapped to the one-liner that installs a C/C++ toolchain on
// the matching distro family. Ordered by detection priority.
const PACKAGE_MANAGER_HINTS: readonly { bin: string; install: string }[] = [
{ bin: 'apt-get', install: 'sudo apt-get install -y build-essential python3' },
{ bin: 'dnf', install: 'sudo dnf install -y make gcc gcc-c++ python3' },
{ bin: 'yum', install: 'sudo yum install -y make gcc gcc-c++ python3' },
{ bin: 'pacman', install: 'sudo pacman -S --needed base-devel python' },
{ bin: 'apk', install: 'sudo apk add build-base python3' },
{ bin: 'zypper', install: 'sudo zypper install -y gcc gcc-c++ make python3' }
]
export type BuildToolchainStatus = {
present: string[]
packageManager: string | null
// node-gyp needs make, Python, and a C++ compiler. The caller only uses this
// verdict after npm/node-gyp output already points at a native-build failure,
// so custom Python paths do not make unrelated npm failures look toolchainy.
toolchainMissing: boolean
}
function hasCxxCompiler(present: ReadonlySet<string>): boolean {
return present.has('g++') || present.has('c++') || present.has('clang++')
}
function hasPython(present: ReadonlySet<string>): boolean {
return present.has('python3') || present.has('python')
}
// POSIX-sh probe: echo a `HAVE <tool>` line per resolvable build tool and a
// single `PKG <manager>` line for the host's package manager. Runs under
// `/bin/sh -c` (see wrapRemoteCommandForPosixShell), so it stays portable.
export function buildToolchainProbeCommand(): string {
const toolLoop = `for t in ${PROBED_TOOLS.join(
' '
)}; do if command -v "$t" >/dev/null 2>&1; then echo "HAVE $t"; fi; done`
const pkgList = PACKAGE_MANAGER_HINTS.map((hint) => hint.bin).join(' ')
const pkgLoop = `for p in ${pkgList}; do if command -v "$p" >/dev/null 2>&1; then echo "PKG $p"; break; fi; done`
return `${toolLoop}; ${pkgLoop}`
}
export function parseBuildToolchainProbe(output: string): BuildToolchainStatus {
const present = new Set<string>()
let packageManager: string | null = null
for (const line of output.split('\n')) {
const haveMatch = line.trim().match(/^HAVE (\S+)$/)
if (haveMatch) {
present.add(haveMatch[1])
continue
}
const pkgMatch = line.trim().match(/^PKG (\S+)$/)
if (pkgMatch && !packageManager) {
packageManager = pkgMatch[1]
}
}
return {
present: PROBED_TOOLS.filter((tool) => present.has(tool)),
packageManager,
toolchainMissing: !present.has('make') || !hasCxxCompiler(present) || !hasPython(present)
}
}
export function shouldProbeBuildToolchainAfterNativeDepsFailure(message: string): boolean {
const lower = message.toLowerCase()
if (!lower.includes('gyp') && !lower.includes('node-gyp')) {
return false
}
return (
/\bnot found:\s*(make|gmake|gcc|g\+\+|cc|c\+\+|clang|clang\+\+|python|python3)\b/i.test(
message
) ||
/\b(make|gmake|gcc|g\+\+|cc|c\+\+|clang|clang\+\+|python|python3)\b.*\bnot found\b/i.test(
message
) ||
lower.includes('could not find any python installation') ||
lower.includes('no xcode or clt version detected')
)
}
function missingToolNames(status: BuildToolchainStatus): string[] {
const present = new Set(status.present)
const missing: string[] = []
if (!present.has('make')) {
missing.push('make')
}
if (!hasCxxCompiler(present)) {
missing.push('a C++ compiler (g++ or clang++)')
}
if (!hasPython(present)) {
missing.push('python3')
}
return missing
}
/** Install hint for the host's package manager, or the cross-distro list when it is unknown. */
export function toolchainInstallHintLines(status: BuildToolchainStatus): string[] {
const tailored = status.packageManager
? PACKAGE_MANAGER_HINTS.find((hint) => hint.bin === status.packageManager)?.install
: null
if (tailored) {
return [` ${tailored}`]
}
return [
' Debian/Ubuntu: sudo apt-get install -y build-essential python3',
' Fedora/RHEL: sudo dnf install -y make gcc gcc-c++ python3',
' Arch: sudo pacman -S --needed base-devel python',
' Alpine: sudo apk add build-base python3'
]
}
/** One-line summary for the deploy log when node-pty is skipped rather than compiled. */
export function formatSkippedNodePtyWarning(status: BuildToolchainStatus): string {
// Why: with no package manager detected the hint list is the cross-distro menu, whose first line
// is Debian's — quoting it alone would name the wrong distro, so stay neutral instead.
const hintLines = toolchainInstallHintLines(status)
const hint =
hintLines.length === 1
? hintLines[0].trim()
: 'install a C/C++ toolchain (make, a C++ compiler, python3)'
return (
`missing build tools (${missingToolNames(status).join(', ')}); skipping node-pty so the ` +
`connection still serves files and git. Remote terminals need: ${hint}`
)
}
export function formatMissingToolchainError(
status: BuildToolchainStatus,
underlyingError: string
): string {
const lines = [
`The remote host is missing the C/C++ build tools (${missingToolNames(status).join(', ')}) ` +
`needed to compile Orca's relay native modules (node-pty, @parcel/watcher). node-pty has no ` +
`prebuilt binary for Linux, so they must be compiled on the remote host.`,
'',
'Install the build tools on the remote host, then reconnect:',
...toolchainInstallHintLines(status),
'',
`Underlying install error: ${underlyingError}`
]
return lines.join('\n')
}
const NODE_HEADERS_TARBALL_RE = /node-v[0-9.]+-headers\.tar\.gz/i
/**
* Whether a native-deps failure is node-gyp failing to download Node headers from nodejs.org.
*
* Why it needs naming: the raw output is forty lines of `gyp http` and stack frames around one
* `ECONNREFUSED`, and it reads as a broken host or a broken Orca. Which of two things it is
* depends on what the local-headers export found first, so the formatter takes that answer.
*/
export function isNodeHeadersDownloadFailure(message: string): boolean {
// Why `configure error` is required: node-gyp's fetch client logs `attempt N failed with <code>`
// on retries it then recovers from, so a network token alone also matches a build that got its
// headers and died later for an unrelated reason. Only the configure step downloads headers.
return (
/gyp ERR! configure error/i.test(message) &&
NODE_HEADERS_TARBALL_RE.test(message) &&
/\b(ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|ECONNRESET)\b/.test(
message
)
)
}
const NODE_HEADERS_CONTEXT =
'node-pty has no prebuilt binary for Linux, so it must be compiled on the remote host, and ' +
'node-gyp fetches the Node.js headers from nodejs.org unless the Node install provides them ' +
'at <prefix>/include/node.'
/**
* @param localHeadersDir what the local-headers export found: a dir it exported, `null` when
* the host's Node ships no matching headers, `undefined` when the answer never came back.
*
* Why the exported-dir case is its own message: the export is the fix, so node-gyp downloading
* anyway means its `nodedir` env keys were not honoured (a future npm dropping the passthrough,
* a wrapper scrubbing the env). That is an Orca defect, not a host problem, and must not be
* reported as one -- it names the dir so the report is checkable.
*/
export function formatNodeHeadersDownloadError(
underlyingError: string,
localHeadersDir: string | null | undefined
): string {
const lines = localHeadersDir
? [
`The remote host could not download the Node.js headers needed to compile node-pty, even ` +
`though its Node install ships matching headers at ${localHeadersDir}/include/node and ` +
`Orca pointed node-gyp at them. node-gyp ignored that setting; this is an Orca defect, ` +
`please report it with the log below.`,
'',
'Workaround on the remote host until then: allow outbound HTTPS to nodejs.org, or point ' +
'npm at a mirror: npm config set disturl https://<mirror>/dist'
]
: [
'The remote host could not download the Node.js headers needed to compile node-pty, and ' +
`its Node install has no local headers matching its own version. ${NODE_HEADERS_CONTEXT}`,
'',
'Fix one of the following on the remote host, then reconnect:',
' - Install Node.js from an official build or a version manager (nvm, fnm, volta, n), ' +
'which ship headers for exactly the Node they run; or',
' - Allow outbound HTTPS to nodejs.org, or point npm at a mirror: ' +
'npm config set disturl https://<mirror>/dist'
]
return [...lines, '', `Underlying install error: ${underlyingError}`].join('\n')
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { evaluateOrcadActivation } from './orcad-activation-gate'
import { classifyOrcadHostUnavailable } from './orcad-host-unavailable'
import { classifyOrcadHostUnavailable } from '../orcad/orcad-host-unavailable'
import type { ServeReadiness } from '../server/serve-readiness'
import type { OrcadHealth, TerminalDaemonHealth } from '../orcad/orcad-health'
+1 -1
View File
@@ -57,7 +57,7 @@ import {
stopTransactionIncumbent
} from './orcad-transaction-incumbent'
import { withOrcadLogTail } from './orcad-remote-log-tail'
import { orcadCandidateLaunchFailureCode } from './orcad-host-unavailable'
import { orcadCandidateLaunchFailureCode } from '../orcad/orcad-host-unavailable'
import { errorMessage } from '../../shared/error-message'
type Outcome = Extract<OrcadDeployResult, { outcome: 'installed-not-activated' }>
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { emptyOrcadActivationRecord, type OrcadActivationRecord } from './orcad-activation-record'
import { OrcadHostUnsupportedError } from './orcad-host-unavailable'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
const mocks = vi.hoisted(() => {
const state: { record: OrcadActivationRecord | null } = { record: null }
@@ -29,7 +29,9 @@ vi.mock('./orcad-managed-runtime-context', () => ({
vi.mock('./orcad-managed-migration-status', () => ({
findIncompleteManagedOrcadMigration: mocks.migrating
}))
vi.mock('./orcad-artifact-materializer', () => ({ materializeOrcadArtifact: mocks.materialize }))
vi.mock('../orcad/orcad-artifact-materializer', () => ({
materializeOrcadArtifact: mocks.materialize
}))
vi.mock('./remote-versioned-install', () => ({ readLocalFullVersion: () => '0.1.0+new' }))
import {
+5 -2
View File
@@ -10,8 +10,11 @@ import {
refuseOrcadHostDowngrade,
type OrcadHostVersionRefusal
} from './orcad-host-version-admission'
import { materializeOrcadArtifact } from './orcad-artifact-materializer'
import { OrcadArtifactsUnavailableError, OrcadHostUnsupportedError } from './orcad-host-unavailable'
import { materializeOrcadArtifact } from '../orcad/orcad-artifact-materializer'
import {
OrcadArtifactsUnavailableError,
OrcadHostUnsupportedError
} from '../orcad/orcad-host-unavailable'
import { findIncompleteManagedOrcadMigration } from './orcad-managed-migration-status'
import { ORCAD_ACTIVATION_FENCE_BUSY_CODE } from './orcad-activation-fence-hold'
import { resolveLinkedOrcadContext } from './orcad-managed-runtime-context'
+1 -1
View File
@@ -16,7 +16,7 @@ import {
type RemoteHostPlatform
} from './ssh-remote-platform'
import { detectRemoteHostPlatform } from './ssh-remote-platform-detection'
import { OrcadHostUnsupportedError } from './orcad-host-unavailable'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import { rememberSshHostPlatform } from './ssh-host-platform-memo'
export type OrcadRemoteContext = {
+2 -2
View File
@@ -20,10 +20,10 @@ import { installOrcadBundle } from './orcad-remote-install'
import { getAppEnvironment } from '../../shared/app-environment'
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 { materializeOrcadArtifact } from '../orcad/orcad-artifact-materializer'
import { readOrcadBundleTarget } from './orcad-deployment-target'
import { resolveOrcadRuntimeTarget } from './orcad-runtime-target'
import { materializeNodeRuntimeArchive } from './pinned-runtime-materializer'
import { materializeNodeRuntimeArchive } from '../node-runtime/pinned-runtime-materializer'
import {
orcadActivationFenceExists,
resolveOrcadActivationReadinessTimeout,
+1 -1
View File
@@ -37,7 +37,7 @@ import {
ORCAD_WINDOWS_PROCESS_FILENAME
} from './orcad-remote-host-support'
import { orcadManagedLaunchEnv, type OrcadLaunchSpec } from './orcad-remote-launch'
import { ORCAD_WINDOWS_LAUNCH_REFUSED_CODE } from './orcad-host-unavailable'
import { ORCAD_WINDOWS_LAUNCH_REFUSED_CODE } from '../orcad/orcad-host-unavailable'
export class OrcadWindowsLaunchRefusedError extends Error {
readonly code = ORCAD_WINDOWS_LAUNCH_REFUSED_CODE
+1 -1
View File
@@ -23,7 +23,7 @@ import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exe
import { RELAY_REMOTE_DIR } from './relay-protocol'
import { joinRemotePath, remoteDirname, type RemoteHostPlatform } from './ssh-remote-platform'
import { powerShellLiteral } from './ssh-remote-powershell'
import { ORCAD_WINDOWS_COMMAND_LINE_UNSAFE_CODE } from './orcad-host-unavailable'
import { ORCAD_WINDOWS_COMMAND_LINE_UNSAFE_CODE } from '../orcad/orcad-host-unavailable'
import {
ORCAD_WINDOWS_HOST_SCRIPT,
ORCAD_WINDOWS_HOST_SCRIPT_FILENAME,
@@ -49,7 +49,7 @@ vi.mock('./orcad-activation-transaction-store', () => ({
readOrcadActivationTransaction: mocks.readTransaction
}))
vi.mock('./orcad-remote-deploy', () => ({ deployOrcad: mocks.deploy }))
vi.mock('./orcad-artifact-materializer', () => ({
vi.mock('../orcad/orcad-artifact-materializer', () => ({
materializeOrcadArtifact: async () => '/local/orcad'
}))
vi.mock('./orcad-local-build-hash', () => ({ computeLocalOrcadBuildHash: () => 'build-hash' }))
+1 -1
View File
@@ -15,7 +15,7 @@ import {
} from '../../shared/orcad-managed-runtime'
import { getManagedOrcadFenceEnvironmentId } from '../../shared/managed-orcad-ssh-owner'
import { runTargetLifecycle } from '../ipc/ssh-target-lifecycle-queue'
import { materializeOrcadArtifact } from './orcad-artifact-materializer'
import { materializeOrcadArtifact } from '../orcad/orcad-artifact-materializer'
import {
closeOrcadManagedTunnel,
ensureOrcadManagedTunnel,
@@ -38,7 +38,7 @@ vi.mock('./orcad-remote-rollback', () => ({ rollbackOrcad: mocks.rollback }))
vi.mock('./orcad-activation-recovery', () => ({ recoverInterruptedOrcadActivation: mocks.recover }))
vi.mock('./orcad-active-readiness', () => ({ probeActiveOrcadReadiness: mocks.probe }))
vi.mock('./orcad-remote-build-hash', () => ({ readRemoteOrcadBuildHash: mocks.buildHash }))
vi.mock('./orcad-artifact-materializer', () => ({
vi.mock('../orcad/orcad-artifact-materializer', () => ({
materializeOrcadArtifact: async () => '/local/orcad'
}))
vi.mock('./orcad-local-build-hash', () => ({ computeLocalOrcadBuildHash: () => 'local-hash' }))
+1 -1
View File
@@ -19,7 +19,7 @@ import { runTargetLifecycle } from '../ipc/ssh-target-lifecycle-queue'
import type { ServeReadiness } from '../server/serve-readiness'
import { recoverInterruptedOrcadActivation } from './orcad-activation-recovery'
import { probeActiveOrcadReadiness } from './orcad-active-readiness'
import { materializeOrcadArtifact } from './orcad-artifact-materializer'
import { materializeOrcadArtifact } from '../orcad/orcad-artifact-materializer'
import { CURRENT_ORCAD_DAEMON_PROTOCOL } from './orcad-daemon-protocol-crossing'
import { ensureOrcadManagedTunnel } from './orcad-managed-tunnel'
import {
+1 -1
View File
@@ -1,6 +1,6 @@
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import { resolveOrcadDeploymentTargetFacts } from './orcad-deployment-target'
import { OrcadHostUnsupportedError } from './orcad-host-unavailable'
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
import type { OrcadHostTransport } from './orcad-host-transport'
import { pinnedRuntimeTargetForHost } from './pinned-runtime-host-target'
import type { RemoteHostPlatform } from './ssh-remote-platform'
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PassThrough } from 'node:stream'
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { OrcadStdioBridgeUnavailableError } from './orcad-host-unavailable'
import { OrcadStdioBridgeUnavailableError } from '../orcad/orcad-host-unavailable'
import type { OrcadStdioBridge } from './orcad-stdio-bridge'
import { ORCAD_STDIO_BRIDGE_MAX_CHANNELS } from './orcad-stdio-bridge-listener'
import { OrcadStdioBridgePortForwardProvider } from './orcad-stdio-bridge-provider'
+1 -1
View File
@@ -6,7 +6,7 @@
* Disconnects match the forward: a lost channel drops its socket and the listener stays, so the
* tunnel's own reconnect logic decides, and a lost transport never reads as a dead server.
*/
import { OrcadStdioBridgeUnavailableError } from './orcad-host-unavailable'
import { OrcadStdioBridgeUnavailableError } from '../orcad/orcad-host-unavailable'
import type { OrcadHostTransport } from './orcad-host-transport'
import {
checkOrcadStdioBridge,
+1 -1
View File
@@ -5,7 +5,7 @@ import type { Server } from 'node:net'
import type { EventEmitter } from 'node:events'
import { PassThrough } from 'node:stream'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { OrcadStdioBridgeUnavailableError } from './orcad-host-unavailable'
import { OrcadStdioBridgeUnavailableError } from '../orcad/orcad-host-unavailable'
import { checkOrcadStdioBridge, type OrcadStdioBridge } from './orcad-stdio-bridge'
import {
orcadPosixStdioBridgeCommand,
+1 -1
View File
@@ -2,7 +2,7 @@
import {
OrcadHostUnsupportedError,
OrcadStdioBridgeUnavailableError
} from './orcad-host-unavailable'
} from '../orcad/orcad-host-unavailable'
import { resolveOrcadRemoteContext } from './orcad-remote-context'
import { orcadWindowsHomeOpCommand } from './orcad-remote-windows-node'
import {
@@ -10,7 +10,7 @@ import { getAppEnvironment } from '../../shared/app-environment'
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'
import { materializeNodeRuntimeArchive } from '../node-runtime/pinned-runtime-materializer'
import type { OrcadHostTransport } from './orcad-host-transport'
import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform'
@@ -8,7 +8,7 @@ import type {
} from '../../shared/orcad-managed-runtime'
import type { SshTarget } from '../../shared/ssh-types'
import type { HostServerOnConnectResult } from './ssh-host-server-on-connect'
import { classifyOrcadHostUnavailable } from './orcad-host-unavailable'
import { classifyOrcadHostUnavailable } from '../orcad/orcad-host-unavailable'
export type HostServerConnectEvent =
| {
@@ -3,7 +3,7 @@ import type { SshTarget } from '../../shared/ssh-types'
import {
OrcadHostUnsupportedError,
OrcadStdioBridgeUnavailableError
} from './orcad-host-unavailable'
} from '../orcad/orcad-host-unavailable'
import type { ManagedOrcadAutoUpdateOutcome } from './orcad-managed-auto-update'
import type { HostServerConnectEvent } from './ssh-host-server-connect-events'
import {
@@ -3,7 +3,7 @@ import type { SshTarget } from '../../shared/ssh-types'
import {
OrcadHostUnsupportedError,
OrcadStdioBridgeUnavailableError
} from './orcad-host-unavailable'
} from '../orcad/orcad-host-unavailable'
import {
resolveHostServerOnConnect,
type HostServerOnConnectDeps
+1 -1
View File
@@ -28,7 +28,7 @@ import { managedResultAfterUpdate, unservedManagedResult } from './managed-serve
import {
classifyOrcadHostUnavailable,
ORCAD_TUNNEL_UNAVAILABLE_REASON
} from './orcad-host-unavailable'
} from '../orcad/orcad-host-unavailable'
import {
decidedEvent,
reportHostServerConversion,
+1 -1
View File
@@ -4,7 +4,7 @@
* client-side deploy against each one; `.github/workflows/ssh-hostile-hosts.yml` runs it.
*/
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import type { OrcadHostUnavailableReason } from './orcad-host-unavailable'
import type { OrcadHostUnavailableReason } from '../orcad/orcad-host-unavailable'
/** Every shimmed toolchain command appends its name here; a managed deploy must leave it empty. */
export const FORBIDDEN_TOOL_LOG = '/tmp/orca-forbidden-tool-calls.log'
@@ -10,7 +10,7 @@ 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 { classifyOrcadHostUnavailable } from '../orcad/orcad-host-unavailable'
import { managedOrcadSlot } from './orcad-managed-runtime-context'
import { orcadSlotDir, type OrcadSlotOptions } from './orcad-recovery-slot'
import { resolveOrcadRemoteContext } from './orcad-remote-context'
+1 -1
View File
@@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({
'C:/cache/pinned.tar.gz'
)
}))
vi.mock('../ssh/pinned-runtime-materializer', () => ({
vi.mock('../node-runtime/pinned-runtime-materializer', () => ({
materializeNodeRuntimeArchive: mocks.download
}))
function runner(present = false, libc = 'glibc 2.31', promoted = 'ORCA_NODE_RUNTIME_READY') {
+1 -1
View File
@@ -1,7 +1,7 @@
import { randomBytes } from 'node:crypto'
import { basename } from 'node:path'
import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
import { materializeNodeRuntimeArchive } from '../ssh/pinned-runtime-materializer'
import { materializeNodeRuntimeArchive } from '../node-runtime/pinned-runtime-materializer'
import { parseGlibcVersion, parseOrcadLinuxLibc } from '../ssh/orcad-deployment-target'
import {
installNodeRuntimeFromHostArchiveCommand,