Files
orca/src/main/ssh/ssh-exec-stdin-file-transfer.ts
T
NeilandClaude 6328a104bd refactor(ssh): split ssh-connection-utils into concretely named modules (#30488)
* refactor(ssh): split ssh-connection-utils into concretely named modules

- classifiers -> ssh-reconnect-error-classification.ts
- retry/timeout constants and sleep -> connection/ssh-connection-timing.ts
- buildConnectConfig -> connection/ssh-connect-config.ts
- credential callbacks -> connection/ssh-connection-callbacks.ts
- exec options and abort error -> connection/ssh-exec-options.ts
- wrapRemoteCommandForPosixShell -> ssh-posix-command-wrapper.ts
- shellEscape and shared/wsl-login-shell-command quotePosixShell (identical) -> shared/shell-argument-quoting.ts quotePosixShell

No re-exports are left behind.

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

* fix: point the WSL git benchmark and the WSL lane route at shell-argument-quoting

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 20:48:05 -07:00

132 lines
5.0 KiB
TypeScript

/**
* Upload by streaming bytes into a POSIX exec channel's stdin (design D5 transfer fallback), for
* hosts whose SFTP subsystem is disabled or chrooted (#12868, #15479). Works on either transport
* because it only needs `conn.exec`; the system-SSH transport already writes this way.
*
* Each file lands under a unique partial name and is renamed only after the host counts exactly
* the bytes that were sent, so a transfer cut short never leaves a plausible-looking file behind.
*/
import { randomBytes } from 'node:crypto'
import { constants } from 'node:fs'
import { lstat, open } from 'node:fs/promises'
import { posix } from 'node:path'
import { Readable } from 'node:stream'
import type { RemoteInstallTransport } from '../managed-server/install/remote-install-transport'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { execCommand } from './remote-exec/remote-exec-command'
import { RELAY_DEPLOY_TIMEOUT_MS } from '../managed-server/install/remote-deploy-timing'
import type { RemoteHostPlatform } from './ssh-remote-platform'
import { makePosixWriteFileCommand } from './system-transport/system-ssh-file-binary-transfer'
import { collectLocalUploadPlan } from './system-transport/system-ssh-file-transfer'
export const EXEC_STDIN_WRITE_DONE = 'ORCA_EXEC_STDIN_WRITE_DONE'
type ExecStdinOptions = { signal?: AbortSignal }
/** `cat` into a partial file, check its size, then rename it into place. */
export function makeExecStdinWriteFileCommand(
remotePath: string,
byteLength: number,
options: { token: string; executable?: boolean }
): string {
if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
throw new Error(`Invalid upload size: ${byteLength}`)
}
const part = `${remotePath}.orca-part-${options.token}`
const escapedPart = quotePosixShell(part)
const steps = [
`mkdir -p ${quotePosixShell(posix.dirname(remotePath))}`,
makePosixWriteFileCommand(part),
// Why tr: BSD `wc -c` pads its count with spaces.
`[ "$(wc -c < ${escapedPart} | tr -d ' ')" = ${byteLength} ]`,
...(options.executable ? [`chmod 755 ${escapedPart}`] : []),
`mv -f ${escapedPart} ${quotePosixShell(remotePath)}`,
`echo ${EXEC_STDIN_WRITE_DONE}`
]
return `{ ${steps.join(' && ')}; } || { rm -f ${escapedPart}; exit 1; }`
}
async function writeViaExecStdin(
conn: RemoteInstallTransport,
remotePath: string,
byteLength: number,
stdin: Readable,
executable: boolean,
options?: ExecStdinOptions
): Promise<void> {
const token = randomBytes(8).toString('hex')
const output = await execCommand(
conn,
makeExecStdinWriteFileCommand(remotePath, byteLength, { token, executable }),
// Why the deploy bound: a runtime archive over a slow link outlasts the 30 s probe default.
{ signal: options?.signal, stdin, timeoutMs: RELAY_DEPLOY_TIMEOUT_MS }
)
if (output.trim().split('\n').at(-1) !== EXEC_STDIN_WRITE_DONE) {
throw new Error(`The host did not confirm the upload of ${remotePath}`)
}
}
export async function uploadFileViaExecStdin(
conn: RemoteInstallTransport,
localPath: string,
remotePath: string,
options?: ExecStdinOptions
): Promise<void> {
options?.signal?.throwIfAborted()
const sourceStat = await lstat(localPath)
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
throw new Error(`Unsupported upload source: ${localPath}`)
}
const handle = await open(localPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0))
try {
const opened = await handle.stat()
// Same identity check as the system-SSH writer; some Windows filesystems report inode 0.
if (
!opened.isFile() ||
opened.size !== sourceStat.size ||
(sourceStat.ino !== 0 && opened.ino !== 0 && opened.ino !== sourceStat.ino)
) {
throw new Error(`File changed during upload: ${localPath}`)
}
const input = handle.createReadStream({ autoClose: false })
await writeViaExecStdin(
conn,
remotePath,
opened.size,
input,
(opened.mode & 0o111) !== 0,
options
)
} finally {
await handle.close()
}
}
export async function writeStringViaExecStdin(
conn: RemoteInstallTransport,
remotePath: string,
contents: string,
options?: ExecStdinOptions
): Promise<void> {
const bytes = Buffer.from(contents, 'utf-8')
await writeViaExecStdin(conn, remotePath, bytes.length, Readable.from([bytes]), false, options)
}
/** One exec creates the tree (empty directories included), then one exec per file. */
export async function uploadDirectoryViaExecStdin(
conn: RemoteInstallTransport,
localDir: string,
remoteDir: string,
host: RemoteHostPlatform,
options?: ExecStdinOptions
): Promise<void> {
const plan = await collectLocalUploadPlan(localDir, remoteDir, host, options?.signal)
await execCommand(conn, `mkdir -p ${plan.directories.map(quotePosixShell).join(' ')}`, {
signal: options?.signal
})
for (const file of plan.files) {
options?.signal?.throwIfAborted()
await uploadFileViaExecStdin(conn, file.localPath, file.remotePath, options)
}
}