fix: keep active ssh downloads alive

This commit is contained in:
Codex
2026-06-11 18:08:56 +00:00
parent 0a9d07c760
commit a8d49845ca
5 changed files with 132 additions and 23 deletions
+36 -5
View File
@@ -12,7 +12,12 @@ export interface SshRemoteCommandStreamHandlers {
export interface SshRemoteCommandExecutor {
runRemoteCommand(remoteCommand: string, input?: string): Promise<SshCaptureResult>;
streamRemoteCommand?(remoteCommand: string, handlers: SshRemoteCommandStreamHandlers, input?: string): Promise<SshCaptureResult>;
streamRemoteCommand?(
remoteCommand: string,
handlers: SshRemoteCommandStreamHandlers,
input?: string,
options?: { inactivityTimeoutMs?: number },
): Promise<SshCaptureResult>;
}
export interface SshFileTransferCommandBuilders {
@@ -32,6 +37,7 @@ interface SshFileTransferCliOptions {
action: "upload" | "download";
localPath: string;
remotePath: string;
inactivityTimeoutMs?: number;
}
interface SshFileTransferStat {
@@ -111,7 +117,7 @@ export async function runSshFileTransferOperation(
return 0;
}
const read = await downloadRemoteFileVerified(invocation, executor, builders, options.remotePath, localPath);
const read = await downloadRemoteFileVerified(invocation, executor, builders, options.remotePath, localPath, options.inactivityTimeoutMs);
const verification = buildTransferVerification(
{ side: "remote", path: options.remotePath, ...read.remote },
{ side: "local", path: localPath, ...read.local },
@@ -143,6 +149,7 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
const action = args[0];
if (action !== "upload" && action !== "download") throw new Error("ssh file transfer requires upload or download");
const positionals: string[] = [];
let inactivityTimeoutMs: number | undefined;
for (let index = 1; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--") {
@@ -152,6 +159,21 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
if (arg === "--chunk-bytes" || arg === "--block-bytes") {
throw new Error(`unsupported ssh ${action} option: ${arg}; downloads use host.ssh.tcp-pool stdout streaming and no longer support the legacy base64 block reader`);
}
if (arg === "--inactivity-timeout-ms" || arg === "--runtime-timeout-ms") {
const value = args[index + 1];
if (value === undefined) throw new Error(`ssh ${action} ${arg} requires a positive integer value`);
index += 1;
inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(value, `ssh ${action} ${arg}`);
continue;
}
if (arg.startsWith("--inactivity-timeout-ms=")) {
inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(arg.slice("--inactivity-timeout-ms=".length), `ssh ${action} --inactivity-timeout-ms`);
continue;
}
if (arg.startsWith("--runtime-timeout-ms=")) {
inactivityTimeoutMs = parsePositiveRuntimeTimeoutMs(arg.slice("--runtime-timeout-ms=".length), `ssh ${action} --runtime-timeout-ms`);
continue;
}
if (arg === "--help" || arg === "-h" || arg === "help") {
throw new Error(`ssh ${action} usage: trans <route> ${action} ${action === "upload" ? "<local-file> <remote-file>" : "<remote-file> <local-file>"}`);
}
@@ -164,8 +186,16 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
const [first, second] = positionals;
if (!first || !second) throw new Error(`ssh ${action} paths must be non-empty`);
return action === "upload"
? { action, localPath: first, remotePath: second }
: { action, remotePath: first, localPath: second };
? { action, localPath: first, remotePath: second, inactivityTimeoutMs }
: { action, remotePath: first, localPath: second, inactivityTimeoutMs };
}
function parsePositiveRuntimeTimeoutMs(value: string, label: string): number {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
throw new Error(`${label} must be a positive integer number of milliseconds`);
}
return parsed;
}
async function writeRemoteFileVerified(
@@ -210,6 +240,7 @@ async function downloadRemoteFileVerified(
builders: SshFileTransferCommandBuilders,
remotePath: string,
localPath: string,
inactivityTimeoutMs?: number,
): Promise<SshFileTransferDownloadResult> {
if (executor.streamRemoteCommand === undefined) {
throw new SshFileTransferError("ssh download requires a tcp-pool stdout stream sink", {
@@ -248,7 +279,7 @@ async function downloadRemoteFileVerified(
hash.update(chunk);
emitDownloadProgress(invocation, remotePath, chunkCount, actualBytes, remote.bytes, chunk.length, startedAtMs);
},
});
}, undefined, { inactivityTimeoutMs });
await closeWriteStream(output);
if (result.exitCode !== 0) {
throw new SshFileTransferError("remote ssh download stream failed", {