/** * SSH Readiness Service * * Waits for SSH to become available on target host with exponential backoff */ import { spawn } from 'node:child_process'; import { FuelGauge } from '../cli/fuel-gauge'; import { log } from '../cli/prompts'; export interface SSHResult { success: boolean; error?: string; attempts?: number; } /** * Wait for SSH to become available on target host * Execution function - polls SSH connectivity with backoff * * @param host - Target host IP address * @param user - SSH user (default: root) * @param timeoutSeconds - Timeout in seconds (default: 120) * @returns SSH readiness result */ export async function waitForSSH( host: string, user = 'root', timeoutSeconds = 120, ): Promise { const startTime = Date.now(); const timeoutMs = timeoutSeconds * 1000; let attempt = 0; const gauge = new FuelGauge(`Waiting for SSH access to ${user}@${host}`); gauge.start(); while (Date.now() - startTime < timeoutMs) { attempt++; gauge.addOutput(`Attempt ${attempt} - connecting to ${user}@${host}...`); const connected = await trySSHConnection(host, user); if (connected) { gauge.stop(true); log.success(`SSH ready after ${attempt} attempt${attempt === 1 ? '' : 's'}`); return { success: true, attempts: attempt }; } // Calculate backoff delay: 1s, 1.5s, 2.25s, ..., max 10s const backoffDelay = Math.min(1000 * 1.5 ** (attempt - 1), 10000); await sleep(backoffDelay); } gauge.stop(false); return { success: false, error: `SSH connection timeout after ${timeoutSeconds} seconds (${attempt} attempts)\n\nTarget: ${user}@${host}\n\nPossible causes:\n - Container failed to start\n - SSH service not configured\n - Firewall blocking connection\n - Wrong IP address`, attempts: attempt, }; } /** * Try SSH connection to host * Execution function - attempts single SSH connection * * @param host - Target host IP * @param user - SSH user * @returns True if connection successful */ async function trySSHConnection(host: string, user: string): Promise { return new Promise((resolve) => { // Try SSH connection with short timeout const child = spawn( 'ssh', [ '-o', 'ConnectTimeout=5', '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'LogLevel=ERROR', `${user}@${host}`, 'echo', 'ready', ], { stdio: ['ignore', 'pipe', 'pipe'], }, ); let output = ''; child.stdout.on('data', (data) => { output += data.toString(); }); child.on('close', (exitCode) => { // Success if command executed and returned "ready" if (exitCode === 0 && output.trim() === 'ready') { resolve(true); } else { resolve(false); } }); child.on('error', () => { resolve(false); }); // Timeout after 6 seconds setTimeout(() => { child.kill(); resolve(false); }, 6000); }); } /** * Sleep for specified milliseconds * Utility function * * @param ms - Milliseconds to sleep */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); }