mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
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:
@@ -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.`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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() !==
|
||||
|
||||
Reference in New Issue
Block a user