Files
orca/tests/e2e/helpers/docker-ssh-relay-target.ts
T
Neil 11cd2b4310 revert(ssh): back out #13326 and #13928 — reconnect loses every tab (#14361)
* Revert "fix(daemon): stop killing live coding agents when the daemon can't report its sessions (#13928)"

This reverts commit 2e8cf589de.

Reverted together with #13326: the 1.4.182-daily.202608131439 build carrying
both loses every tab on an SSH disconnect/reconnect cycle. Reverting first so
main stays releasable and the P0 fixes in the wild remain cherry-pickable,
rather than fixing forward on a shipped regression.

* Revert "fix(ssh): stop SSH reconnect from multiplying terminals and resuming agents twice (STA-3077)" (#13326)

This reverts commit 3ab8b6a117.

Reported on 1.4.182-daily.202608131439: connect to an SSH worktree, disconnect
the host from the hosts popup, reconnect — every tab is gone. That is worse than
the behaviour this PR set out to fix, where most tabs were retained.

Reverting rather than fixing forward, so main stays releasable and the P0 fixes
already out in the wild stay cherry-pickable. STA-3077 stays open.
2026-08-13 14:54:29 -07:00

216 lines
6.6 KiB
TypeScript

import { execFileSync, spawnSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { getDockerSshRelayImage } from './docker-ssh-relay-image'
import type { TestInfo } from '@stablyai/playwright-test'
export const DOCKER_SSH_RELAY_REMOTE_REPO_PATH = '/tmp/orca-docker-relay-perf-repo'
export const DOCKER_SSH_PROXY_JUMP_REMOTE_REPO_PATH = '/tmp/orca-docker-proxy-jump-repo'
export const DOCKER_SSH_SECOND_HUB_REMOTE_REPO_PATH = '/tmp/orca-docker-second-hub-repo'
export type DockerSshRelayTarget = {
containerName: string
containerIp: string
host: string
identityFile: string
port: number
tempDir: string
}
function run(command: string, args: string[], opts: { timeoutMs?: number } = {}): string {
return execFileSync(command, args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: opts.timeoutMs ?? 30_000
}).trim()
}
function tryRun(command: string, args: string[], opts: { timeoutMs?: number } = {}): void {
spawnSync(command, args, { stdio: 'ignore', timeout: opts.timeoutMs ?? 10_000 })
}
export function shellQuote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`
}
export function execDockerSshRelayTargetCommand(
target: DockerSshRelayTarget,
command: string
): string {
return run('docker', ['exec', target.containerName, 'bash', '-lc', command], {
timeoutMs: 60_000
})
}
export function execDockerSshRelayTargetControlCommand(
target: DockerSshRelayTarget,
command: string
): string {
return run('docker', [
'exec',
target.containerName,
'bash',
'--noprofile',
'--norc',
'-c',
command
])
}
function sshArgs(target: DockerSshRelayTarget, command: string): string[] {
return [
'-i',
target.identityFile,
'-p',
String(target.port),
'-o',
'StrictHostKeyChecking=no',
'-o',
'UserKnownHostsFile=/dev/null',
'-o',
'BatchMode=yes',
'-o',
'IdentitiesOnly=yes',
`root@${target.host}`,
command
]
}
function waitForSsh(target: DockerSshRelayTarget): void {
const deadline = Date.now() + 90_000
let lastError = ''
while (Date.now() < deadline) {
const result = spawnSync('ssh', sshArgs(target, 'true'), {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 5_000
})
if (result.status === 0) {
return
}
lastError = result.stderr || result.stdout || `exit ${result.status}`
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1_000)
}
const logs = spawnSync('docker', ['logs', target.containerName], {
encoding: 'utf8',
timeout: 10_000
})
throw new Error(
`Timed out waiting for Docker SSH target: ${lastError}\n${logs.stderr || logs.stdout}`
)
}
export function dockerSshRelayRepoSentinel(target: DockerSshRelayTarget, repoPath: string): string {
return `${target.containerName}:${repoPath}`
}
function seedRemoteRepo(target: DockerSshRelayTarget, repoPath: string): void {
const sentinel = dockerSshRelayRepoSentinel(target, repoPath)
execDockerSshRelayTargetCommand(
target,
[
`rm -rf ${shellQuote(repoPath)}`,
`mkdir -p ${shellQuote(repoPath)}`,
`cd ${shellQuote(repoPath)}`,
'git init',
'git config user.email e2e@test.local',
'git config user.name "Orca Docker SSH E2E"',
`printf '%s\\n' ${shellQuote(sentinel)} > .orca-e2e-destination-id`,
`printf '%s\\n' ${shellQuote(`remote relay ${sentinel}`)} > README.md`,
'git add README.md .orca-e2e-destination-id',
'git commit -m initial'
].join(' && ')
)
}
export function writeDockerSshRelayTargetFile(
target: DockerSshRelayTarget,
filePath: string,
contents: string
): void {
execDockerSshRelayTargetCommand(
target,
`printf '%s' ${shellQuote(contents)} > ${shellQuote(filePath)}`
)
}
export function startDockerSshRelayTarget(testInfo: TestInfo): DockerSshRelayTarget {
const host = process.env.ORCA_E2E_SSH_TARGET_HOST?.trim() || '127.0.0.1'
if (host === 'localhost' || host === '::1' || host.startsWith('127.')) {
if (process.env.ORCA_E2E_SSH_TARGET_HOST) {
throw new Error(`ORCA_E2E_SSH_TARGET_HOST must be non-loopback: ${host}`)
}
}
const bindHost = host === '127.0.0.1' ? host : '0.0.0.0'
const tempDir = mkdtempSync(path.join(os.tmpdir(), 'orca-ssh-docker-'))
const identityFile = path.join(tempDir, 'id_ed25519')
run('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', identityFile, '-q'])
const publicKey = readFileSync(`${identityFile}.pub`, 'utf8').trim()
const containerName = `orca-ssh-e2e-${testInfo.workerIndex}-${Date.now()}-${randomUUID().slice(0, 8)}`
let target: DockerSshRelayTarget | null = null
try {
tryRun('docker', ['rm', '-f', containerName])
run(
'docker',
[
'run',
'-d',
'--name',
containerName,
'-p',
`${bindHost}::22`,
'-e',
`AUTHORIZED_KEY=${publicKey}`,
getDockerSshRelayImage(),
'bash',
'-lc',
[
'printf "%s\\n" "$AUTHORIZED_KEY" > /root/.ssh/authorized_keys',
'chmod 600 /root/.ssh/authorized_keys',
'git config --global user.email e2e@test.local',
'git config --global user.name "Orca Docker SSH E2E"',
'exec /usr/sbin/sshd -D -e'
].join(' && ')
],
{ timeoutMs: 120_000 }
)
const port = Number(run('docker', ['port', containerName, '22/tcp']).split(':').at(-1))
if (!Number.isInteger(port) || port <= 0) {
throw new Error(`Unable to read mapped SSH port for ${containerName}`)
}
const containerIp = run('docker', [
'inspect',
'--format',
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}',
containerName
])
if (!containerIp) {
throw new Error(`Unable to read container IP for ${containerName}`)
}
target = { containerName, containerIp, host, identityFile, port, tempDir }
waitForSsh(target)
seedRemoteRepo(target, DOCKER_SSH_RELAY_REMOTE_REPO_PATH)
seedRemoteRepo(target, DOCKER_SSH_PROXY_JUMP_REMOTE_REPO_PATH)
seedRemoteRepo(target, DOCKER_SSH_SECOND_HUB_REMOTE_REPO_PATH)
return target
} catch (error) {
cleanupDockerSshRelayTarget(
target ?? { containerName, containerIp: '', host, identityFile, port: 0, tempDir }
)
throw error
}
}
export function cleanupDockerSshRelayTarget(target: DockerSshRelayTarget | null): void {
if (!target) {
return
}
tryRun('docker', ['rm', '-f', target.containerName], { timeoutMs: 20_000 })
rmSync(target.tempDir, { recursive: true, force: true })
}