mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
* fix(ssh): connect to Linux hosts that cannot compile node-pty node-pty ships no Linux prebuilt at any architecture, so it is compiled on the remote. On a host without a C/C++ toolchain that build fails, and because both native deps install in one npm command it also took down @parcel/watcher — which does have a working Linux prebuilt — and failed the whole connection. Every Linux image without build tools was unusable. node-pty only backs remote terminals; files, git, and the editor do not need it, and a missing native dep is already non-fatal further down the deploy. So when the existing toolchain probe confirms the compiler is missing, reinstall without node-pty instead of aborting. The manifest has to drop it too — npm reconciles every dependency in package.json, not just the ones named on the command line, so naming only @parcel/watcher still rebuilds node-pty. If that reinstall also fails the actionable build-tools error is rethrown, so a host broken for some other reason still reports the toolchain gap. The relay's PTY error now names the fix rather than saying only that node-pty is unavailable. Verified on a stock Rocky Linux 10.2 aarch64 container (openssh-server, git, nodejs, npm, no compiler): connect succeeds, /etc lists over SSH, node-pty is absent while @parcel/watcher installs its linux-arm64-glibc prebuilt, and spawning a terminal reports the install hint. * fix(ssh): keep the node-pty skip path honest about platform and watcher The PTY unavailable message named build tools unconditionally, but only Linux compiles node-pty — the deploy-side skip is gated on linux and the toolchain probe returns null on Windows. A Windows or macOS remote, where node-pty ships prebuilds, was told to install make/g++/python3. Pick the remedy by the relay's own platform. The skip path returned before the install probe, so a @parcel/watcher that installs but cannot require() (glibc below the floor) connected with dead file watching and nothing logged. Probe before returning and warn; no rebuild, since node-pty provably cannot compile on that host, and never fatal. Also log the pty-less reinstall's own failure and attach it as cause — the rethrown toolchain message is built from the original npm error, so an unrelated retry failure (registry, ENOSPC, EACCES) was lost. The reinstall now keeps the caller's resetDeps as well, so a repair reconnect still clears every dep the probe found broken. Tests: the skip-success fixture queued a chmod/probe/rebuild sequence production never runs, and the surplus slots were absorbed by launchRelay's readiness poll (1817ms vs 3-9ms for its peers). It now emits exactly the 12 execs production performs, and pins that no rebuild is issued. Adds the missing negative case: a gyp-shaped failure on a host whose probe reports a complete toolchain must still hard-fail rather than silently degrade. * fix(ssh): hedge the node-pty remedy and keep repair resets on the skip path
185 lines
6.8 KiB
TypeScript
185 lines
6.8 KiB
TypeScript
import { execCommand } from './ssh-relay-deploy-helpers'
|
|
import type { SshConnection } from './ssh-connection'
|
|
import { isWindowsRemoteHost, type RemoteHostPlatform } from './ssh-remote-platform'
|
|
|
|
// 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')
|
|
}
|
|
|
|
// Best-effort: returns null on Windows hosts (node-pty ships win32 prebuilds, so
|
|
// a missing toolchain isn't the failure there) or if the probe itself errors —
|
|
// callers fall back to the original install error in those cases.
|
|
export async function probeBuildToolchain(
|
|
conn: SshConnection,
|
|
hostPlatform: RemoteHostPlatform,
|
|
signal?: AbortSignal
|
|
): Promise<BuildToolchainStatus | null> {
|
|
if (isWindowsRemoteHost(hostPlatform)) {
|
|
return null
|
|
}
|
|
try {
|
|
const output = await execCommand(conn, buildToolchainProbeCommand(), {
|
|
wrapCommand: true,
|
|
signal
|
|
})
|
|
return parseBuildToolchainProbe(output)
|
|
} catch {
|
|
signal?.throwIfAborted()
|
|
return null
|
|
}
|
|
}
|