Files
orca/src/main/ssh/ssh-remote-node-resolution.ts
T
98d02bca47 fix: support windows ssh hosts (#5004)
* feat: add windows ssh relay base support

* feat: support windows ssh relay runtime services

* fix: default windows ssh pty cwd to user profile

* fix: support windows hosts over system ssh

* fix: preserve degraded windows relay native deps

* fix: gate windows shell args by relay platform

* fix: preserve windows relay fallback pipes

* test: align windows native deps relay fixture

* fix: build valid windows install lock command

* fix: address windows SSH relay review findings

Resolve correctness, efficiency, and reuse issues found reviewing the
Windows SSH native-host support:

- GC liveness on Windows now probes the actual named pipe (via node
  net.connect against markers + deterministic candidates) instead of
  substring-matching Win32_Process command lines, which could remove a
  live relay dir. Reports ALIVE conservatively only when there is no
  liveness signal at all (no markers and no seed pipes).
- Resolve the remote node path once per deploy and thread it through
  install/repair/launch instead of re-resolving 3-7x.
- Replace the 200ms node -e poll loop with a single long-lived remote
  wait process during Windows relay startup.
- Skip the no-op executable command on Windows in uploadRelay.
- Make the Windows fallback pipe name deterministic and recoverable
  (drop the global counter), with an extra reconnect attempt.
- Normalize the prepended node bin dir to backslashes on Windows PATH.
- Batch the system-SSH Windows directory upload into a single streamed
  JSON package instead of one ssh process per file.
- Extract relay endpoint/marker helpers into ssh-relay-endpoints.ts and
  consolidate the PowerShell EncodedCommand encoding into the shared
  powershell-command-encoding module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Support cancellation and timeouts in Windows port scanning

- Propagate the request AbortSignal and a 5-second timeout to both
  PowerShell and netstat child processes during Windows port scanning.
- Avoid spawning the netstat fallback process if the port scan has
  already been aborted.
- Wrap the .NET OSArchitecture check in a try/catch block during SSH
  Windows platform detection to robustly fall back to environment
  variables if needed.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-06-09 01:17:34 -07:00

99 lines
3.6 KiB
TypeScript

import type { SshConnection } from './ssh-connection'
import { execCommand } from './ssh-relay-deploy-helpers'
import type { RemoteHostPlatform } from './ssh-remote-platform'
import { isWindowsRemoteHost, normalizeWindowsRemotePath } from './ssh-remote-platform'
import { powerShellCommand } from './ssh-remote-powershell'
// Why: non-login SSH shells (the default for `exec`) don't source
// .bashrc/.zshrc, so node installed via nvm/fnm/Homebrew isn't in PATH.
// We try common locations and fall back to a login-shell `which`.
export async function resolveRemoteNodePath(
conn: SshConnection,
host?: RemoteHostPlatform
): Promise<string> {
if (host && isWindowsRemoteHost(host)) {
return resolveRemoteWindowsNodePath(conn)
}
const script = [
'command -v node 2>/dev/null',
'command -v /usr/local/bin/node 2>/dev/null',
'command -v /opt/homebrew/bin/node 2>/dev/null',
// Why: nvm installs into a versioned directory. `ls -1` sorts
// alphabetically, which misorders versions (e.g. v9 > v18). Pipe
// through `sort -V` (version sort) so we pick the highest version.
'ls -1 $HOME/.nvm/versions/node/*/bin/node 2>/dev/null | sort -V | tail -1',
'command -v $HOME/.local/bin/node 2>/dev/null',
'command -v $HOME/.fnm/aliases/default/bin/node 2>/dev/null'
].join(' || ')
try {
const result = await execCommand(conn, script)
const nodePath = result.trim().split('\n')[0]
if (nodePath) {
console.log(`[ssh-relay] Found node at: ${nodePath}`)
return nodePath
}
} catch {
// Fall through to login shell attempt
}
// Why: last resort — source the full login profile. This is separated into
// its own exec because `bash -lc` can hang on remotes with interactive
// shell configs (conda prompts, etc.). If this times out, the error message
// from execCommand will tell us it was the login shell attempt.
try {
console.log('[ssh-relay] Trying login shell to find node...')
const result = await execCommand(conn, "bash -lc 'command -v node' 2>/dev/null")
const nodePath = result.trim().split('\n')[0]
if (nodePath) {
console.log(`[ssh-relay] Found node via login shell: ${nodePath}`)
return nodePath
}
} catch {
// Fall through
}
throwNodeNotFound()
}
async function resolveRemoteWindowsNodePath(conn: SshConnection): Promise<string> {
const script = [
'$paths = @()',
'$cmd = Get-Command node.exe -ErrorAction SilentlyContinue',
'if ($cmd -and $cmd.Source) { $paths += $cmd.Source }',
'if ($env:ProgramFiles) { $paths += (Join-Path $env:ProgramFiles "nodejs/node.exe") }',
'if (${env:ProgramFiles(x86)}) { $paths += (Join-Path ${env:ProgramFiles(x86)} "nodejs/node.exe") }',
'if ($env:LOCALAPPDATA) { $paths += (Join-Path $env:LOCALAPPDATA "Programs/nodejs/node.exe") }',
'foreach ($path in $paths) {',
' if ($path -and (Test-Path -LiteralPath $path -PathType Leaf)) {',
' Write-Output $path',
' exit 0',
' }',
'}',
"Write-Error 'Node.js not found'",
'exit 1'
].join('\n')
try {
const result = await execCommand(conn, powerShellCommand(script), { wrapCommand: false })
const nodePath = result.trim().split('\n')[0]
if (nodePath) {
const normalized = normalizeWindowsRemotePath(nodePath)
console.log(`[ssh-relay] Found Windows node at: ${normalized}`)
return normalized
}
} catch {
// Fall through to the shared error below.
}
throwNodeNotFound()
}
function throwNodeNotFound(): never {
throw new Error(
'Node.js not found on remote host. Orca relay requires Node.js 18+. ' +
'Install Node.js on the remote and try again.'
)
}