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.
This commit is contained in:
Neil
2026-10-21 13:54:26 -07:00
committed by GitHub
parent 0b82f88e9f
commit e7d77e863c
21 changed files with 130 additions and 129 deletions
@@ -10,8 +10,8 @@ vi.mock('./remote-install-lock', () => ({
RELAY_INSTALL_LOCK_NAME: '.install-lock'
}))
vi.mock('./remote-install-transfers', () => ({
uploadRelayDirectory: vi.fn(),
writeRelayFile: vi.fn()
uploadRemoteInstallDirectory: vi.fn(),
writeRemoteInstallFile: vi.fn()
}))
vi.mock('./orcad-remote-node-runtime', () => ({
ensureRemoteOrcadNodeRuntime: vi.fn().mockResolvedValue(undefined)
@@ -19,7 +19,7 @@ vi.mock('./orcad-remote-node-runtime', () => ({
import type { OrcadHostTransport } from '../../ssh/orcad-host-transport'
import { execCommand } from '../../ssh/remote-exec/remote-exec-command'
import { uploadRelayDirectory, writeRelayFile } from './remote-install-transfers'
import { uploadRemoteInstallDirectory, writeRemoteInstallFile } from './remote-install-transfers'
import { installOrcadBundle } from './orcad-remote-install'
import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform'
import { decodeRemotePowerShellScript } from '../../ssh/ssh-remote-powershell'
@@ -35,8 +35,8 @@ import { join } from 'node:path'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: All connection operations are mocked.
const conn = {} as OrcadHostTransport
const mockExec = vi.mocked(execCommand)
const mockUpload = vi.mocked(uploadRelayDirectory)
const mockWrite = vi.mocked(writeRelayFile)
const mockUpload = vi.mocked(uploadRemoteInstallDirectory)
const mockWrite = vi.mocked(writeRemoteInstallFile)
const fullVersion = '0.2.0+abcdef123456'
function issuedCommands(): string[] {
@@ -17,7 +17,7 @@ import { ORCAD_FENCE_OWNER_FILENAME } from '../activation/orcad-activation-fence
import { forgetHeldOrcadFence, rememberHeldOrcadFence } from '../../ssh/orcad-held-fence-tokens'
import { exitedOwnLockProof } from '../../ssh/orcad-exited-own-lock'
import { orcadWindowsBaseDir } from './orcad-remote-windows-node'
import { uploadRelayDirectory, writeRelayFile } from './remote-install-transfers'
import { uploadRemoteInstallDirectory, writeRemoteInstallFile } from './remote-install-transfers'
import {
abandonInstall,
finalizeInstall,
@@ -91,9 +91,15 @@ export async function installOrcadBundle(
) {
return
}
await uploadRelayDirectory(options.conn, options.localOrcadDir, remoteDir, options.host, {
signal: options.signal
})
await uploadRemoteInstallDirectory(
options.conn,
options.localOrcadDir,
remoteDir,
options.host,
{
signal: options.signal
}
)
if (options.host.os !== 'win32') {
await execCommand(
options.conn,
@@ -104,7 +110,7 @@ export async function installOrcadBundle(
}
)
}
await writeRelayFile(
await writeRemoteInstallFile(
options.conn,
options.host,
joinRemotePath(options.host, remoteDir, ORCAD_INSTALL_MODEL.versionFilename),
@@ -16,8 +16,8 @@ vi.mock('./remote-install-lock', () => ({
RELAY_INSTALL_LOCK_NAME: '.install-lock'
}))
vi.mock('./remote-install-transfers', () => ({
uploadRelayDirectory: vi.fn().mockResolvedValue(undefined),
writeRelayFile: vi.fn().mockResolvedValue(undefined)
uploadRemoteInstallDirectory: vi.fn().mockResolvedValue(undefined),
writeRemoteInstallFile: vi.fn().mockResolvedValue(undefined)
}))
vi.mock('./orcad-remote-record-file', async (importOriginal) => ({
...(await importOriginal<typeof RecordFile>()),
@@ -10,7 +10,7 @@ import {
REMOTE_NODE_RUNTIME_READY
} from '../../ssh/orcad-remote-node-runtime-report'
import { execCommand } from '../../ssh/remote-exec/remote-exec-command'
import { uploadRelayDirectory } from './remote-install-transfers'
import { uploadRemoteInstallDirectory } from './remote-install-transfers'
import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform'
import { pinnedNodeRuntimeAsset } from '../../../shared/node-runtime-pin'
import { DesktopDownloadUnreachableError } from '../../node-runtime/pinned-runtime-materializer'
@@ -25,7 +25,9 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => (
...(await importOriginal<typeof ExecCommandModule>()),
execCommand: vi.fn()
}))
vi.mock('./remote-install-transfers', () => ({ uploadRelayDirectory: vi.fn(async () => {}) }))
vi.mock('./remote-install-transfers', () => ({
uploadRemoteInstallDirectory: vi.fn(async () => {})
}))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand.
const conn = {} as OrcadHostTransport
@@ -36,7 +38,7 @@ const refused = (): Promise<string> => Promise.reject(new Error('net::ERR_CONNEC
afterEach(() => {
vi.mocked(execCommand).mockReset()
vi.mocked(uploadRelayDirectory).mockClear()
vi.mocked(uploadRemoteInstallDirectory).mockClear()
})
/** A fresh POSIX host; `download` is what its curl/wget step prints. */
@@ -77,7 +79,7 @@ describe('pinned Node when this computer cannot download it', () => {
const result = await ensure(refused)
expect(result.transfer).toBe('host-downloaded')
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
const promote = commands.find((command) => command.includes('tar -xzf'))
expect(promote).toContain(pinnedNodeRuntimeAsset(TARGET).archive)
expect(promote).toContain(pinnedNodeRuntimeAsset(TARGET).executableSha256)
@@ -105,7 +107,7 @@ describe('pinned Node when this computer cannot download it', () => {
expect(message).toContain('Settings > Advanced > Network > Node.js download mirror')
expect(message).toContain("NODEJS_ORG_MIRROR in this computer's environment")
expect(failure).toMatchObject({ mirror: 'unset' })
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
})
it('records that both attempts went through the configured mirror', async () => {
@@ -171,7 +173,7 @@ describe('pinned Node when this computer cannot download it', () => {
writeFileSync(archive, 'archive')
try {
await expect(ensure(async () => archive)).resolves.toMatchObject({ transfer: 'uploaded' })
expect(uploadRelayDirectory).toHaveBeenCalledTimes(1)
expect(uploadRemoteInstallDirectory).toHaveBeenCalledTimes(1)
expect(commands.some((command) => command.includes('command -v curl'))).toBe(false)
} finally {
rmSync(cache, { recursive: true, force: true })
@@ -25,14 +25,16 @@ import {
} from '../../ssh/orcad-remote-node-runtime-report'
import { classifyOrcadHostUnavailable } from '../../orcad/orcad-host-unavailable'
import { execCommand } from '../../ssh/remote-exec/remote-exec-command'
import { uploadRelayDirectory } from './remote-install-transfers'
import { uploadRemoteInstallDirectory } from './remote-install-transfers'
import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform'
vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({
...(await importOriginal<typeof ExecCommandModule>()),
execCommand: vi.fn()
}))
vi.mock('./remote-install-transfers', () => ({ uploadRelayDirectory: vi.fn(async () => {}) }))
vi.mock('./remote-install-transfers', () => ({
uploadRemoteInstallDirectory: vi.fn(async () => {})
}))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand.
const conn = {} as OrcadHostTransport
@@ -50,7 +52,7 @@ afterEach(() => {
rmSync(directory, { recursive: true, force: true })
}
vi.mocked(execCommand).mockReset()
vi.mocked(uploadRelayDirectory).mockClear()
vi.mocked(uploadRemoteInstallDirectory).mockClear()
})
describe('pinned-Node runtime store on a host that refuses to run programs', () => {
@@ -112,7 +114,7 @@ describe('pinned-Node runtime store on a host that refuses to run programs', ()
expect(classifyOrcadHostUnavailable(failure)).toBe('runtime_noexec')
expect(String(failure)).toContain('/root/.orca-remote/runtimes')
expect(archivePath).not.toHaveBeenCalled()
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
const commands = vi.mocked(execCommand).mock.calls.map(([, command]) => command)
expect(commands.at(-1)).toMatch(/^rm -rf '\/root\/\.orca-remote\/runtimes\/\.stage-/)
})
@@ -144,7 +146,7 @@ describe('pinned-Node runtime store on a host that refuses to run programs', ()
target: 'linux-x64-glibc',
archivePath: async () => archive()
})
expect(uploadRelayDirectory).toHaveBeenCalledTimes(1)
expect(uploadRemoteInstallDirectory).toHaveBeenCalledTimes(1)
})
})
@@ -20,7 +20,9 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => (
...(await importOriginal<typeof ExecCommandModule>()),
execCommand: vi.fn()
}))
vi.mock('./remote-install-transfers', () => ({ uploadRelayDirectory: vi.fn(async () => {}) }))
vi.mock('./remote-install-transfers', () => ({
uploadRemoteInstallDirectory: vi.fn(async () => {})
}))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand.
const conn = {} as OrcadHostTransport
@@ -17,14 +17,14 @@ import {
} from '../../ssh/orcad-remote-node-runtime-report'
import { RUNTIME_STORE_LOCK_NAME, withRuntimeStoreLock } from './remote-node-runtime-store-lock'
import { execCommand } from '../../ssh/remote-exec/remote-exec-command'
import { uploadRelayDirectory } from './remote-install-transfers'
import { uploadRemoteInstallDirectory } from './remote-install-transfers'
import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform'
vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({
...(await importOriginal<typeof ExecCommandModule>()),
execCommand: vi.fn()
}))
vi.mock('./remote-install-transfers', () => ({ uploadRelayDirectory: vi.fn() }))
vi.mock('./remote-install-transfers', () => ({ uploadRemoteInstallDirectory: vi.fn() }))
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand, which runs it in a local shell.
const conn = {} as OrcadHostTransport
@@ -68,7 +68,7 @@ describe.skipIf(process.platform === 'win32')('runtime install on slow storage',
const archive = join(root, 'node.tar.gz')
writeFileSync(archive, 'archive')
// A sibling installer publishes this pin while this client uploads.
vi.mocked(uploadRelayDirectory).mockImplementation(async () => {
vi.mocked(uploadRemoteInstallDirectory).mockImplementation(async () => {
mkdirSync(join(runtimeDir, 'bin'), { recursive: true })
writeFileSync(posixNodeRuntimeExecutable(host, runtimeDir), 'node', { mode: 0o755 })
writeFileSync(join(runtimeDir, REMOTE_NODE_RUNTIME_VERIFIED_MARKER), '')
@@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { NODE_RUNTIME_ASSETS, NODE_RUNTIME_PIN } from '../../../shared/node-runtime-pin'
import type { OrcadHostTransport } from '../../ssh/orcad-host-transport'
import { execCommand } from '../../ssh/remote-exec/remote-exec-command'
import { uploadRelayDirectory } from './remote-install-transfers'
import { uploadRemoteInstallDirectory } from './remote-install-transfers'
import {
ensureRemoteOrcadNodeRuntime,
NODE_RUNTIME_PROMOTE_TIMEOUT_MS,
@@ -29,7 +29,7 @@ vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => (
...(await importOriginal<typeof ExecCommandModule>()),
execCommand: vi.fn()
}))
vi.mock('./remote-install-transfers', () => ({ uploadRelayDirectory: vi.fn() }))
vi.mock('./remote-install-transfers', () => ({ uploadRemoteInstallDirectory: vi.fn() }))
const host = getRemoteHostPlatform('win32-x64')
const target = 'win32-x64'
@@ -47,7 +47,7 @@ function promoteScript(): string {
beforeEach(() => {
vi.mocked(execCommand).mockReset()
vi.mocked(uploadRelayDirectory).mockReset().mockResolvedValue(undefined)
vi.mocked(uploadRemoteInstallDirectory).mockReset().mockResolvedValue(undefined)
})
describe('Windows runtime store layout', () => {
@@ -190,7 +190,7 @@ describe('ensureRemoteOrcadNodeRuntime on Windows', () => {
await ensureRemoteOrcadNodeRuntime({ conn, host, slotDir: relayDir, target, archivePath })
expect(execCommand).toHaveBeenCalledTimes(1)
expect(vi.mocked(execCommand).mock.calls[0][2]).toMatchObject({ wrapCommand: false })
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
})
/** Answers the probe, the store lock and the promote script by what each script does. */
@@ -226,7 +226,7 @@ describe('ensureRemoteOrcadNodeRuntime on Windows', () => {
expect(stage).toMatch(
/^C:\/Users\/u\/\.orca-remote\/runtimes\/\.stage-node-[0-9a-f]{64}-[0-9a-f]{16}$/
)
expect(vi.mocked(uploadRelayDirectory).mock.calls[0][2]).toBe(stage)
expect(vi.mocked(uploadRemoteInstallDirectory).mock.calls[0][2]).toBe(stage)
const locked = scripts.findIndex((s) => s.includes('CreateNew'))
const promoted = scripts.findIndex((s) => s.includes('Invoke-OrcaPromote'))
const released = scripts.findIndex(
@@ -252,7 +252,7 @@ describe('ensureRemoteOrcadNodeRuntime on Windows', () => {
it('still removes the stage when the upload fails before promote runs', async () => {
vi.mocked(execCommand).mockResolvedValueOnce(REMOTE_NODE_RUNTIME_MISSING).mockResolvedValue('')
vi.mocked(uploadRelayDirectory).mockRejectedValueOnce(new Error('sftp write failed'))
vi.mocked(uploadRemoteInstallDirectory).mockRejectedValueOnce(new Error('sftp write failed'))
await expect(
ensureRemoteOrcadNodeRuntime({ conn, host, slotDir: relayDir, target, archivePath })
).rejects.toThrow('sftp write failed')
@@ -28,7 +28,7 @@ import {
execCommand,
isUnconfirmedSshCommandTermination
} from '../../ssh/remote-exec/remote-exec-command'
import { uploadRelayDirectory } from './remote-install-transfers'
import { uploadRemoteInstallDirectory } from './remote-install-transfers'
import { remoteSha256Command } from './remote-sha256-command'
import { stageNodeRuntimeArchive } from './remote-node-runtime-host-download'
import {
@@ -320,7 +320,7 @@ export async function ensureRemoteOrcadNodeRuntime(options: {
exec,
stageDir,
upload: (dir) =>
remoteStep(() => uploadRelayDirectory(conn, dir, stageDir, host, { signal })),
remoteStep(() => uploadRemoteInstallDirectory(conn, dir, stageDir, host, { signal })),
onLocalStage: (dir) => (localStage = dir)
})
let promoteRan = false
@@ -13,8 +13,8 @@ vi.mock('../../../shared/shell-argument-quoting', () => ({
quotePosixShell: (s: string) => `'${s}'`
}))
vi.mock('./remote-install-transfers', () => ({
writeRelayFile: vi.fn().mockResolvedValue(undefined),
uploadRelayDirectory: vi.fn().mockResolvedValue(undefined)
writeRemoteInstallFile: vi.fn().mockResolvedValue(undefined),
uploadRemoteInstallDirectory: vi.fn().mockResolvedValue(undefined)
}))
vi.mock('./remote-install-lock', async (importOriginal) => ({
...(await importOriginal<typeof InstallLock>()),
@@ -1,13 +1,3 @@
// npm install on a cold Windows cache plus antivirus scanning can exceed the
// default 30s exec timeout.
export const NATIVE_DEPS_COMMAND_TIMEOUT_MS = 240_000
// Why: a missing binding can require both install and rebuild while the same
// install lock is held. Concurrent first installs must wait for that valid
// holder instead of failing halfway through its bounded work.
const NATIVE_DEPS_REPAIR_BUDGET_MS = 2 * NATIVE_DEPS_COMMAND_TIMEOUT_MS
// Why: native repair also runs separately bounded chmod/probe/diagnostic/
// finalize commands around install + rebuild. Keep the outer bound above that
// valid worst path while remaining below the 20-minute stale-lock threshold.
export const RELAY_DEPLOY_TIMEOUT_MS = NATIVE_DEPS_REPAIR_BUDGET_MS + 7 * 60_000
// Why 15 minutes: it bounds one remote install step and a concurrent install's wait for the
// install lock, and must stay below the 20-minute stale-lock threshold (remote-install-lock.ts).
export const REMOTE_INSTALL_TIMEOUT_MS = 15 * 60_000
@@ -1,6 +1,6 @@
import type { RemoteInstallTransport } from './remote-install-transport'
import { execHostCommand } from '../../ssh/remote-exec/remote-host-exec'
import { RELAY_DEPLOY_TIMEOUT_MS } from './remote-deploy-timing'
import { REMOTE_INSTALL_TIMEOUT_MS } from './remote-deploy-timing'
import { isUnconfirmedSshCommandTermination } from '../../ssh/remote-exec/remote-exec-command'
import { isRelayGcClaimed, waitForRelayGcClaimRelease } from './remote-install-gc-claim'
import {
@@ -23,12 +23,9 @@ const INSTALL_LOCK_POLL_MS = 1_000
// Why: a fresh lock can cross the stale threshold during our bounded wait.
// Recheck infrequently so it becomes recoverable without adding an exec per poll.
const INSTALL_LOCK_STALE_RECHECK_MS = 60_000
// Why: the lock holder can legitimately use the full deploy bound for upload,
// native install/rebuild, probes, and finalization. A concurrent first install
// must not fail earlier; completed-relay repair uses a separate one-shot path.
const INSTALL_LOCK_TIMEOUT_MS = RELAY_DEPLOY_TIMEOUT_MS
// Why: native-deps repair can keep running after the deploy backstop wins its
// Promise.race, so stale takeover must leave room for that bounded work.
// Why: a waiter gives the holder at least one full install step's bound before giving up.
const INSTALL_LOCK_TIMEOUT_MS = REMOTE_INSTALL_TIMEOUT_MS
// Why longer: a holder's step can outlive the bound that raced it, so takeover leaves it room.
export const INSTALL_LOCK_STALE_MS = 20 * 60_000
export const INSTALL_LOCK_STALE_SECONDS = INSTALL_LOCK_STALE_MS / 1000
const DEFAULT_REMOTE_HOST = getRemoteHostPlatform('linux-x64')
@@ -36,7 +33,7 @@ const DEFAULT_REMOTE_HOST = getRemoteHostPlatform('linux-x64')
export class RemoteInstallLockBusyError extends Error {
constructor(lockDir: string, timeoutMs: number) {
super(
`Could not acquire relay install lock at ${lockDir} after ${timeoutMs / 1000}s; ` +
`Could not acquire the install lock at ${lockDir} after ${timeoutMs / 1000}s; ` +
'another install is still in progress.'
)
this.name = 'RemoteInstallLockBusyError'
@@ -180,7 +177,7 @@ export async function acquireInstallLock(
if (exitedOwner && reason === 'exited-owner') {
options?.exitedOwner?.reclaimed(exitedOwner.token)
}
console.warn(`[ssh-relay] Stealing ${reason} install lock at ${lockDir}`)
console.warn(`[ssh] Stealing ${reason} install lock at ${lockDir}`)
if (await keptAfterGcRecheck()) {
return
}
@@ -188,7 +185,7 @@ export async function acquireInstallLock(
}
if (Date.now() - lastWaitLogAt >= INSTALL_LOCK_STALE_RECHECK_MS) {
lastWaitLogAt = Date.now()
console.info(`[ssh-relay] Waiting for install lock at ${lockDir}`)
console.info(`[ssh] Waiting for install lock at ${lockDir}`)
}
if (Date.now() - start >= waitTimeoutMs) {
if (!sawHolder && lastCommandError !== undefined) {
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { SshTarget } from '../../../shared/ssh-types'
import { spawnProcess } from '@orca/process-host'
import { SshConnection } from '../../ssh/connection/ssh-connection'
import { uploadRelayDirectory, writeRelayFile } from './remote-install-transfers'
import { uploadRemoteInstallDirectory, writeRemoteInstallFile } from './remote-install-transfers'
import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform'
type NoisyHost = { port: number; subsystems: number; execs: number; close: () => Promise<void> }
@@ -121,8 +121,8 @@ describe.skipIf(process.platform === 'win32')(
const conn = connectionWithClient(client)
const linux = getRemoteHostPlatform('linux-x64')
const remoteDir = join(remoteRoot, 'relay')
await uploadRelayDirectory(conn, localDir, remoteDir, linux)
await writeRelayFile(conn, linux, join(remoteDir, '.version'), '0.1.0+noisy')
await uploadRemoteInstallDirectory(conn, localDir, remoteDir, linux)
await writeRemoteInstallFile(conn, linux, join(remoteDir, '.version'), '0.1.0+noisy')
expect(await readFile(join(remoteDir, 'nested', 'payload.bin'))).toEqual(
Buffer.from([0, 1, 2, 255])
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RemoteInstallTransport } from './remote-install-transport'
import { uploadRelayDirectory, writeRelayFile } from './remote-install-transfers'
import { uploadRemoteInstallDirectory, writeRemoteInstallFile } from './remote-install-transfers'
import {
RemoteInstallOutOfSpaceError,
remoteFreeSpaceProbeCommand
@@ -49,7 +49,7 @@ afterEach(() => rmSync(localDir, { recursive: true, force: true }))
describe('a failed install write on a full host', () => {
it('says the disk is full instead of the bare SFTP "Failure"', async () => {
vi.mocked(execHostCommand).mockResolvedValue(dfLine(12))
const upload = uploadRelayDirectory(
const upload = uploadRemoteInstallDirectory(
connection(sftpFailure),
localDir,
'/root/.orca-remote/orcad-0.1.0+abc',
@@ -67,7 +67,7 @@ describe('a failed install write on a full host', () => {
it('sizes a single file write by its contents', async () => {
vi.mocked(execHostCommand).mockResolvedValue(dfLine(0))
await expect(
writeRelayFile(connection(sftpFailure), posix, '/root/.orca-remote/x/.version', 'abc')
writeRemoteInstallFile(connection(sftpFailure), posix, '/root/.orca-remote/x/.version', 'abc')
).rejects.toThrow(/has 0 KB free, and Orca needs about 1 KB/)
})
@@ -76,7 +76,7 @@ describe('a failed install write on a full host', () => {
vi.mocked(execHostCommand).mockResolvedValue(dfLine(1_500))
const failure = sftpFailure()
await expect(
uploadRelayDirectory(
uploadRemoteInstallDirectory(
connection(() => failure),
localDir,
'/root/r',
@@ -89,7 +89,7 @@ describe('a failed install write on a full host', () => {
vi.mocked(execHostCommand).mockRejectedValue(new Error('df: not found'))
const failure = sftpFailure()
await expect(
uploadRelayDirectory(
uploadRemoteInstallDirectory(
connection(() => failure),
localDir,
'/root/r',
@@ -101,7 +101,7 @@ describe('a failed install write on a full host', () => {
it('keeps explicit refusals such as permission denied without probing', async () => {
const denied = Object.assign(new Error('Permission denied'), { code: 3 })
await expect(
uploadRelayDirectory(
uploadRemoteInstallDirectory(
connection(() => denied),
localDir,
'/root/r',
@@ -117,7 +117,7 @@ describe('a failed install write on a full host', () => {
sshChannelCloseConfirmed: false
})
await expect(
uploadRelayDirectory(
uploadRemoteInstallDirectory(
connection(() => lost),
localDir,
'/root/r',
@@ -127,13 +127,13 @@ describe('a failed install write on a full host', () => {
const controller = new AbortController()
controller.abort()
await expect(
uploadRelayDirectory(connection(sftpFailure), localDir, '/root/r', posix, {
uploadRemoteInstallDirectory(connection(sftpFailure), localDir, '/root/r', posix, {
signal: controller.signal
})
).rejects.toThrow()
const failure = sftpFailure()
await expect(
uploadRelayDirectory(
uploadRemoteInstallDirectory(
connection(() => failure),
localDir,
'C:/Users/u/r',
@@ -7,8 +7,8 @@ import {
} from '../../ssh/ssh-exec-stdin-file-transfer'
import {
classifySftpFailureForExecFallback,
uploadRelayDirectory,
writeRelayFile
uploadRemoteInstallDirectory,
writeRemoteInstallFile
} from './remote-install-transfers'
import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform'
import { tagSftpHandshakeCorruption } from '../../ssh/sftp-handshake-corruption'
@@ -101,8 +101,8 @@ describe('classifySftpFailureForExecFallback', () => {
describe('relay install transfer fallback selection', () => {
it('streams over exec stdin when the subsystem is refused, and skips SFTP for the rest of the connect', async () => {
const conn = connection(subsystemRefused)
await uploadRelayDirectory(conn, '/local/relay', '/h/.orca-remote/stage/payload', posix)
await writeRelayFile(conn, posix, '/h/.orca-remote/stage/payload/.version', '0.1.0+abc')
await uploadRemoteInstallDirectory(conn, '/local/relay', '/h/.orca-remote/stage/payload', posix)
await writeRemoteInstallFile(conn, posix, '/h/.orca-remote/stage/payload/.version', '0.1.0+abc')
expect(uploadDirectoryViaExecStdin).toHaveBeenCalledWith(
conn,
@@ -123,16 +123,16 @@ describe('relay install transfer fallback selection', () => {
it('asks SFTP again after a reconnect', async () => {
let generation = 1
const conn = connection(subsystemRefused, { getConnectGeneration: () => generation })
await uploadRelayDirectory(conn, '/l', '/h/r', posix)
await uploadRemoteInstallDirectory(conn, '/l', '/h/r', posix)
generation = 2
await uploadRelayDirectory(conn, '/l', '/h/r', posix)
await uploadRemoteInstallDirectory(conn, '/l', '/h/r', posix)
expect(conn.uploadDirectory).toHaveBeenCalledTimes(2)
})
it('falls back for a chrooted subsystem without remembering it as a host verdict', async () => {
const conn = connection(sandboxed)
await uploadRelayDirectory(conn, '/l', '/h/r', posix)
await uploadRelayDirectory(conn, '/l', '/h/r', posix)
await uploadRemoteInstallDirectory(conn, '/l', '/h/r', posix)
await uploadRemoteInstallDirectory(conn, '/l', '/h/r', posix)
expect(conn.uploadDirectory).toHaveBeenCalledTimes(2)
expect(uploadDirectoryViaExecStdin).toHaveBeenCalledTimes(2)
})
@@ -140,7 +140,7 @@ describe('relay install transfer fallback selection', () => {
it('surfaces unverifiable transport loss instead of retrying down another path', async () => {
const lost = unconfirmed()
const conn = connection(() => lost)
await expect(uploadRelayDirectory(conn, '/l', '/h/r', posix)).rejects.toBe(lost)
await expect(uploadRemoteInstallDirectory(conn, '/l', '/h/r', posix)).rejects.toBe(lost)
expect(uploadDirectoryViaExecStdin).not.toHaveBeenCalled()
})
@@ -151,17 +151,17 @@ describe('relay install transfer fallback selection', () => {
return subsystemRefused()
})
await expect(
uploadRelayDirectory(conn, '/l', '/h/r', posix, { signal: controller.signal })
uploadRemoteInstallDirectory(conn, '/l', '/h/r', posix, { signal: controller.signal })
).rejects.toThrow()
expect(uploadDirectoryViaExecStdin).not.toHaveBeenCalled()
})
it('keeps the sandbox diagnosis where exec stdin cannot apply', async () => {
await expect(
uploadRelayDirectory(connection(sandboxed), '/l', 'C:/Users/u/r', windows)
uploadRemoteInstallDirectory(connection(sandboxed), '/l', 'C:/Users/u/r', windows)
).rejects.toThrow(/SFTP subsystem sees a different filesystem/)
await expect(
uploadRelayDirectory(
uploadRemoteInstallDirectory(
connection(sandboxed, { usesSystemSshTransport: () => true }),
'/l',
'/h/r',
@@ -1,5 +1,5 @@
// Relay-install file writes over the host transport's own transfer methods (SFTP on SSH). When
// the host definitively refuses SFTP, POSIX hosts fall back to exec-channel stdin.
// Install file writes over the host transport's own transfer methods (SFTP on SSH). When the
// host definitively refuses SFTP, POSIX hosts fall back to exec-channel stdin.
import type { RemoteInstallTransport } from './remote-install-transport'
import type { SftpNamespacePathMapping } from '../../ssh/sftp-namespace-resolution'
@@ -16,43 +16,43 @@ import {
import { isSftpHandshakeCorruption } from '../../ssh/sftp-handshake-corruption'
import { explainRemoteInstallWriteFailure, localTreeBytes } from './remote-install-out-of-space'
export type RelayTransferOptions = {
export type RemoteInstallTransferOptions = {
signal?: AbortSignal
sftpNamespace?: SftpNamespacePathMapping
}
export async function uploadRelayDirectory(
export async function uploadRemoteInstallDirectory(
conn: RemoteInstallTransport,
localRelayDir: string,
localDir: string,
shellRemoteDir: string,
hostPlatform: RemoteHostPlatform,
options?: RelayTransferOptions
options?: RemoteInstallTransferOptions
): Promise<void> {
await transferWithExecStdinFallback(
conn,
hostPlatform,
shellRemoteDir,
options?.signal,
() => localTreeBytes(localRelayDir),
() => localTreeBytes(localDir),
() =>
conn.uploadDirectory(localRelayDir, shellRemoteDir, {
conn.uploadDirectory(localDir, shellRemoteDir, {
hostPlatform,
signal: options?.signal,
sftpNamespace: options?.sftpNamespace
}),
() =>
uploadDirectoryViaExecStdin(conn, localRelayDir, shellRemoteDir, hostPlatform, {
uploadDirectoryViaExecStdin(conn, localDir, shellRemoteDir, hostPlatform, {
signal: options?.signal
})
)
}
export async function writeRelayFile(
export async function writeRemoteInstallFile(
conn: RemoteInstallTransport,
hostPlatform: RemoteHostPlatform,
shellRemotePath: string,
contents: string,
options?: RelayTransferOptions
options?: RemoteInstallTransferOptions
): Promise<void> {
await transferWithExecStdinFallback(
conn,
@@ -167,7 +167,7 @@ async function transferOnce(
sftpUnavailableGeneration.set(conn, connectGeneration(conn))
}
console.warn(
`[ssh-relay] SFTP ${reason === 'sftp-unavailable' ? 'is unavailable' : 'cannot see the install path'} (${error instanceof Error ? error.message : String(error)}); streaming ${remotePath} over exec stdin`
`[ssh] SFTP ${reason === 'sftp-unavailable' ? 'is unavailable' : 'cannot see the install path'} (${error instanceof Error ? error.message : String(error)}); streaming ${remotePath} over exec stdin`
)
}
await execStdinTransfer()
@@ -33,22 +33,22 @@ type RemoteInstallProbeOptions = {
}
/**
* Read the local relay's content-hashed version (e.g. "0.1.0+0a5fe134d020") from
* `${localRelayDir}/.version`. Throws on missing/empty so the caller can't
* Read the local build's content-hashed version (e.g. "0.1.0+0a5fe134d020") from
* `${localDir}/.version`. Throws on missing/empty so the caller can't
* silently fall back to a path where a stale-generation daemon may be running.
*/
export function readLocalFullVersion(localRelayDir: string): string {
const versionFile = join(localRelayDir, '.version')
export function readLocalFullVersion(localDir: string): string {
const versionFile = join(localDir, '.version')
if (!existsSync(versionFile)) {
throw new Error(
`Orca's local relay build is missing its version marker at ${versionFile}. ` +
`Orca's local server build is missing its version marker at ${versionFile}. ` +
`This usually indicates a packaging or build problem; reinstall Orca.`
)
}
const v = readFileSync(versionFile, 'utf-8').trim()
if (!v) {
throw new Error(
`Orca's local relay version marker at ${versionFile} is empty. ` +
`Orca's local server version marker at ${versionFile} is empty. ` +
`This usually indicates a packaging or build problem; reinstall Orca.`
)
}
+21 -19
View File
@@ -18,8 +18,8 @@ vi.mock('../managed-server/install/remote-install-lock', () => ({
RELAY_INSTALL_LOCK_NAME: '.install-lock'
}))
vi.mock('../managed-server/install/remote-install-transfers', () => ({
uploadRelayDirectory: vi.fn().mockResolvedValue(undefined),
writeRelayFile: vi.fn().mockResolvedValue(undefined)
uploadRemoteInstallDirectory: vi.fn().mockResolvedValue(undefined),
writeRemoteInstallFile: vi.fn().mockResolvedValue(undefined)
}))
vi.mock('../managed-server/install/orcad-remote-record-file', async (importOriginal) => ({
...(await importOriginal<typeof RecordFile>()),
@@ -37,7 +37,7 @@ import {
acquireInstallLock,
isRelayInstallLockStale
} from '../managed-server/install/remote-install-lock'
import { uploadRelayDirectory } from '../managed-server/install/remote-install-transfers'
import { uploadRemoteInstallDirectory } from '../managed-server/install/remote-install-transfers'
import { writeAtomicOrcadRemoteRecord } from '../managed-server/install/orcad-remote-record-file'
import { deployOrcad, type OrcadDeployOptions } from './orcad-remote-deploy'
import { installOrcadBundle } from '../managed-server/install/orcad-remote-install'
@@ -243,7 +243,7 @@ describe('orcad install lock ownership', () => {
await install()
expect(acquireInstallLock).not.toHaveBeenCalled()
expect(abandonInstall).not.toHaveBeenCalled()
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
})
it('releases its lock when another installer completed while acquisition waited', async () => {
@@ -252,13 +252,13 @@ describe('orcad install lock ownership', () => {
expect(acquireInstallLock).toHaveBeenCalledOnce()
expect(abandonInstall).toHaveBeenCalledOnce()
expect(abandonInstall).toHaveBeenCalledWith(expect.anything(), remoteDir, options().host)
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
expect(finalizeInstall).not.toHaveBeenCalled()
})
it('publishes a complete install before releasing its lock once', async () => {
await install()
expect(uploadRelayDirectory).toHaveBeenCalledOnce()
expect(uploadRemoteInstallDirectory).toHaveBeenCalledOnce()
expect(finalizeInstall).toHaveBeenCalledWith(expect.anything(), remoteDir, options().host, {
signal: undefined,
releaseLock: false
@@ -271,7 +271,7 @@ describe('orcad install lock ownership', () => {
it('releases a failed upload without publishing it or replacing its error', async () => {
const error = new Error('upload interrupted')
vi.mocked(uploadRelayDirectory).mockRejectedValueOnce(error)
vi.mocked(uploadRemoteInstallDirectory).mockRejectedValueOnce(error)
await expect(install()).rejects.toBe(error)
expect(abandonInstall).toHaveBeenCalledOnce()
expect(finalizeInstall).not.toHaveBeenCalled()
@@ -280,7 +280,7 @@ describe('orcad install lock ownership', () => {
it('releases its lock without reusing an aborted operation signal', async () => {
const controller = new AbortController()
const error = new Error('deployment canceled')
vi.mocked(uploadRelayDirectory).mockImplementationOnce(async () => {
vi.mocked(uploadRemoteInstallDirectory).mockImplementationOnce(async () => {
controller.abort(error)
controller.signal.throwIfAborted()
})
@@ -295,7 +295,7 @@ describe('orcad install lock ownership', () => {
vi.mocked(acquireInstallLock).mockRejectedValueOnce(error)
await expect(install()).rejects.toBe(error)
expect(abandonInstall).not.toHaveBeenCalled()
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
})
})
@@ -338,7 +338,9 @@ describe('deployOrcad', () => {
expect(vi.mocked(acquireInstallLock).mock.calls[0][1]).toBe(
`/home/u/.orca-remote/orcad-${NEW_VERSION}`
)
expect(vi.mocked(uploadRelayDirectory).mock.calls[0][2]).toContain(`orcad-${NEW_VERSION}`)
expect(vi.mocked(uploadRemoteInstallDirectory).mock.calls[0][2]).toContain(
`orcad-${NEW_VERSION}`
)
})
it.each([
@@ -367,7 +369,7 @@ describe('deployOrcad', () => {
expect(ensureRemoteOrcadNodeRuntime).toHaveBeenCalledWith(
expect.objectContaining({ target, slotDir: `/home/u/.orca-remote/orcad-${NEW_VERSION}` })
)
expect(vi.mocked(uploadRelayDirectory).mock.invocationCallOrder[0]).toBeLessThan(
expect(vi.mocked(uploadRemoteInstallDirectory).mock.invocationCallOrder[0]).toBeLessThan(
mockExec.mock.invocationCallOrder[chmod]
)
expect(mockExec.mock.invocationCallOrder[chmod]).toBeLessThan(
@@ -389,7 +391,7 @@ describe('deployOrcad', () => {
NEW_VERSION,
`C:/Users/u/.orca-remote/orcad-${NEW_VERSION}`
)
expect(uploadRelayDirectory).toHaveBeenCalledOnce()
expect(uploadRemoteInstallDirectory).toHaveBeenCalledOnce()
expect(finalizeInstall).toHaveBeenCalledOnce()
expect(mockExec.mock.calls.some(([, command]) => String(command).startsWith('chmod '))).toBe(
false
@@ -425,7 +427,7 @@ describe('deployOrcad', () => {
outcome: 'installed-not-activated',
code
})
expect(uploadRelayDirectory).not.toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
})
// BUG-21: a bare stale fence (a wake cut short) failed every update with "Recover it first".
@@ -446,7 +448,7 @@ describe('deployOrcad', () => {
expect(vi.mocked(acquireInstallLock).mock.calls[0]?.[3]).toMatchObject({
allowStaleTakeover: true
})
expect(uploadRelayDirectory).toHaveBeenCalled()
expect(uploadRemoteInstallDirectory).toHaveBeenCalled()
})
it('leaves an upload incomplete when the remote cannot make search executable', async () => {
@@ -568,7 +570,7 @@ describe('deployOrcad', () => {
log: []
}
scriptHost(script)
vi.mocked(uploadRelayDirectory).mockImplementationOnce(async () => {
vi.mocked(uploadRemoteInstallDirectory).mockImplementationOnce(async () => {
script.activationRecord = JSON.stringify(settle(newer))
})
const admitRecord = vi.fn((record: OrcadActivationRecord) =>
@@ -602,7 +604,7 @@ describe('deployOrcad', () => {
const counted = JSON.parse(ACTIVE_OLD)
// An admitted peer activation: same app line, so only the census binding can catch it.
const peer = withActivatedVersion(counted, '0.1.5+dd01', null, new Date(5), '1.4.0')
vi.mocked(uploadRelayDirectory).mockImplementationOnce(async () => {
vi.mocked(uploadRemoteInstallDirectory).mockImplementationOnce(async () => {
script.activationRecord = JSON.stringify(peer)
})
expect(
@@ -649,7 +651,7 @@ describe('deployOrcad', () => {
scriptHost(script)
const counted = JSON.parse(ACTIVE_OLD)
const peer = withActivatedVersion(counted, '0.1.5+dd01', null, new Date(5), '1.4.0')
vi.mocked(uploadRelayDirectory).mockImplementationOnce(async () => {
vi.mocked(uploadRemoteInstallDirectory).mockImplementationOnce(async () => {
script.activationRecord = JSON.stringify(peer)
})
const recountCensus = vi.fn()
@@ -672,7 +674,7 @@ describe('deployOrcad', () => {
code: 'orcad_update_terminals_running'
})
// The bytes landed; nothing was stopped, launched or snapshotted.
expect(vi.mocked(uploadRelayDirectory)).toHaveBeenCalled()
expect(vi.mocked(uploadRemoteInstallDirectory)).toHaveBeenCalled()
expect(script.log).toEqual([])
})
@@ -822,6 +824,6 @@ describe('deployOrcad', () => {
}
scriptHost(script)
await expect(deployOrcad(options())).rejects.toThrow('activation record')
expect(vi.mocked(uploadRelayDirectory)).not.toHaveBeenCalled()
expect(vi.mocked(uploadRemoteInstallDirectory)).not.toHaveBeenCalled()
})
})
+4 -4
View File
@@ -6,7 +6,7 @@ import { PassThrough } from 'node:stream'
import type { SFTPWrapper } from 'ssh2'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { uploadBuffer, uploadFile, writeStringViaSftp } from './sftp-upload'
import { writeRelayFile } from '../managed-server/install/remote-install-transfers'
import { writeRemoteInstallFile } from '../managed-server/install/remote-install-transfers'
import type { SshConnection } from './connection/ssh-connection'
import { getRemoteHostPlatform } from './ssh-remote-platform'
@@ -91,7 +91,7 @@ describe('sandboxed SFTP subsystem diagnosis', () => {
writeFile: () => Promise.reject(Object.assign(new Error('permission denied'), { code: 3 }))
} as unknown as SshConnection
const failure: unknown = await writeRelayFile(
const failure: unknown = await writeRemoteInstallFile(
conn,
getRemoteHostPlatform('linux-x64'),
'/home/user/.orca-remote/relay-1/.version',
@@ -112,7 +112,7 @@ describe('sandboxed SFTP subsystem diagnosis', () => {
// Windows hosts have no exec-stdin fallback; POSIX hosts stream instead (exec-fallback tests).
await expect(
writeRelayFile(
writeRemoteInstallFile(
conn,
getRemoteHostPlatform('win32-x64'),
'C:/Users/user/.orca-remote/relay-1/.version',
@@ -127,7 +127,7 @@ describe('sandboxed SFTP subsystem diagnosis', () => {
} as unknown as SshConnection
await expect(
writeRelayFile(conn, getRemoteHostPlatform('linux-x64'), '/home/user/x', 'v1')
writeRemoteInstallFile(conn, getRemoteHostPlatform('linux-x64'), '/home/user/x', 'v1')
).rejects.toThrow('Connection lost')
})
})
+2 -2
View File
@@ -14,7 +14,7 @@ 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 { REMOTE_INSTALL_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'
@@ -59,7 +59,7 @@ async function writeViaExecStdin(
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 }
{ signal: options?.signal, stdin, timeoutMs: REMOTE_INSTALL_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}`)
+2 -2
View File
@@ -12,7 +12,7 @@ import { parseWslUncPath, toLinuxPath } from '../../shared/wsl-paths'
import type { RemoteInstallTransport } from '../managed-server/install/remote-install-transport'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { execCommand } from '../ssh/remote-exec/remote-exec-command'
import { RELAY_DEPLOY_TIMEOUT_MS } from '../managed-server/install/remote-deploy-timing'
import { REMOTE_INSTALL_TIMEOUT_MS } from '../managed-server/install/remote-deploy-timing'
import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform'
import {
collectLocalUploadPlan,
@@ -182,7 +182,7 @@ export async function uploadDirectoryIntoWsl(
const manifest = await digestManifest(plan, signal)
const stage = `${remoteDir.replace(/\/+$/, '')}/.orca-upload-${randomBytes(8).toString('hex')}`
// Why the deploy bound: a runtime tree over a cold distro outlasts the 30 s probe default.
const execOptions = { signal, timeoutMs: RELAY_DEPLOY_TIMEOUT_MS }
const execOptions = { signal, timeoutMs: REMOTE_INSTALL_TIMEOUT_MS }
const stagedFromGuest =
guestSource !== null &&
(await execCommand(conn, stageFromGuestPathCommand(guestSource, stage), execOptions)).trim() !==