/** * Transport on top of the system OpenSSH * * Connections are reused through the ControlMaster mechanism: the first * command brings up the control connection, and every later one rides it — * from this process, from a sibling client window, from any other process on * the machine. The server sees one login instead of one login per command. */ import { type ServerPassport } from './passport.js'; import { type SshRuntime } from './runtime-check.js'; import { type RunnerConfig } from './ssh-args.js'; import type { CommandRunner, ExecOptions, ExecResult, MasterCloseOutcome, PingResult, RunnerStats, TransferOptions } from './types.js'; /** The default command deadline: also what the `ssh_exec` schema promises via `ssh-executor.ts` */ export declare const DEFAULT_EXEC_TIMEOUT_MS = 30000; /** * Transport for a single destination (user + host + port) */ export declare class OpenSshRunner implements CommandRunner { private readonly config; private readonly runtime; private commandCount; private transferCount; private lastError?; /** The first command brings up the master; the rest wait on it to avoid logging in twice */ private firstCommandGate?; /** When a command last proved the master is up */ private masterSeenAt; private askpassScriptPath?; /** The last passport read — for messages where waiting for it isn't an option */ private knownPassport?; /** Whether the destination only speaks the classic scp protocol */ private legacyScp; constructor(config: RunnerConfig, runtime: SshRuntime); /** Where this transport connects to — for logs and the cache key */ get destination(): string; exec(command: string, options?: ExecOptions): Promise; upload(localPath: string, remotePath: string, options?: TransferOptions): Promise; download(remotePath: string, localPath: string, options?: TransferOptions): Promise; ping(options?: { timeoutMs?: number; }): Promise; stats(): Promise; closeMaster(): Promise; /** Whether the control connection is alive */ private checkMaster; private capabilities; private buildEnv; /** * Whether the master is up right now. * * Judged by the clock rather than by asking `ssh -O check`: a probe is an * extra process on every command. The master stays up for ControlPersist * seconds after the last command and doesn't leave early on its own, so * within that window "up" is a correct answer, and past it the gate simply closes again. */ private masterLikelyUp; /** * Run a command, routing the first one through the gate. * * Without the gate, commands on a cold profile would each open their own * connection and fire a burst of logins instead of one. The gate is closed * before every cold start, not just once for the transport's lifetime: the * connection closes both on command and after the idle deadline, and after * that the profile is cold again. */ private execGuarded; private execOnce; /** * Mark the command for cancellation, and guard it where the server can. * * Killing the local ssh closes the channel, but doesn't necessarily end * the process on the server. The `timeout` utility finishes the job where * it exists; the marker goes on either way, because a server without * `timeout` is exactly the one whose commands nothing else would stop. * * The marker rides as an argument after the command, where the shell puts * it into `$1` and leaves `$0` as it was. The environment would have hidden * it from other users on the server, but `environ` is unreadable to anyone * but the process itself — including root — so nothing could find it there. * * `exit $?` on a line of its own keeps the shell from replacing itself with * the command: a shell that execs away takes the marker with it, and the * only process left holding it is the timeout watcher, which by then has no * relation to the command. The newline matters — a command ending in `&` * makes `; exit` a syntax error. * * The command language is declared explicitly: bash when it's available, * otherwise sh. This matters because on Debian and Ubuntu sh is dash — * bash-specific constructs that work fine in a login shell would break * under it, and only on some servers. */ private wrapCommand; /** * Stop the cancelled command on the server. * * Sent on its own connection and given its own deadline: the call it * belongs to is already over, and its cancellation signal would abort this * one too. A stop that fails changes nothing for the caller — the * cancellation stands either way — so it is reported to the log and no * further. * * Without a marker there is nothing to find: the command went unwrapped, * and stopping it was never possible. */ private stopOnServer; /** * Server passport: one probe per session for a destination. * * The probe bypasses the first-command gate and the remote guard — * otherwise it would be a chicken and egg problem: it's the one finding * out the command language, while commands sitting in the gate are * themselves waiting on the passport. */ passport(): Promise; /** * Transfer with a fallback to the classic protocol. * * On servers without an sftp subsystem (routers, embedded systems) modern * scp fails, while the classic protocol works. There's no way to tell such * a server apart in advance, so it's tried once and the destination's answer is remembered. */ private transfer; /** * A single transfer attempt. * * Returns a failed transfer rather than throwing it: the caller decides * whether to retry it on another protocol. Cancellation and timeout are a * different case — they propagate up right away. */ private transferOnce; } /** * Destination key: the profile name is deliberately left out of it, so two * profiles pointing at the same server share one connection instead of holding * two separate ones to the same host. */ export declare function runnerKey(config: RunnerConfig): string; /** * Get the transport for a profile. * * The key is the destination together with the credential fingerprint, the * same one used in the shared socket's name. Two profiles with the same key * for one server share a connection; a profile with different credentials * gets its own, rather than reusing someone else's or evicting it. */ export declare function getOpenSshRunner(config: RunnerConfig): Promise; /** Close every control connection (used in tests) */ export declare function closeAllRunners(): Promise; /** Reset the transport cache without touching the connections */ export declare function resetRunnerCache(): void;