* Probe connection health by running a simple command
(
config: SSHConnectionConfig,
timeoutMs: number,
key: string,
controlPath = getControlPath(config),
abortSignal?: AbortSignal
)
| 456 | * Probe connection health by running a simple command |
| 457 | */ |
| 458 | private async probeConnection( |
| 459 | config: SSHConnectionConfig, |
| 460 | timeoutMs: number, |
| 461 | key: string, |
| 462 | controlPath = getControlPath(config), |
| 463 | abortSignal?: AbortSignal |
| 464 | ): Promise<void> { |
| 465 | if (abortSignal?.aborted) { |
| 466 | throw new Error(SSH_OPERATION_ABORTED_ERROR); |
| 467 | } |
| 468 | |
| 469 | const promptService = sshPromptService; |
| 470 | const canPromptInteractively = isInteractiveHostKeyApprovalAvailable(); |
| 471 | |
| 472 | const args: string[] = ["-T"]; // No PTY needed for probe |
| 473 | |
| 474 | if (config.port) { |
| 475 | args.push("-p", config.port.toString()); |
| 476 | } |
| 477 | |
| 478 | if (config.identityFile) { |
| 479 | args.push("-i", config.identityFile); |
| 480 | args.push("-o", "LogLevel=ERROR"); |
| 481 | } |
| 482 | |
| 483 | // Connection multiplexing |
| 484 | args.push("-o", "ControlMaster=auto"); |
| 485 | args.push("-o", `ControlPath=${controlPath}`); |
| 486 | args.push("-o", "ControlPersist=60"); |
| 487 | |
| 488 | // ConnectTimeout covers the entire SSH handshake including SSH_ASKPASS waits. |
| 489 | // When host-key prompts are possible, use the longer prompt timeout so SSH |
| 490 | // doesn't self-terminate while the user is responding to the dialog. |
| 491 | // The Node.js timer still provides fast-fail for unreachable hosts. |
| 492 | const connectTimeout = canPromptInteractively |
| 493 | ? Math.ceil(HOST_KEY_APPROVAL_TIMEOUT_MS / 1000) |
| 494 | : Math.min(Math.ceil(timeoutMs / 1000), 15); |
| 495 | args.push("-o", `ConnectTimeout=${connectTimeout}`); |
| 496 | args.push("-o", "ServerAliveInterval=5"); |
| 497 | args.push("-o", "ServerAliveCountMax=2"); |
| 498 | |
| 499 | // Scope insecure host-key fallback to explicitly headless contexts where |
| 500 | // no verification service is wired (e.g. CLI/test harness without UI). |
| 501 | // Responder liveness only affects askpass prompt mechanics, not trust policy. |
| 502 | appendOpenSSHHostKeyPolicyArgs(args); |
| 503 | |
| 504 | args.push(config.host, "echo ok"); |
| 505 | |
| 506 | log.debug(`SSH probe: ssh ${args.join(" ")}`); |
| 507 | |
| 508 | let stderr = ""; |
| 509 | // Wired to the probe timer inside the Promise; the askpass callback |
| 510 | // calls this to transition from connection phase (10s) to interaction |
| 511 | // phase (60s) when a host-key prompt is detected. |
| 512 | let extendDeadline: ((ms: number) => void) | undefined; |
| 513 | |
| 514 | // Set up SSH_ASKPASS for interactive host-key verification. |
| 515 | // The askpass helper exchanges prompt/response text through temp files. |
no test coverage detected